1

HTTP 200 não significa que seus dados estão corretos: validando respostas de APIs com Python

Quando comecei a trabalhar mais seriamente com APIs, eu tinha um hábito bem simples: se a resposta fosse 200 OK, eu assumia que estava tudo certo.

Algo assim:

import requests

response = requests.get(url)

if response.status_code == 200:
data = response.json()
print(data)

Bonito, curto e aparentemente funcional.

O problema é que 200 OK responde apenas a uma pergunta: a requisição HTTP foi processada com sucesso?

Ele não responde perguntas como:

Os campos que eu esperava realmente existem?
O valor retornado faz sentido?
O timestamp é recente?
Algum campo importante veio como null?
A estrutura da resposta mudou?
Estou usando dados atuais ou algo que já ficou velho?

Quando começamos a trabalhar com dados financeiros ou qualquer outra informação atualizada com frequência, essa diferença fica bem importante.

O servidor respondeu. E agora?

Imagine uma API que deveria retornar algo parecido com isto:

{
"symbol": "BTCUSDT",
"price": 58432.15,
"timestamp": 1789027200
}

A requisição retorna 200.

Ótimo.

Mas e se amanhã ela retornar:

{
"symbol": "BTCUSDT",
"price": null,
"timestamp": 1789027200
}

Tecnicamente, a requisição continua funcionando.

Para o seu programa, porém, talvez não.

Esse é o primeiro ponto que comecei a prestar mais atenção: sucesso de transporte não é a mesma coisa que sucesso dos dados.

Primeiro: não confie cegamente no JSON

Uma pequena melhoria já ajuda bastante:

import requests

response = requests.get(url, timeout=10)
response.raise_for_status()

try:
data = response.json()
except ValueError:
raise RuntimeError("A API não retornou um JSON válido")

Aqui já estamos verificando duas coisas diferentes.

raise_for_status() detecta problemas HTTP, enquanto o bloco try verifica se a resposta realmente pode ser interpretada como JSON.

Ainda assim, falta uma etapa importante.

Precisamos validar o conteúdo.

Verifique os campos que realmente importam

Digamos que nosso programa precise de três campos:

required_fields = ["symbol", "price", "timestamp"]

for field in required_fields:
if field not in data:
raise ValueError(f"Campo ausente: {field}")

Isso parece básico. E é.

Mas código básico que impede dados ruins de entrar no restante do sistema vale bastante.

Também podemos verificar o tipo:

if not isinstance(data["price"], (int, float)):
raise TypeError("O preço recebido não é numérico")

if data["price"] <= 0:
raise ValueError("Preço inválido")

Agora um 200 OK com "price": null já não passa silenciosamente pelo nosso programa.

Dados em tempo real também envelhecem

Esse foi um detalhe que passei a considerar mais depois de trabalhar com dados de mercado.

Trabalho com a BYDFi, então APIs e dados financeiros aparecem com certa frequência nos meus testes e pequenos scripts. Uma coisa que fica evidente nesse tipo de sistema é que um dado pode estar perfeitamente formatado e ainda assim não ser útil.

Imagine receber:

{
"symbol": "BTCUSDT",
"price": 58432.15,
"timestamp": 1789020000
}

Nada parece errado.

Mas qual é a idade desse preço?

Podemos fazer uma verificação simples:

import time

current_time = int(time.time())
data_age = current_time - data["timestamp"]

if data_age > 30:
raise ValueError(
f"Dado muito antigo: {data_age} segundos"
)

O limite de 30 segundos é apenas um exemplo. O valor correto depende completamente da aplicação.

Para um relatório diário, alguns minutos podem não importar.

Para um sistema que monitora preços em tempo real, alguns segundos podem fazer diferença.

Esse contexto não aparece no status HTTP.

E se a estrutura da API mudar?

Esse é outro problema divertido. Divertido, claro, até acontecer em produção.

Hoje você recebe:

{
"price": 58432.15
}

Amanhã a API passa a retornar:

{
"data": {
"price": 58432.15
}
}

A API continua online.

O servidor continua respondendo 200.

Seu código, por outro lado, quebra.

Para projetos pequenos, verificações manuais podem ser suficientes. Em aplicações maiores, faz sentido usar validação de schema.

Com Pydantic, por exemplo:

from pydantic import BaseModel

class MarketData(BaseModel):
symbol: str
price: float
timestamp: int

