Skip to content

fix: make ShiprocketQuickProvider fail soft on every error, not just 422 - #84

Merged
lordlabakdas merged 1 commit into
masterfrom
fix/shiprocket-provider-fail-soft
Oct 12, 2026
Merged

lordlabakdas merged 1 commit into
masterfrom
fix/shiprocket-provider-fail-soft

Conversation

@lordlabakdas

Copy link
Copy Markdown
Owner

Summary

Production bug report: clicking "Location set — update" at checkout threw "Failed to fetch". Reproduced it live against groceror.store (Claude in Chrome, logged-in session) — the browser's Network tab showed POST /order/delivery-quote coming back as a raw 503 from Fly's edge, while every other endpoint (GET /inventory/browse, etc.) responded normally in the same session. A plain curl to the same path got a clean 401 (unauthenticated), confirming the app itself was up — only this specific authenticated call was crashing.

Root cause: ShiprocketQuickProvider (engine/delivery/shiprocket_quick.py) is explicitly flagged in its own docstring as an unverified vendor integration — "a best-effort guess at a typical REST courier API," pending real Shiprocket Quick credentials per SPEC_DELIVERY_DISPATCH.md. It only converted a 422 response into DeliveryUnavailableError. Every other failure mode — connection error, timeout, an unexpected HTTP status via raise_for_status(), a malformed/short JSON body — was a raw, uncaught requests exception that crashed the request handler, which Fly's edge then reports as a 503 with no CORS headers, which the browser then surfaces to JS as a generic "Failed to fetch".

DeliveryService.get_quote()/request_delivery() and their callers already only catch DeliveryUnavailableError and fail soft correctly — the gap was entirely inside the provider not honoring its own DeliveryProvider interface contract.

Fix

Routed every vendor call (get_quote, create_delivery, get_status, cancel) through a shared _request()/_json_body() pair that converts requests.exceptions.RequestException, an unexpected HTTP status, a malformed JSON body, or a missing expected field into DeliveryUnavailableError. A checkout shopper now sees a clean 400 ("Delivery unavailable for this address") instead of a crash, and request_delivery() falls back to a failed Delivery row as already designed — this doesn't fix the underlying unverified-vendor problem (still blocked on real credentials), it just stops it from taking the whole request down.

Test plan

  • New tests/unit/test_shiprocket_quick_provider.py — 14 tests covering connection errors, timeouts, 422/404/409/5xx statuses, malformed/short response bodies, and the success paths for all four provider methods
  • pytest tests/unit/test_shiprocket_quick_provider.py tests/unit/test_delivery_provider.py tests/unit/test_delivery_service.py tests/integration/test_delivery_dispatch.py -v — 42/42 pass
  • pytest tests/ -v — 624/624 pass

🤖 Generated with Claude Code

https://claude.ai/code/session_014t1eBFvEm5tiPyqQiQ3DNM

Production incident: clicking "Location set — update" at checkout threw
"Failed to fetch". Traced it to POST /order/delivery-quote returning a
raw 503 from Fly's edge (confirmed via the browser's network tab),
while every other endpoint responded normally in the same session.

Root cause: ShiprocketQuickProvider (its own docstring: "UNVERIFIED
PROVIDER... a best-effort guess", pending real vendor credentials per
SPEC_DELIVERY_DISPATCH.md) only converted a 422 response into
DeliveryUnavailableError. Every other failure mode — connection error,
timeout, an unexpected HTTP status via raise_for_status(), a malformed
or short response body — was a raw, uncaught exception that crashed
the request handler. DeliveryService.get_quote()/request_delivery()
and their callers already only catch DeliveryUnavailableError and fail
soft correctly (per existing design); the gap was entirely inside this
provider not honoring its own interface contract.

Routed every vendor call through a shared _request()/_json_body()
pair that converts requests.exceptions.RequestException, an
unexpected HTTP status, a malformed JSON body, or a missing expected
field into DeliveryUnavailableError — so a checkout shopper now sees
"Delivery unavailable for this address" (400) instead of a crash, and
request_delivery() falls back to a `failed` Delivery row as already
designed, until Shiprocket Quick is wired up for real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014t1eBFvEm5tiPyqQiQ3DNM
@lordlabakdas
lordlabakdas merged commit c3918c9 into master Oct 12, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant