Tratamento de erros
Todas as exceções do SDK derivam de nfeio.NfeError e podem ser importadas direto de nfeio (ou
de nfeio.errors). Nenhuma delas carrega a chave de API, em mensagem, atributo ou repr.
Hierarquia
NfeError
├── ConfigurationError chave ausente, opção inválida, TLS inseguro (também ValueError)
├── InvalidParameterError parâmetro recusado localmente, antes do HTTP (também ValueError)
├── APIError a API respondeu com status de erro
│ ├── InvalidRequestError 400, 405, 410, 415, 422 e demais 4xx
│ │ └── DuplicateExternalIdError externalId repetido na emissão
│ ├── AuthenticationError 401
│ ├── PermissionDeniedError 403 (a mensagem diz qual chave a família usa)
│ ├── NotFoundError 404
│ ├── ConflictError 409
│ ├── RateLimitError 429 (retry_after)
│ └── ServerError 408 e 5xx
├── APIConnectionError falha de rede (phase)
│ └── APITimeoutError prazo de conexão, leitura ou total esgotado
├── ResponseTooLargeError resposta acima de max_response_bytes
├── UnexpectedResponseError resposta de sucesso que o SDK não consegue interpretar
├── InvoiceProcessingError a nota terminou em IssueFailed, CancelFailed ou Error
├── PollingTimeoutError o prazo de espera acabou antes de um estado final
└── SignatureVerificationError assinatura de webhook inválida
Atributos
| Exceção | Atributos |
|---|---|
APIError e subclasses | status_code, message, error_code, request_id, trace_id, body (bytes), json_body, headers, outcome_unknown, external_id |
RateLimitError | os de APIError + retry_after (segundos, ou None) |
APIConnectionError, APITimeoutError | message, phase (FailurePhase.NOT_ESTABLISHED ou MAYBE_SENT), outcome_unknown, external_id |
InvalidParameterError | message, param (nome do parâmetro recusado) |
InvoiceProcessingError | invoice, flow_status, flow_message (a explicação da prefeitura) |
PollingTimeoutError | invoice (a última observada, ou None), flow_status, elapsed |
UnexpectedResponseError | status_code, body, headers, request_id |
request_id é o x-request-id da resposta: informe esse valor ao suporte. A mensagem já vem limpa
(sem caracteres de controle, até 1.000 caracteres), extraída dos formatos de erro que a API usa.
from decimal import Decimal
from nfeio import APIError, InvalidRequestError, NfeClient
client = NfeClient()
try:
client.service_invoices.create(
"ID_DA_EMPRESA",
{"cityServiceCode": "10677", "description": "Teste", "servicesAmount": Decimal("10.00")},
external_id="pedido-3001",
)
except InvalidRequestError as erro:
# Validação da API: corrija o payload antes de tentar de novo.
print(erro.status_code, erro.message, erro.request_id)
print(erro.json_body) # corpo decodificado, quando é JSON
except APIError as erro:
print(type(erro).__name__, erro) # status, código e request_id já no str()
A regra: nunca reenviar uma emissão às cegas
A API pode criar a nota e mesmo assim responder 500 ou 504, ou a conexão pode cair depois
que o pedido chegou. Reenviar nesse caso emite a nota duas vezes. Por isso o SDK:
- nunca repete um
POSTdepois de timeout de leitura, conexão interrompida,408,429ou5xx. Só repete quando a falha aconteceu antes de conectar, porque aí nada saiu da máquina; - marca esses erros com
outcome_unknown=Truee oexternal_idusado; - devolve
DuplicateExternalIdErrorquando a API recusa umexternalIdjá usado. A primeira tentativa existe.
outcome_unknown=True quer dizer "não sabemos se a nota foi criada". Não é falha nem sucesso: é
um estado a reconciliar. Idempotency-Key não resolve, porque a API ignora esse cabeçalho hoje.
Receita de reconciliação
Sempre envie external_id e, diante de resultado incerto, procure a nota antes de tentar de novo:
from nfeio import (
APIConnectionError,
APIError,
DuplicateExternalIdError,
NfeClient,
ServiceInvoice,
)
from nfeio.types import ServiceInvoiceCreateParams
class EmissaoNaoRegistrada(Exception):
"""A API não registrou a emissão. Tente de novo mais tarde, com o mesmo external_id."""
def emitir(
client: NfeClient,
company_id: str,
external_id: str,
params: ServiceInvoiceCreateParams,
) -> ServiceInvoice:
try:
return client.service_invoices.create(company_id, params, external_id=external_id)
except DuplicateExternalIdError:
pass # uma tentativa anterior foi registrada: busque a nota
except (APIError, APIConnectionError) as erro:
if not erro.outcome_unknown:
raise # validação, chave inválida etc.: corrija antes de tentar de novo
nota = client.service_invoices.find_by_external_id(company_id, external_id, wait=30)
if nota is None:
raise EmissaoNaoRegistrada(external_id)
return nota
Como a receita funciona:
DuplicateExternalIdErrorvem antes deAPIErrorporque é subclasse dele.- Erros com
outcome_unknown=False(validação,401,403,404) não criaram nota. Corrija a causa antes de tentar de novo. find_by_external_id(..., wait=30)repete a busca com backoff por até 30 segundos, cobrindo o atraso de indexação logo após o202. Devolve a nota ouNone. Uma resposta que não seja a lista vazia explícita da API levantaUnexpectedResponseError: o SDK prefere falhar a concluir "pode emitir de novo" por engano.- Com
None, a emissão não foi registrada. Tente de novo mais tarde com o mesmoexternal_id: se a primeira tentativa aparecer depois, a API recusa comDuplicateExternalIdErrore a receita reconcilia, sem nota duplicada.
external_id é a sua garantiaSem external_id, não há como reconciliar: a única informação sobre a nota criada é a que se
perdeu com a resposta. Use um valor estável e único por venda (o id do pedido, da fatura ou da
cobrança no seu sistema), nunca um UUID gerado a cada tentativa.
429 em emissãoRateLimitError num POST também vem com outcome_unknown=True: não há garantia de que o
limitador recusou o pedido antes de processá-lo. Reconcilie antes de reenviar.
Erros de rede
APIConnectionError.phase diz se o pedido pode ter saído:
phase | Significado | outcome_unknown num POST |
|---|---|---|
FailurePhase.NOT_ESTABLISHED | DNS, conexão recusada ou handshake TLS: nada saiu da máquina | False (o SDK já repetiu) |
FailurePhase.MAYBE_SENT | timeout de leitura, conexão interrompida: o pedido pode ter chegado | True |
Em GET, PUT e DELETE, que são seguros de repetir, o SDK tenta de novo sozinho até
max_retries antes de levantar o erro.
Erros de espera
wait, create_and_wait e cancel_and_wait têm dois erros próprios:
InvoiceProcessingError: a nota terminou emIssueFailed,CancelFailedouError. O motivo da prefeitura está emflow_message. A nota existe, com status de falha, e oexternal_idusado ficou registrado nela. Corrija a causa e emita uma nota nova, com outroexternal_id.PollingTimeoutError: o prazo acabou antes de um estado final. A nota existe e continua sendo processada. Não emita de novo: consulte depois comretrieveouwait.
Erros locais
InvalidParameterError e ConfigurationError são levantados antes de qualquer requisição, e também
são ValueError:
from nfeio import ConfigurationError, InvalidParameterError, NfeClient, Timeout
try:
Timeout(connect=10, read=60, total=0)
except ConfigurationError as erro:
print(erro) # timeout.total must be a finite number of seconds greater than zero
client = NfeClient(api_key="chave-de-exemplo", data_api_key="chave-de-dados-de-exemplo")
try:
client.lookups.cnpj("11.222.333/0001-00") # dígito verificador errado
except InvalidParameterError as erro:
print(erro.param) # cnpj
print(erro) # cnpj is not a valid CNPJ: '11.222.333/0001-00'
O SDK valida localmente ids, external_id, CNPJ (inclusive o alfanumérico), CPF, CEP, UF e datas.
As consultas podem ser cobradas, então um documento inválido nunca chega à API.
Chave errada para o host
Um 403 quase sempre é a chave da família errada (a principal numa consulta de CEP, ou a de dados
na emissão). PermissionDeniedError diz qual parâmetro a família usa:
permission denied for this API family (address); it uses data_api_key — check that the right key was configured
Veja também
- Configuração: a tabela de retry por método.
- Síncrono, assíncrono e polling: os estados da nota.
- Mensagens de erro da API de NFS-e.