Skip to content

fix: retry after a post-persist booking failure returns 409 without the deal id #88

Description

@numarasSigmaSoftware

Symptom

In POST /api/v1/deals, deal_service.book_deal persists the deal and flips the quote to booked before returning, and the router writes the idempotency record only after the service returns. Any failure in that window leaves the store in the booked state with no idempotency record. Depending on where it fails the client sees a 500, or a 200 whose idempotency record was silently dropped; either way its retry with the same key finds the quote already booked and gets a 409, and if the first response was lost the deal_id that was minted is never returned to it.

Any post-persist failure triggers this (a storage error on the idempotency write, for example). #85 adds one more failure point inside the window: deal.created is audit-class, so if the bus fails and the audit fallback write also fails, the error propagates after the deal is persisted. That PR documents the behavior and points here.

Reproduction

Book an available quote with an authenticated buyer, but make the storage set for the idempotency: record fail on the first call; then retry with the same key and body:

call 1: POST /api/v1/deals {"idempotency_key": "probe-book-1", "quote_id": ...}  -> 200, deal DEMO-A5A09D6FE8A7 persisted, quote flipped to booked
call 2: same body                                                                -> 409 {"error": "quote_already_booked", "message": "Quote status is 'booked', expected 'available'."}

Two things in that trace. First, call 1 returned 200 even though the idempotency write failed: the router wraps that write in except Exception: pass (routers/deals.py, after the deal_service.book_deal call), so the failure is invisible and no record exists. Second, the retry cannot recover: the 409 carries no deal_id, and the store holds exactly one deal the client never learned about.

The same end state arises when the failure is loud instead of swallowed. With the audit-class event from #85, a bus failure plus a fallback-write failure raises after the persist, so call 1 is a 500 and call 2 is the same 409 without a deal_id.

Environment

seller-agent main at 8dfd5e0 (after #78), installed with uv sync --locked: Python 3.12.12, fastapi 0.139.0, iab-agentic-primitives==0.5.1. macOS 26.6. Reproduced in-process against the FastAPI app through httpx.ASGITransport, storage replaced by an in-memory double, so the numbers above are what the routes return, not what an ad server does.

Expected

Either write the idempotency record inside the service in the same step as the persist, so a retry replays the deal, or make the "already booked" 409 carry the deal_id so the client can recover. The first is the durable fix.

Pre-existing on main at 8dfd5e0; the surrounding idempotency design is from #50.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions