Управление HTTP-ошибками
Описание
HTTP-ошибки — это объекты с определённой логикой, которые могут возвращать ответ веб-сервера с предопределённым HTTP-кодом.
Ошибки вызываются с помощью конструкции raise.
from rapidy import Rapidy
from rapidy.http import get, HTTPBadRequest
@get('/')
async def handler() -> None:
raise HTTPBadRequest() # 400
app = Rapidy(http_route_handlers=[handler])
HTTP-ошибки могут быть вызваны как разработчиком, так и самим веб-сервером Rapidy, если клиент или сервер совершат ошибку.
Все ошибки находятся в модуле rapidy.web_exceptions, но их также можно импортировать из rapidy.http.
Виды HTTP-ошибок
Rapidy поддерживает четыре типа HTTP-ошибок:
- 2xx — успешные ответы (базовый класс —
HTTPSuccessful) - 3xx — перенаправления (базовый класс —
HTTPRedirection) - 4xx — ошибки клиента (базовый класс —
HTTPClientError) - 5xx — ошибки сервера (базовый класс —
HTTPServerError)
Базовые классы можно использовать для обработки всех дочерних ошибок.
Подробнее о HTTP-ошибках можно прочитать в документации aiohttp здесь.
Вызов HTTP-ошибок
Вызов HTTP-ошибки разработчиком
Разработчик может вызвать ошибку самостоятельно, если обработка запроса идёт по неуспешному сценарию.
from rapidy import Rapidy
from rapidy.http import get, HTTPBadRequest
@get('/')
async def handler() -> None:
raise HTTPBadRequest() # 400
app = Rapidy(http_route_handlers=[handler])
Вызов HTTP-ошибки веб-сервером
Веб-сервер вызовет ошибку автоматически, если запрос не может быть обработан.
Not Found — 404
Method Not Allowed — 405
Ошибка валидации (Validation Error)
При неуспешной валидации веб-запроса клиент получит ответ в формате application/json с описанием ошибки.
from pydantic import BaseModel, Field
from rapidy import Rapidy
from rapidy.http import post, Body
class BodyRequestSchema(BaseModel):
data: str = Field(min_length=3, max_length=20)
@post('/')
async def handler(
body: BodyRequestSchema = Body(),
) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy(http_route_handlers=[handler])
< HTTP/1.1 422 Unprocessable Entity ...
{
"errors": [
{
"loc": ["body", "data"],
"type": "string_too_short",
"msg": "String should have at least 3 characters",
"ctx": {"min_length": 3}
}
]
}
HTTP-ошибка HTTPValidationFailure содержит список ошибок в поле validation_errors.
Чтобы получить доступ к этим ошибкам, можно перехватить HTTPValidationFailure:
HTTPValidationFailure наследуется от HTTPUnprocessableEntity.
Это значит, что обе ошибки можно обработать через HTTPUnprocessableEntity, если не требуется раскрывать клиенту подробности ошибки.
Перехват ошибок
Иногда требуется перехватить ошибку, например, чтобы изменить ответ сервера.
Для этого можно использовать middleware:
from logging import getLogger
from rapidy.http import (
middleware, Request, StreamResponse, HTTPValidationFailure, HTTPInternalServerError, HTTPException,
)
from rapidy.typedefs import CallNext
logger = getLogger(__name__)
@middleware
async def error_catch_middleware(
request: Request,
call_next: CallNext,
) -> StreamResponse:
try:
return await call_next(request)
except HTTPValidationFailure as validation_failure_error:
validation_errors = validation_failure_error.validation_errors
logger.debug('Validation error: `%s request: %s`', str(request.rel_url), validation_errors)
raise validation_failure_error
except HTTPInternalServerError as server_error:
logger.info('Internal server error: %s', server_error)
raise server_error
except HTTPException as unhandled_http_error:
raise unhandled_http_error
except Exception as unhandled_error:
logger.exception('Unhandled error `%s`: %s', str(request.rel_url), unhandled_error)
raise HTTPInternalServerError
Пример обработки всех ошибок с унифицированным ответом
from http import HTTPStatus
from logging import getLogger
from typing import Any
from pydantic import BaseModel
from rapidy import Rapidy, run_app
from rapidy.http import (
get,
middleware,
Request,
Response,
StreamResponse,
HTTPValidationFailure,
HTTPClientError,
HTTPInternalServerError,
HTTPException,
)
from rapidy.typedefs import CallNext, ValidationErrorList
logger = getLogger(__name__)
class ServerResponse(BaseModel):
message: str = 'Success'
result: Any | None = None
errors: ValidationErrorList | None = None
@middleware
async def error_catch_middleware(
request: Request,
call_next: CallNext,
response: Response,
) -> StreamResponse | ServerResponse:
try:
return await call_next(request)
except HTTPValidationFailure as validation_failure_error:
validation_errors = validation_failure_error.validation_errors
logger.debug('Validation error while processing: `%s request: %s', str(request.rel_url), validation_errors)
response.set_status(validation_failure_error.status)
return ServerResponse(message='Validation error', errors=validation_errors)
except HTTPClientError as client_error: # all other `4xx' errors
logger.info('Client error while processing: %s request: %s', str(request.rel_url), client_error)
response.set_status(client_error.status)
return ServerResponse(message=client_error.reason)
except HTTPInternalServerError as server_error:
logger.info('Internal error - server raise HTTPInternalServerError: %s', server_error)
response.set_status(server_error.status)
return ServerResponse(message=server_error.reason)
except HTTPException as unhandled_http_error: # all other unhandled http-errors
raise unhandled_http_error
except Exception as unhandled_error:
logger.exception('Internal error while processing `%s` error: %s', str(request.rel_url), unhandled_error)
response.set_status(HTTPStatus.INTERNAL_SERVER_ERROR)
return ServerResponse(message='Internal server error')
@get('/')
async def handler() -> ServerResponse:
return ServerResponse(result={'hello': 'rapidy'})
app = Rapidy(middlewares=[error_catch_middleware], http_route_handlers=[handler])
if __name__ == '__main__':
run_app(app)