feat(fetcher): auto-mint Idempotency-Key on send endpoints#23
feat(fetcher): auto-mint Idempotency-Key on send endpoints#23sidharth0612 wants to merge 1 commit into
Conversation
|
Rebased onto the regenerated main (0.5.14). The regen's native idempotency support composes cleanly with this patch: the generated clients merge |
4fdef07 to
2be469a
Compare
When the caller does not supply requestOptions.idempotencyKey, the SDK now defaults it to a fresh UUID4 (once per logical call) for exactly the five send endpoints: inboxes.messages.send / .reply / .replyAll / .forward and inboxes.drafts.send. A caller-supplied idempotencyKey (or a raw Idempotency-Key entry in requestOptions.headers) always wins, and no other method is affected. The default is applied at the wrapper method level, before the generated client merges the key into the request headers and hands off to core.fetcher — retries happen inside the fetcher reusing those headers, so every internal retry of one call carries the identical key while separate calls mint fresh keys. Implemented entirely in src/wrapper (MessagesClient / DraftsClient / InboxesClient subclasses plus an inboxes getter override on AgentMailClient), the sanctioned custom-code area: Fern's generator only emits typed pass-through for idempotency headers, and value generation is client behavior. No generated files are modified or pinned in .fernignore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2be469a to
674807d
Compare
|
@duharry0915 reworked per the (inferred) closure concern: the fetcher-layer patch is gone — |
Makes the SDK's send calls idempotent-by-default. The AgentMail API dedupes sends via an
Idempotency-Keyheader (agentmail-api#522). The SDK auto-retries 408/429/5xx, and without a stable key each retry would be an un-deduped send — this defaults a key per logical call so retries can never duplicate an email.Reworked: wrapper approach — no generated files pinned
This PR was previously a core-fetcher patch that required freezing
Fetcher.tsin.fernignore(the likely reason it was closed — fair). It has been reworked from scratch on top of 0.5.14:.fernignoreuntouched.src/core/untouched. The entire change lives insrc/wrapper/+tests/unit/wrapper/— the human-owned custom-code area this repo already uses for the customWebsocketsClient. Fern regeneration flows freely through everything it owns.MessagesClient(send/reply/replyAll/forward) andDraftsClient(send) default the option 0.5.14 itself introduced:requestOptions.idempotencyKey ?? uuidv4(). A caller-supplied key always wins; a rawrequestOptions.headers['Idempotency-Key']also wins (it merges last in the generated client).inboxesgetter override insrc/wrapper/Client.ts, mirroring the existingwebsocketsoverride pattern exactly.idempotencyKeyinto the headers once, beforecore.fetcher's retry loop — so every internal retry of one call reuses the same key. Verified by a 503-then-200 test asserting identical keys on both attempts.Why Fern can't generate this part
Fern's
idempotency-headersfeature (enabled in agentmail-docs#180, live in 0.5.14) generates the typed pass-through —requestOptions.idempotencyKey→ header. There is no generator feature for value generation ("mint a UUID when the caller omits one"): that's client behavior, not request shape, so it belongs in the wrapper. Long-term, auto-generation would be a reasonable upstream feature request to Fern; until then this is the sanctioned place for it.Without a default, the SDK's own auto-retries are idempotent only for callers who explicitly pass
idempotencyKey— everyone else keeps the double-send-on-retry hole. (Stripe's SDKs auto-generate idempotency keys on retryable requests for the same reason.)Known non-covered path
client.fetch(...)(the raw passthrough escape hatch) bypasses the wrapper by design — it's documented raw access..withRawResponse()is covered (it flows through the same public methods).Tests / build
tests/unit/wrapper/idempotency.test.ts— 11 tests through the real publicAgentMailClientwith injected fetch: mint-when-absent (valid UUID4), calleridempotencyKeywins, raw header wins, identical key across a 503→200 retry, distinct keys per call, all five endpoints mint, non-send methods untouched. Full unit suite: 491/491.tsccjs+esm clean. Biome clean on the new files (the 2 lazy-getter warnings match the identical pre-existing pattern on thewebsocketsgetter). No version bump.🤖 Generated with Claude Code