Capítulo 11, Básico
Códigos de status e erros com HTTPException
O código de status é a primeira coisa que o cliente lê. Acertar o código é o que faz um cliente (e um humano) saber, sem ler o corpo, se deu certo, se o erro foi dele ou do servidor.
Os códigos que importam
| Código | Nome | Quando usar |
|---|---|---|
| 200 | OK | A leitura ou a alteração deu certo |
| 201 | Created | Um recurso novo foi criado |
| 204 | No Content | Deu certo, e não há corpo (um DELETE, por exemplo) |
| 400 | Bad Request | A requisição está malformada ou viola uma regra de negócio |
| 401 | Unauthorized | O cliente não está autenticado (quem é você?) |
| 403 | Forbidden | O cliente está autenticado, mas não tem permissão |
| 404 | Not Found | O recurso não existe |
| 409 | Conflict | Conflito com o estado atual (um e-mail já cadastrado) |
| 422 | Unprocessable Entity | Os dados enviados não passam na validação (o FastAPI usa sozinho) |
| 500 | Internal Server Error | Um bug no servidor |
A primeira dezena de dígitos conta a história: 2xx é sucesso, 4xx é erro do cliente (quem chamou pode corrigir) e 5xx é erro do servidor (o cliente não pode fazer nada).
Declarar o status de sucesso
Por padrão, toda rota devolve 200. Para um POST que cria, o certo é 201, e o módulo status oferece constantes com nome, que se leem sozinhas:
from fastapi import FastAPI, HTTPException, status
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class UsuarioEntrada(BaseModel):
nome: str
email: str
usuarios: dict[str, UsuarioEntrada] = {}
@app.post("/usuarios", status_code=status.HTTP_201_CREATED)
def criar(usuario: UsuarioEntrada):
if usuario.email in usuarios:
raise HTTPException(status.HTTP_409_CONFLICT, detail="E-mail já cadastrado")
usuarios[usuario.email] = usuario
return {"mensagem": "Usuário criado", "email": usuario.email}
cliente = TestClient(app)
corpo = {"nome": "Mohit", "email": "mohit@exemplo.com"}
print(cliente.post("/usuarios", json=corpo).status_code)
repetido = cliente.post("/usuarios", json=corpo)
print(repetido.status_code, repetido.json())
201
409 {'detail': 'E-mail já cadastrado'}
HTTPException: interromper e responder o erro
O raise HTTPException(...) interrompe a função na hora e devolve a resposta de erro, com o detail que você escolheu. Um 404 clássico:
@app.get("/usuarios/{email}")
def obter(email: str):
if email not in usuarios:
raise HTTPException(status_code=404, detail="Usuário não encontrado")
return usuarios[email]
print(cliente.get("/usuarios/mohit@exemplo.com").json())
resposta = cliente.get("/usuarios/nao@existe.com")
print(resposta.status_code, resposta.json())
{'nome': 'Mohit', 'email': 'mohit@exemplo.com'}
404 {'detail': 'Usuário não encontrado'}
O HTTPException aceita ainda headers, que o 401 usa para dizer ao cliente como autenticar (capítulo 20).
204: sucesso sem corpo
Um DELETE bem-sucedido não tem nada a dizer. O status 204 significa isso, e a resposta vem sem corpo:
@app.delete("/usuarios/{email}", status_code=status.HTTP_204_NO_CONTENT)
def remover(email: str):
if email not in usuarios:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Usuário não encontrado")
del usuarios[email]
resposta = cliente.delete("/usuarios/mohit@exemplo.com")
print(resposta.status_code, resposta.content)
204 b''
Documentar os erros que a rota pode devolver
Por padrão, a documentação só conhece o sucesso e o 422. O parâmetro responses acrescenta os outros códigos ao documento OpenAPI, para quem consome a API saber o que esperar:
@app.get("/contas/{conta_id}", responses={404: {"description": "Conta não encontrada"}})
def conta(conta_id: int):
raise HTTPException(status_code=404, detail="Conta não encontrada")
respostas = app.openapi()["paths"]["/contas/{conta_id}"]["get"]["responses"]
print(sorted(respostas))
['200', '404', '422']
Nunca devolva um erro com status 200
Muito material ensina a escrever return {"error": "não encontrado"}. O resultado é uma resposta de sucesso (200) com uma mensagem de erro dentro, e o cliente, que olha primeiro o status, acha que deu tudo certo:
@app.get("/errado/{item_id}")
def errado(item_id: int):
return {"error": "Item não encontrado"}
resposta = cliente.get("/errado/1")
print(resposta.status_code, resposta.json())
200 {'error': 'Item não encontrado'}
E aquele "envelope" com status e message?
Alguns guias recomendam sempre devolver
{"status": "success", "message": ..., "data": ...}. Eu não faço isso: o protocolo HTTP já tem o status, e um envelope duplica a informação (e convida ao erro do parágrafo acima). O que vale é um formato de erro consistente em toda a API, e o próximo capítulo mostra como garantir isso em um lugar só.
Exercício 1
Um saque com saldo insuficiente
Crie POST /saques que receba {"valor": ...} e tenha um saldo de 100. Se o valor passar do saldo, devolva 400 com a mensagem "Saldo insuficiente". Se for válido, desconte e devolva 201 com o novo saldo.