market_data = MarketData(**data)

Agora temos uma definição explícita do formato esperado.

Isso também deixa o código mais fácil de entender meses depois, quando você já esqueceu por que aquele JSON tinha tantos campos.

Não esqueça do timeout

Outro erro comum é simplesmente fazer:

requests.get(url)

Sem timeout.

Se o servidor demorar demais para responder, sua aplicação pode ficar esperando muito mais do que deveria.

Prefiro definir isso explicitamente:

response = requests.get(
url,
timeout=10
)

Ou, dependendo do projeto:

response = requests.get(
url,
timeout=(3, 10)
)

Nesse caso, podemos separar o timeout de conexão do timeout de leitura.

É uma mudança pequena, mas torna o comportamento do programa muito mais previsível.

Retry também precisa de limite

Quando uma API falha temporariamente, tentar novamente faz sentido.

O problema começa quando fazemos retry sem pensar.

Algo assim é perigoso:

while True:
try:
response = requests.get(url)
break
except:
pass

Além de esconder erros, isso pode criar um loop infinito.

Uma abordagem melhor é limitar as tentativas:

import time
import requests

MAX_RETRIES = 3

for attempt in range(MAX_RETRIES):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
break

except requests.RequestException:
    if attempt == MAX_RETRIES - 1:
        raise

    time.sleep(2 ** attempt)

Agora temos um pequeno exponential backoff:

1ª falha -> espera 1 segundo
2ª falha -> espera 2 segundos
3ª falha -> encerra

Em sistemas maiores, claro, existem bibliotecas e estratégias muito mais completas.

Mas até essa implementação simples já é melhor do que repetir requisições indefinidamente.

Logging é mais útil do que parece

Quando estamos testando localmente, print() resolve muita coisa.

Até o dia em que alguma coisa quebra às três da manhã e ninguém sabe o que aconteceu.

Por isso comecei a considerar logs parte da validação:

import logging

logging.basicConfig(level=logging.INFO)

logging.info(
"Dados recebidos: symbol=%s price=%s timestamp=%s",
data["symbol"],
data["price"],
data["timestamp"]
)

Também vale registrar situações suspeitas:

if data_age > 10:
logging.warning(
"Dados com %s segundos de atraso",
data_age
)

O objetivo não é registrar absolutamente tudo.

É conseguir responder depois:

“O que o sistema realmente recebeu naquele momento?”

Essa pergunta aparece muito mais vezes do que eu imaginava.

Uma função um pouco mais robusta

Juntando algumas dessas ideias:

import time
import requests

def get_market_data(url):
response = requests.get(url, timeout=10)
response.raise_for_status()

try:
    data = response.json()
except ValueError:
    raise RuntimeError("Resposta JSON inválida")

required = ["symbol", "price", "timestamp"]

for field in required:
    if field not in data:
        raise ValueError(
            f"Campo obrigatório ausente: {field}"
        )

if not isinstance(data["price"], (int, float)):
    raise TypeError("Preço precisa ser numérico")

if data["price"] <= 0:
    raise ValueError("Preço inválido")

age = int(time.time()) - data["timestamp"]

if age > 30:
    raise ValueError(
        f"Dados desatualizados: {age}s"
    )

return data

Ainda não é uma solução completa para produção.

E essa é justamente a ideia.

O objetivo não é construir uma arquitetura gigantesca para uma requisição simples. É perceber que existe uma camada entre:

"A API respondeu"

e

"Posso confiar nesses dados para continuar meu processamento"

Essa camada é a validação.

O detalhe que o status code não conta

APIs tornam muito fácil conectar sistemas diferentes. Às vezes fácil até demais.

Fazemos uma requisição, recebemos 200, chamamos .json() e seguimos em frente.

Na maioria dos testes, funciona.

Até não funcionar.

Hoje tento pensar em uma resposta de API em pelo menos quatro níveis:

Transporte: a requisição HTTP funcionou?
Formato: recebi o tipo de conteúdo esperado?
Estrutura: os campos necessários estão presentes?
Semântica: os valores realmente fazem sentido para minha aplicação?

O primeiro nível é justamente onde 200 OK ajuda.

Os outros três continuam sendo responsabilidade do nosso código.

E talvez essa seja a parte mais interessante de trabalhar com APIs: receber dados é fácil. Decidir quando confiar neles é o verdadeiro trabalho.

Carregando publicação patrocinada...