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}")

Próxima Seção

9. Boas Práticas