8. Fluxo de Reenvio em Caso de Erro
Este capítulo detalha como identificar, diagnosticar e corrigir erros, permitindo reenvios bem-sucedidos.
8.1 Árvore de Decisão — Quando Reenviar
┌─────────────────────┐
│ Recebeu Resposta? │
└──────────┬──────────┘
│
Sim │ Não
─┼─
│
┌──────▼──────┐
│ HTTP Status │
└──────┬──────┘
│
┌────┼──────────────────────────────┐
│ │
┌───▼────┐ ┌─────────┐ ┌────┬────┐
│ 2xx │ │ 4xx │ │5xx │429 │
│Sucesso │ │ Erro │ │Err │Rate│
└────┬───┘ │ Dado │ │Srv │Lim│
│ └─────┬───┘ └┬──┬┘
Concluído │ │ │
Pode parar ERRO! Erro do servidor
│ Aguarde e reenvie
▼
┌──────────────────┐
│ Qual erro (4xx)? │
└────────┬─────────┘
│
┌────────┼────────┐
│ │ │
┌────▼──┐ ┌──▼──┐ ┌──▼──┐
│ 400 │ │404 │ │409 │
│Valid. │ │Não │ │Dupl.│
└────┬──┘ │Enc. │ └──┬──┘
│ └─────┘ │
Corrigir Ajustar
dados dados
│ │
└──────┬───────┘
│
Reenviar dados
corrigidos
8.2 Identificar Tipo de Erro
Erros de Validação (HTTP 400)
Assinatura:
{
"success": false,
"message": "Validation failed",
"errors": [
"Field 'ncmCode' is required",
"Field 'description' must not exceed 500 characters"
]
}
Causa: Dados inválidos, formato incorreto, ou campos obrigatórios ausentes.
Ação: CORRIJA OS DADOS
NÃO reenvie: A mesma requisição receberá o mesmo erro.
Erros de Autenticação (HTTP 401)
Assinatura:
{
"message": "Unauthorized: Invalid API Key"
}
Causa: API Key ausente, inválida, ou expirada.
Ação: VERIFIQUE A AUTENTICAÇÃO
NÃO reenvie: Até corrigir a chave.
Erros de Recurso Não Encontrado (HTTP 404)
Assinatura:
{
"success": false,
"message": "Product with ErpId 99999999 not found"
}
Causa: Recurso não existe (tentativa de update de produto inexistente).
Ação: - Para atualizações: Crie o recurso primeiro (POST), depois atualize (PUT) - Para consultas: Verifique se o ID está correto
Erros de Duplicidade (HTTP 409)
Assinatura:
{
"success": false,
"message": "A product with ErpId 12345 already exists"
}
Causa: Tentativa de criar com ID que já existe.
Ação: - Se é criação: Use um ID diferente - Se é atualização: Use PUT em vez de POST - Verifique se não há duplicatas no seu ERP
Erros do Servidor (HTTP 500)
Assinatura:
{
"success": false,
"message": "An unexpected error occurred",
"errors": ["Database connection failed"]
}
Causa: Problema técnico do servidor Portal Retail.
Ação: AGUARDE E REENVIE
Reenvie após: 5-10 minutos (não é erro de dados)
Rate Limiting (HTTP 429)
Assinatura:
{
"message": "Too many authentication attempts. Please try again later",
"retryAfter": 45
}
Causa: Muitas tentativas falhadas com autenticação.
Ação: AGUARDE SEGUNDOS INDICADOS
Reenvie após: Valor de retryAfter (45 segundos, por exemplo)
8.3 Erros em Operações de Batch
Erro: Validação Pré-processamento (Não Enfileirado)
Resposta (400 Bad Request):
{
"message": "Validation failed at item 5",
"totalItems": 100,
"invalidItems": 1,
"itemErrors": [
{
"index": 5,
"errors": [
"Field 'ncmCode' is required"
]
}
]
}
Interpretação: - Item no índice 5 (sexto item) tem erro - Campo 'ncmCode' é obrigatório
Como resolver: 1. Localize o item 5 na sua lista 2. Adicione o campo 'ncmCode' 3. Reenvie o batch completo
Código Python:
# Identificar item com erro
index = 5
items = dados['produtos']
item_com_erro = items[index] # Sexto item (0-indexed)
print(f"Item com erro: {item_com_erro}")
# Corrigir
item_com_erro['ncmCode'] = '84733090'
# Reenviar
enviar_batch(dados)
Erro: Batch Muito Grande (Oversized)
Resposta (400 Bad Request):
{
"message": "Batch size exceeds maximum allowed (1000 items). Received 1500 items."
}
Como resolver: 1. Divida o batch em múltiplas requisições 2. Máximo 1.000 itens por requisição 3. Reenvie em chunks
Código Python:
from math import ceil
def enviar_em_chunks(produtos, tamanho_max=1000):
"""
Divide produtos em chunks e envia cada um
"""
num_chunks = ceil(len(produtos) / tamanho_max)
for i in range(num_chunks):
inicio = i * tamanho_max
fim = min((i + 1) * tamanho_max, len(produtos))
chunk = produtos[inicio:fim]
print(f"Enviando chunk {i+1}/{num_chunks} ({len(chunk)} itens)...")
resposta = enviar_batch({'products': chunk})
if resposta.status_code == 202:
job_id = resposta.json()['jobId']
aguardar_batch(job_id)
# Usar
produtos = [...] # 5.000 produtos
enviar_em_chunks(produtos) # Será dividido em 5 requisições
Erro: Batch Processado com Falhas Parciais
Resposta do relatório (200 OK):
{
"state": "Finished",
"successCount": 95,
"failureCount": 5,
"hasPartialData": true,
"errorMessage": "5 items failed validation"
}
Como resolver: 1. Consulte o histórico de integração para identificar quais dos 5 itens falharam 2. Corrija apenas esses 5 itens 3. Reenvie um novo batch com os 5 itens corrigidos
Importante sobre Histórico de Integração:
⚠️ MUDANÇA NO PADRÃO DE LOGGING:
- A API implementa logging condicional para reduzir ruído
- No histórico de integração (_Erp_IntegrationHistory), você verá:
- ✅ UM log agregado por batch com resumo (total, sucessos, falhas)
- ❌ NENHUM log de itens individuais do batch (mesmo os que falharam)
- ✅ Logs de erros individuais (para operações POST/PUT não-batch)
- ❌ Nenhum log de sucessos individuais (para operações POST/PUT não-batch)
Código Python:
def reprocessar_com_falha(job_id, dados_originais):
"""
Reprocessa apenas items que falharam
"""
# Obter status
status = consultar_batch_status(job_id)
# Se houve falhas
if status['failureCount'] > 0:
# O histórico contém o log agregado do batch
# mas não itens individuais (por design para reduzir ruído)
# Estratégia 1: Reprocessar tudo (simples)
print(f"Reenviando {len(dados_originais['products'])} items...")
enviar_batch(dados_originais)
# Estratégia 2: Rastrear no lado cliente (recomendado)
# Mantenha seus próprios logs de qual item enviou
ids_que_falharam = obter_ids_que_falharam_localmente(job_id)
items_com_erro = [
item for item in dados_originais['products']
if item['erpId'] in ids_que_falharam
]
# Corrigir items
for item in items_com_erro:
# Corrigir dados específicos
item['status'] = 'Active' # Exemplo
# Reenviar apenas os com erro
print(f"Reenviando {len(items_com_erro)} items corrigidos...")
enviar_batch({'products': items_com_erro})
Recomendação: Se você precisa rastrear quais itens de um batch falharam, implemente logging no lado cliente que correlacione jobId com items originais. A API não retorna detalhes de itens individuais no histórico (apenas resumo agregado).
8.4 Erros do Servidor (HTTP 500) — Reenvio Automático
Se o problema é do servidor (não dos dados), você pode reenviar:
Estratégia 1: Retry Simples
import requests
import time
def reenviar_com_retry(dados, max_tentativas=3):
"""
Reenvie com retry automático
"""
for tentativa in range(1, max_tentativas + 1):
try:
response = requests.post(
'https://api.retailportal.com/erp-data/product/batch',
json=dados,
headers={'X-API-Key': api_key},
timeout=30
)
if response.status_code in [201, 202]:
return response # Sucesso
elif response.status_code == 500:
print(f"Erro 500. Tentativa {tentativa}/{max_tentativas}")
if tentativa < max_tentativas:
espera = 5 * tentativa # Progressivo: 5, 10, 15
print(f"Aguardando {espera} segundos...")
time.sleep(espera)
else:
return response # Outro erro
except requests.exceptions.RequestException as e:
print(f"Erro de conexão: {e}")
if tentativa < max_tentativas:
time.sleep(5)
print("Falha após todas as tentativas")
return None
Estratégia 2: Retry Exponencial
def reenviar_com_backoff_exponencial(dados, max_tentativas=5, base_espera=2):
"""
Reenvie com backoff exponencial (2, 4, 8, 16 segundos)
"""
for tentativa in range(1, max_tentativas + 1):
try:
response = requests.post(...)
if response.status_code in [201, 202, 200]:
return response
if response.status_code == 500:
if tentativa < max_tentativas:
espera = base_espera ** tentativa
print(f"Tentativa {tentativa}: Aguardando {espera}s")
time.sleep(espera)
except Exception as e:
if tentativa < max_tentativas:
espera = base_espera ** tentativa
time.sleep(espera)
return None
8.5 Checklist de Reenvio
Antes de reenviar, valide:
Se HTTP 400 (Erro de Validação)
- [ ] Leia a mensagem de erro
- [ ] Identifique o campo problemático
- [ ] Verifique documentação de tipos de dados
- [ ] Valide localmente (antes de reenviar)
- [ ] Reenvie dados corrigidos
Se HTTP 401 (Autenticação)
- [ ] Verifique se X-API-Key está no cabeçalho
- [ ] Verifique se a chave está correta
- [ ] Verifique se não tem espaços extras
- [ ] Contacte suporte se a chave está expirada
- [ ] Reenvie com chave válida
Se HTTP 404 (Não Encontrado)
- [ ] Para criação: Verifique se está usando POST (não PUT)
- [ ] Para atualização: Verifique se o produto existe
- [ ] Crie o recurso primeiro, depois atualize
- [ ] Verifique IDs no seu ERP
Se HTTP 409 (Duplicado)
- [ ] Para criação: Use um ID único
- [ ] Para atualização: Use PUT, não POST
- [ ] Verifique duplicatas no seu ERP
- [ ] Limpe dados duplicados
Se HTTP 500 (Erro Servidor)
- [ ] Não é erro de dados
- [ ] Aguarde 5 minutos
- [ ] Reenvie a mesma requisição
- [ ] Se persistir: Contacte suporte
Se HTTP 429 (Rate Limit)
- [ ] Leia valor de
retryAfter - [ ] Aguarde exatamente esse tempo
- [ ] Evite múltiplas tentativas simultâneas
- [ ] Reenvie após aguardar
8.6 Implementação Completa de Reenvio
import requests
import time
from enum import Enum
from dataclasses import dataclass
class TipoErro(Enum):
VALIDACAO = 400
AUTENTICACAO = 401
NAO_ENCONTRADO = 404
CONFLITO = 409
RATE_LIMIT = 429
ERRO_SERVIDOR = 500
DESCONHECIDO = None
@dataclass
class ResultadoReenvio:
sucesso: bool
tipo_erro: TipoErro
mensagem: str
data_proxima_tentativa: float = None
def diagnosticar_erro(response):
"""
Identifica tipo de erro da resposta HTTP
"""
status = response.status_code
if status in [200, 201, 202]:
return TipoErro.VALIDACAO # Falso positivo, mas OK
tipo_mapa = {
400: TipoErro.VALIDACAO,
401: TipoErro.AUTENTICACAO,
404: TipoErro.NAO_ENCONTRADO,
409: TipoErro.CONFLITO,
429: TipoErro.RATE_LIMIT,
500: TipoErro.ERRO_SERVIDOR,
}
return tipo_mapa.get(status, TipoErro.DESCONHECIDO)
def pode_reenviar(tipo_erro):
"""
Determina se pode reenviar automaticamente
"""
pode = {
TipoErro.VALIDACAO: False, # Corrija dados
TipoErro.AUTENTICACAO: False, # Corrija chave
TipoErro.NAO_ENCONTRADO: False, # Crie recurso
TipoErro.CONFLITO: False, # Ajuste dados
TipoErro.RATE_LIMIT: True, # Aguarde tempo
TipoErro.ERRO_SERVIDOR: True, # Reenvie
}
return pode.get(tipo_erro, False)
def reenviar_inteligente(dados, tentativa=1, max_tentativas=5):
"""
Reenvio inteligente com diagnóstico automático
"""
api_key = "SUA-CHAVE"
url = "https://api.retailportal.com/erp-data/product/batch"
headers = {
'X-API-Key': api_key,
'Content-Type': 'application/json'
}
try:
response = requests.post(url, json=dados, headers=headers)
# Sucesso?
if response.status_code in [200, 201, 202]:
print("✓ Sucesso na primeira tentativa")
return ResultadoReenvio(sucesso=True, tipo_erro=None, mensagem="OK")
# Erro - diagnosticar
tipo_erro = diagnosticar_erro(response)
mensagem = response.json().get('message', 'Erro desconhecido')
print(f"✗ Erro {response.status_code}: {tipo_erro.name}")
print(f" Mensagem: {mensagem}")
# Pode reenviar?
if not pode_reenviar(tipo_erro):
print(f" → Corrija os dados e reenvie manualmente")
return ResultadoReenvio(
sucesso=False,
tipo_erro=tipo_erro,
mensagem=mensagem
)
# Pode reenviar - calcular tempo de espera
if tipo_erro == TipoErro.RATE_LIMIT:
retry_after = int(response.headers.get('Retry-After', 60))
else:
retry_after = 5 * tentativa # Backoff progressivo
print(f" → Será retentado em {retry_after} segundos")
if tentativa >= max_tentativas:
print(f" → Limite de tentativas atingido")
return ResultadoReenvio(
sucesso=False,
tipo_erro=tipo_erro,
mensagem="Limite de tentativas atingido"
)
# Aguardar e reenviar
time.sleep(retry_after)
print(f" → Reenviando (tentativa {tentativa + 1}/{max_tentativas})...")
return reenviar_inteligente(dados, tentativa + 1, max_tentativas)
except requests.exceptions.RequestException as e:
print(f"✗ Erro de conexão: {e}")
if tentativa < max_tentativas:
espera = 5 * tentativa
print(f" → Retentando em {espera} segundos...")
time.sleep(espera)
return reenviar_inteligente(dados, tentativa + 1, max_tentativas)
return ResultadoReenvio(
sucesso=False,
tipo_erro=TipoErro.DESCONHECIDO,
mensagem=str(e)
)
# Usar
dados = {'products': [...]}
resultado = reenviar_inteligente(dados)
if resultado.sucesso:
print("✓ Integração bem-sucedida")
else:
print(f"✗ Falha: {resultado.tipo_erro.name}")
print(f" Mensagem: {resultado.mensagem}")