Управление 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('Ошибка валидации: `%s request: %s`', str(request.rel_url), validation_errors)
raise validation_failure_error
except HTTPInternalServerError as server_error:
logger.info('Внутренняя ошибка сервера: %s', server_error)
raise server_error
except HTTPException as unhandled_http_error:
raise unhandled_http_error
except Exception as unhandled_error:
logger.exception('Ошибка при обработке `%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)