feat(http): retry seguro de POST + idempotência por externalId (release v3.2.0)#23
Merged
Conversation
…FS-e Torna o retry seguro por default e habilita emissão idempotente de NFS-e, eliminando o risco de nota duplicada e o contorno de dois clients. - Retry ciente de método (Nfe\Http\RetryingTransport): POST não retenta mais em 5xx nem em falha de rede ambígua (evita reemitir NFS-e); GET/PUT/DELETE/ HEAD/OPTIONS inalterados; 429 e falha de conexão comprovada ainda retentam; um header Idempotency-Key restaura o retry pleno (a API ainda não honra). - Nfe\Http\FailurePhase (ConnectionNotEstablished/RequestMaybeSent) + ApiConnectionException->failurePhase/curlErrno; CurlTransport classifica por errno (allowlist 5/6/7/35), Psr18Transport por NetworkExceptionInterface. - RequestOptions->retry: override de retry por chamada (liga/desliga em uma única chamada); o decorator de retry passa a envolver sempre o transporte. - ServiceInvoicesResource::findByExternalId() (rota dedicada /external, contrato de coleção do fio) + isDuplicateExternalId() estático; campo externalId no DTO ServiceInvoice (anexado ao fim do construtor — aditivo). Mudança de comportamento (safety fix, minor): POST deixa de retentar em 5xx. Rota de escape: enviar externalId e reconciliar via findByExternalId após falha ambígua; ou RequestOptions(retry: ...) por chamada. Validado ao vivo (2026-07-06): create() pode retornar HTTP 500 e ainda assim criar a nota — o 5xx ambíguo que esta mudança impede de duplicar. Ref: OpenSpec change add-safe-retry-idempotency.
O arquivo VERSION ficou preso em 3.0.0-dev desde o rewrite v3 — permaneceu 3.0.0-dev inclusive nas tags v3.0.0 e v3.1.0. É um órfão legado da era v2.x: nada no pipeline o lê (a fonte canônica de versão é src/Version.php::CURRENT, que release.sh e .github/workflows/release.yml bumpam e validam). Corrige o valor para a linha em desenvolvimento nesta branch. Follow-up sugerido: remover o arquivo (fonte única = src/Version.php) OU ensinar o release.sh a bumpá-lo, para não voltar a driftar.
Segue o padrão do release v3.1.0 (be00f2d): o corte é feito no próprio PR. - src/Version.php::CURRENT 3.1.0 → 3.2.0 (fonte canônica; o CI valida tag↔Version.php). - CHANGELOG: promove [Unreleased] → [3.2.0] — 2026-07-06, mantendo [Unreleased] vazia no topo. - VERSION 3.2.0-dev → 3.2.0 (alinha o arquivo órfão à versão cortada; ver commit anterior). Tag v3.2.0 fica para APÓS o merge (master protegida). Não rodar a promoção do release.sh de novo pós-merge — o CHANGELOG já está cortado; apenas taggear.
andrenfe
added a commit
that referenced
this pull request
Jul 7, 2026
Desde 45aa6e8 ("master passa a ser a linha canônica v3+"), o ci.yml continuou disparando apenas em push/PR da branch `v3`. Efeito: nenhum PR contra master rodou o CI funcional (matriz PHP 8.2/8.3/8.4 + PHPStan + CS + generate:check) — só o CodeQL. Confirmado nos PRs #21 (release v3.1.0) e #23. Adiciona `master` aos gatilhos push e pull_request (mantém `v3` por segurança; a branch ainda existe, congelada em v3.0.0). Mudança não-destrutiva, só de gatilho — sem uso de input não confiável.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Objetivo
Tornar o retry seguro por default e habilitar emissão idempotente de NFS-e, eliminando o risco de nota fiscal duplicada e o contorno de manter dois
Nfe\Client(um sem retry para escrita, outro com retry para leitura).Origem: proposta da integração WooCommerce (
woo-nfe). OpenSpec change:add-safe-retry-idempotency.O que muda
Retry ciente de método (
Nfe\Http\RetryingTransport):Idempotency-KeyFase da falha estruturada —
Nfe\Http\FailurePhase(ConnectionNotEstablished/RequestMaybeSent) +ApiConnectionException->failurePhase/->curlErrno.CurlTransportclassifica por errno (allowlist5/6/7/35);Psr18TransportporNetworkExceptionInterface.Override de retry por chamada —
RequestOptions->retry(?RetryPolicy, 4º parâmetro). Liga/desliga o retry de uma única chamada nos dois sentidos; o decorator de retry passa a envolver sempre o transporte base.Reconciliação por
externalId(NFS-e) —ServiceInvoicesResource::findByExternalId()(rota dedicadaGET /v1/companies/{id}/serviceinvoices/external/{externalId}, contrato de coleção do fio) +isDuplicateExternalId()estático; campoexternalIdno DTOServiceInvoice(anexado ao fim do construtor — puramente aditivo).POSTdeixa de retentar em5xx(e em timeout ambíguo). Reexecutar um POST pode duplicar uma NFS-e. Rota de escape: enviarexternalIde reconciliar comfindByExternalId()após falha ambígua; ouRequestOptions(retry: ...)por chamada.Ciclo de emissão idempotente
Validação ao vivo (2026-07-06)
Sonda contra a API real reproduziu o cenário-alvo: um
create()retornou HTTP 500 e mesmo assim criou a nota (Issued) — exatamente o 5xx ambíguo que, com o retry antigo, teria duplicado a NFS-e. Também confirmados:Idempotency-Keyignorado pela API;externalIdcomo chave única (400 "already exists"); a rota/external(contrato de coleção, miss =200lista vazia).Qualidade
docs/configuration.md, cookbook NFS-e) eCHANGELOG.md([Unreleased]).Release v3.2.0 (corte no PR)
Seguindo o padrão do v3.1.0 (
be00f2d), o corte é feito neste PR:src/Version.php::CURRENT→3.2.0(fonte canônica; o CI valida tag↔Version.php).CHANGELOG.md:[Unreleased]promovida para[3.2.0] — 2026-07-06.VERSION(arquivo órfão legado): corrigido3.0.0-dev→3.2.0. Nada no pipeline o lê(a versão canônica é
src/Version.php); estava defasado desde o rewrite v3. Follow-up:remover o arquivo ou ensinar o
release.sha bumpá-lo, para não voltar a driftar.🏷️ Tag⚠️ Como o CHANGELOG já foi
v3.2.0somente após o merge (master protegida).promovido aqui, não rodar a promoção do
release.shde novo pós-merge (duplicaria a seção);apenas criar/pushar a tag.
SemVer
Minor (v3.2.0). Tudo aditivo; a mudança de default do POST é defendida como correção de
segurança.
externalIdfoi anexado ao fim do construtor do DTO para ser aditivo mesmo soba política estrita de "construtor é contrato".
Fora deste PR (rastreio)
Idempotency-Key;503→404para company inexistente; error code estruturado na duplicata; alinhar o YAML de/externalao fio (envelope de coleção).ServiceInvoicecobre poucos campos do fio e o codegen ignora specs com schemas inline.🏷️ Tag
v3.2.0somente após o merge (política de master protegida).