Pular para o conteúdo

    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ódigoNomeQuando usar
    200OKA leitura ou a alteração deu certo
    201CreatedUm recurso novo foi criado
    204No ContentDeu certo, e não há corpo (um DELETE, por exemplo)
    400Bad RequestA requisição está malformada ou viola uma regra de negócio
    401UnauthorizedO cliente não está autenticado (quem é você?)
    403ForbiddenO cliente está autenticado, mas não tem permissão
    404Not FoundO recurso não existe
    409ConflictConflito com o estado atual (um e-mail já cadastrado)
    422Unprocessable EntityOs dados enviados não passam na validação (o FastAPI usa sozinho)
    500Internal Server ErrorUm 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:

    basico/cap11_status_http.pylinhas 10 a 37
    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())
    
    Saída
    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:

    basico/cap11_status_http.pylinhas 42 a 51
    @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())
    
    Saída
    {'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:

    basico/cap11_status_http.pylinhas 56 a 64
    @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)
    
    Saída
    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:

    basico/cap11_status_http.pylinhas 69 a 75
    @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))
    
    Saída
    ['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:

    basico/cap11_status_http.pylinhas 80 a 86
    @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())
    
    Saída
    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.