Manipulação de erros¶
Há diversas situações em que você precisa notificar um erro a um cliente que está utilizando a sua API.
Esse cliente pode ser um browser com um frontend, o código de outra pessoa, um dispositivo IoT, etc.
Pode ser que você precise comunicar ao cliente que:
- O cliente não tem direitos para realizar aquela operação.
- O cliente não tem acesso aquele recurso.
- O item que o cliente está tentando acessar não existe.
- etc.
Nesses casos, você normalmente retornaria um HTTP status code próximo ao status code na faixa do status code 400 (do 400 ao 499).
Isso é bastante similar ao caso do HTTP status code 200 (do 200 ao 299). Esses "200" status codes significam que, de algum modo, houve sucesso na requisição.
Os status codes na faixa dos 400 significam que houve um erro por parte do cliente.
Você se lembra de todos aqueles erros (e piadas) a respeito do "404 Not Found"?
Use o HTTPException¶
Para retornar ao cliente responses HTTP com erros, use o HTTPException.
Import HTTPException¶
from readyapi import HTTPException, ReadyAPI
app = ReadyAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
Lance o HTTPException no seu código.¶
HTTPException, ao fundo, nada mais é do que a conjunção entre uma exceção comum do Python e informações adicionais relevantes para APIs.
E porque é uma exceção do Python, você não retorna (return) o HTTPException, você lança o (raise) no seu código.
Isso também significa que, se você está escrevendo uma função de utilidade, a qual você está chamando dentro da sua função de operações de caminhos, e você lança o HTTPException dentro da função de utilidade, o resto do seu código não será executado dentro da função de operações de caminhos. Ao contrário, o HTTPException irá finalizar a requisição no mesmo instante e enviará o erro HTTP oriundo do HTTPException para o cliente.
O benefício de lançar uma exceção em vez de retornar um valor ficará mais evidente na seção sobre Dependências e Segurança.
Neste exemplo, quando o cliente pede, na requisição, por um item cujo ID não existe, a exceção com o status code 404 é lançada:
from readyapi import HTTPException, ReadyAPI
app = ReadyAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
A response resultante¶
Se o cliente faz uma requisição para http://example.com/items/foo (um item_id "foo"), esse cliente receberá um HTTP status code 200, e uma resposta JSON:
{
"item": "The Foo Wrestlers"
}
Mas se o cliente faz uma requisição para http://example.com/items/bar (ou seja, um não existente item_id "bar"), esse cliente receberá um HTTP status code 404 (o erro "não encontrado" — not found error), e uma resposta JSON:
{
"detail": "Item not found"
}
Dica
Quando você lançar um HTTPException, você pode passar qualquer valor convertível em JSON como parâmetro de detail, e não apenas str.
Você pode passar um dict ou um list, etc.
Esses tipos de dados são manipulados automaticamente pelo ReadyAPI e convertidos em JSON.
Adicione headers customizados¶
Há certas situações em que é bastante útil poder adicionar headers customizados no HTTP error. Exemplo disso seria adicionar headers customizados para tipos de segurança.
Você provavelmente não precisará utilizar esses headers diretamente no seu código.
Mas caso você precise, para um cenário mais complexo, você pode adicionar headers customizados:
from readyapi import HTTPException, ReadyAPI
app = ReadyAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
return {"item": items[item_id]}
Instalando manipuladores de exceções customizados¶
Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette
Digamos que você tenha uma exceção customizada UnicornException que você (ou uma biblioteca que você use) precise lançar (raise).
Nesse cenário, se você precisa manipular essa exceção de modo global com o ReadyAPI, você pode adicionar um manipulador de exceção customizada com @app.exception_handler().
from readyapi import ReadyAPI, Request
from readyapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = ReadyAPI()
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
Nesse cenário, se você fizer uma requisição para /unicorns/yolo, a operação de caminho vai lançar (raise) o UnicornException.
Essa exceção será manipulada, contudo, pelo unicorn_exception_handler.
Dessa forma você receberá um erro "limpo", com o HTTP status code 418 e um JSON com o conteúdo:
{"message": "Oops! yolo did something. There goes a rainbow..."}
Detalhes Técnicos
Você também pode usar from starlette.requests import Request and from starlette.responses import JSONResponse.
ReadyAPI disponibiliza o mesmo starlette.responses através do readyapi.responses por conveniência ao desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette. O mesmo acontece com o Request.
Sobrescreva o manipulador padrão de exceções¶
ReadyAPI tem alguns manipuladores padrão de exceções.
Esses manipuladores são os responsáveis por retornar o JSON padrão de respostas quando você lança (raise) o HTTPException e quando a requisição tem dados invalidos.
Você pode sobrescrever esses manipuladores de exceção com os seus próprios manipuladores.
Sobrescreva exceções de validação da requisição¶
Quando a requisição contém dados inválidos, ReadyAPI internamente lança para o RequestValidationError.
Para sobrescrevê-lo, importe o RequestValidationError e use-o com o @app.exception_handler(RequestValidationError) para decorar o manipulador de exceções.
from readyapi import HTTPException, ReadyAPI
from readyapi.exceptions import RequestValidationError
from readyapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = ReadyAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
Se você for ao /items/foo, em vez de receber o JSON padrão com o erro:
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
você receberá a versão em texto:
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
RequestValidationError vs ValidationError¶
Aviso
Você pode pular estes detalhes técnicos caso eles não sejam importantes para você neste momento.
RequestValidationError é uma subclasse do ValidationError existente no Pydantic.
ReadyAPI faz uso dele para que você veja o erro no seu log, caso você utilize um modelo de Pydantic em response_model, e seus dados tenham erro.
Contudo, o cliente ou usuário não terão acesso a ele. Ao contrário, o cliente receberá um "Internal Server Error" com o HTTP status code 500.
E assim deve ser porque seria um bug no seu código ter o ValidationError do Pydantic na sua response, ou em qualquer outro lugar do seu código (que não na requisição do cliente).
E enquanto você conserta o bug, os clientes / usuários não deveriam ter acesso às informações internas do erro, porque, desse modo, haveria exposição de uma vulnerabilidade de segurança.
Do mesmo modo, você pode sobreescrever o HTTPException.
Por exemplo, você pode querer retornar uma response em plain text ao invés de um JSON para os seguintes erros:
from readyapi import HTTPException, ReadyAPI
from readyapi.exceptions import RequestValidationError
from readyapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = ReadyAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
Detalhes Técnicos
Você pode usar from starlette.responses import PlainTextResponse.
ReadyAPI disponibiliza o mesmo starlette.responses como readyapi.responses, como conveniência a você, desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette.
Use o body do RequestValidationError.¶
O RequestValidationError contém o body que ele recebeu de dados inválidos.
Você pode utilizá-lo enquanto desenvolve seu app para conectar o body e debugá-lo, e assim retorná-lo ao usuário, etc.
Tente enviar um item inválido como este:
{
"title": "towel",
"size": "XL"
}
Você receberá uma response informando-o de que a data é inválida, e contendo o body recebido:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
O HTTPException do ReadyAPI vs o HTTPException do Starlette.¶
O ReadyAPI tem o seu próprio HTTPException.
E a classe de erro HTTPException do ReadyAPI herda da classe de erro do HTTPException do Starlette.
A diferença entre os dois é a de que o HTTPException do ReadyAPI permite que você adicione headers que serão incluídos nas responses.
Esses headers são necessários/utilizados internamente pelo OAuth 2.0 e também por outras utilidades de segurança.
Portanto, você pode continuar lançando o HTTPException do ReadyAPI normalmente no seu código.
Porém, quando você registrar um manipulador de exceção, você deve registrá-lo através do HTTPException do Starlette.
Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar o HTTPException, o seu manipulador de exceção poderá capturar esse lançamento e tratá-lo.
from starlette.exceptions import HTTPException as StarletteHTTPException
Re-use os manipulares de exceção do ReadyAPI¶
Se você quer usar a exceção em conjunto com o mesmo manipulador de exceção default do ReadyAPI, você pode importar e re-usar esses manipuladores de exceção do readyapi.exception_handlers:
from readyapi import HTTPException, ReadyAPI
from readyapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from readyapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = ReadyAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {repr(exc)}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
Nesse exemplo você apenas imprime (print) o erro com uma mensagem expressiva. Mesmo assim, dá para pegar a ideia. Você pode usar a exceção e então apenas re-usar o manipulador de exceção default.