C.W.K.
Stream
Lesson 04 of 05 · published

The 409 Challenge

~11 min · http-status, confirmation, honest-errors, api-design

Level 0Open Gate
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"When the app isn't sure, the honest move isn't to guess — it's to hand you the choice, clearly labeled."

A failed refresh is not an error to hide

When you ask Keep to refresh a quote and the primary provider fails, there are three tempting-but-wrong moves: silently serve a stale value, silently swap to a fallback, or blow up with a generic 500. Keep does none of them. It returns a 409 — the HTTP status for a conflict that needs resolution — carrying the candidate fallback payload. That 409 is a confirmation challenge: "the primary didn't answer; here's the fallback I could use instead; do you want it?" The refusal to proceed becomes a structured question, not a dead end.

Confirmation as a first-class response

The key design idea is treating "I need you to confirm something" as a legitimate, structured API response rather than an error or a silent default. A FallbackConfirmationRequired maps to 409 with the exact candidate attached, so the frontend can render a real choice — which provider, for which ticker, showing what value — and the human accepts or declines with full information. Compare that to the alternatives: a silent swap gives you no choice; a 500 gives you no information; a stale value gives you a lie. The 409-with-payload gives you agency.

'Needs confirmation' deserves its own status, not a disguise. When a system genuinely cannot proceed without a human decision, encoding that as a distinct, structured response — carrying everything the human needs to decide — is more honest than a silent default or a generic failure. Make the ask a first-class part of your API, not an exception you swallow.

Honest statuses all the way down

The 409 is one instance of a broader rule: Keep's domain errors carry honest HTTP statuses instead of a blanket 500.

  • FallbackConfirmationRequired409 with the candidate payload
  • A bad input value → 400
  • A missing simulation run → 404
  • A provider or runtime failure → 503

Each status tells the client something true about what went wrong and whether retrying, fixing input, or confirming is the right next move. A generic 500 for all of these would erase exactly the information the caller needs. And new provider failures raise a real RuntimeError → 503, never a sentinel value pretending to be data.

The sentinel-value trap: returning -1 or 0 or null on failure. A function that returns a fake number when the provider fails pushes a lie downstream that looks exactly like real data. Keep forbids this — a failure raises, and the raise becomes an honest status. A number you can't trust is worse than an error you can, because the error stops and the fake number spreads.

Code

Domain errors map to honest statuses (illustrative)·python
# Each domain error carries a status that tells the truth about it.
ERROR_STATUS = {
    FallbackConfirmationRequired: 409,   # here's the candidate — confirm?
    ValueError:                   400,   # your input was bad
    MissingRun:                   404,   # that simulation doesn't exist
    RuntimeError:                 503,   # provider/runtime failed, try later
}

@app.exception_handler(FallbackConfirmationRequired)
def needs_confirmation(request, exc):
    # Not a 500. A structured 409 carrying the exact choice to make.
    return JSONResponse(status_code=409, content={
        "needs_confirmation": True,
        "candidate_source": exc.candidate.source,
        "candidate_value":  exc.candidate.value,
        "ticker":           exc.candidate.ticker,
    })

External links

Exercise

Find an endpoint you've written that returns 500 (or a sentinel like -1/null) for several different failure reasons. Split those reasons into honest statuses: which are 400 (bad input), 404 (not found), 409 (needs a decision), 503 (try again later)? For the 409 case, describe the payload you'd attach so the client can actually make the decision rather than just seeing 'conflict.'
Hint
A 409 with no payload is only marginally better than a 500 — the client knows something needs resolving but not what. The power is in the attached candidate: show exactly what the substitute would be, so 'confirm?' is a real, informed choice rather than a blind yes/no.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.