Перейти к содержанию

Управление 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])
curl -X GET http://127.0.0.1:8080
400: Bad Request

Вызов HTTP-ошибки веб-сервером

Веб-сервер вызовет ошибку автоматически, если запрос не может быть обработан.

Not Found — 404

from rapidy import Rapidy
from rapidy.http import get

@get('/')
async def handler() -> ...:
    ...

app = Rapidy(http_route_handlers=[handler])
curl -X POST http://127.0.0.1:8080/some_api
404: Not Found

Method Not Allowed — 405

from rapidy import Rapidy
from rapidy.http import get


@get('/')
async def handler() -> str:
    return 'ok'


rapidy = Rapidy(http_route_handlers=[handler])
curl -X POST http://127.0.0.1:8080
405: Method Not Allowed

Ошибка валидации (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])
curl -X POST \
-H "Content-Type: application/json" -d '{"data": "d"}' -v \
http://127.0.0.1:8080
< 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:

try:
    return await handler(request)
except HTTPValidationFailure as validation_failure_error:
    errors = validation_failure_error.validation_errors
    ...

HTTPValidationFailure наследуется от HTTPUnprocessableEntity.

Это значит, что обе ошибки можно обработать через HTTPUnprocessableEntity, если не требуется раскрывать клиенту подробности ошибки.

try:
    return await handler(request)
except 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)