Error handling¶
All errors come back as RFC 7807 ProblemDetails. One pattern for logging, monitoring, and troubleshooting — no per-provider special-casing.
Error response format¶
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"detail": "Application APP-12345 not found.",
"instance": "/allianz/applications/APP-12345",
"correlationId": "9f3a2b1c-4d5e-6789-abcd-ef0123456789"
}
| Field | What it is |
|---|---|
type |
URI for the error category |
title |
Short description |
status |
HTTP status |
detail |
Detailed message (no sensitive data) |
instance |
Request path |
correlationId |
Trace ID — send this to support |
Status codes¶
4xx — fix the request¶
| Status | Meaning | Common causes |
|---|---|---|
400 Bad Request |
Couldn't parse | Invalid JSON, wrong types |
401 Unauthorized |
Auth failed | Missing/expired/invalid token |
403 Forbidden |
Authenticated, no permission | Missing role/scope |
404 Not Found |
No route/resource | Wrong URL or unknown ID |
409 Conflict |
State mismatch | Duplicate submission |
422 Unprocessable Entity |
Business validation failed | Provider rules |
429 Too Many Requests |
Rate limit | See Rate limits |
5xx — retry with care¶
| Status | Meaning | What to do |
|---|---|---|
500 Internal Server Error |
Unexpected exception | Retry with backoff |
502 Bad Gateway |
Upstream provider error | Usually worth a retry |
503 Service Unavailable |
Service down or circuit breaker | Exponential backoff |
504 Gateway Timeout |
Upstream timeout | Retry, escalate if persistent |
Provider business errors¶
Requests that are technically valid but rejected by the upstream insurer for business
reasons → 422 with extra context:
{
"type": "https://docs.insurancegateway.gr/errors/provider-business",
"title": "Provider rejected the request",
"status": 422,
"detail": "Allianz declined: vehicle has active policy.",
"providerErrorCode": "E_DUPLICATE_POLICY",
"providerErrorMessage": "Vehicle ABC-1234 already has an active policy.",
"correlationId": "9f3a2b1c-..."
}
What the consumer sees vs what stays hidden¶
Not every upstream error is surfaced the same way. The API draws a clear line between business errors (safe to pass through verbatim) and transport / infrastructure errors (which may leak internal details — credentials, internal URLs, stack traces).
422 — business: full upstream message¶
providerErrorMessage and detail contain the verbatim upstream message. It's
actionable end-user feedback (e.g. "invalid tax ID", "vehicle already insured").
{
"status": 422,
"title": "Provider business error",
"detail": "Interamerican declined: Tax ID 123456789 is not valid.",
"provider": "Interamerican",
"providerErrorMessage": "Tax ID 123456789 is not valid.",
"category": "business",
"correlationId": "9f3a2b1c-..."
}
502 — transport: summary only, no raw body¶
The upstream body is not exposed. Only provider and providerStatusCode come
back as a summary. The raw payload (which may contain expired API keys, internal
endpoints, SOAP envelopes with credentials) is sent only to server-side logs
with redaction.
{
"status": 502,
"title": "Upstream provider error",
"detail": "Upstream provider returned an error.",
"provider": "Allianz",
"providerStatusCode": 401,
"category": "transport",
"correlationId": "9f3a2b1c-..."
}
Example: the upstream provider returned 401 Unauthorized with body
{"error":"Invalid API Key 'sk_live_abc123...'"}. The consumer sees only
providerStatusCode: 401 — never the API key, never the raw error message.
504 — timeout: generic message¶
Generic "timed out" — no hints about what the upstream was doing or internal retry mechanics / topology.
How to present this to end-users
- 422: surface
providerErrorMessageverbatim — it's actionable ("Fix the tax ID", "Vehicle already has a policy"). - 502 / 504: show a generic message ("Service temporarily unavailable —
please try again") plus the
correlationId/traceIdfor support escalation. Don't try to interpretproviderStatusCodefor the end-user.
correlationId — why it matters¶
Every request has a correlationId. We log it across our API and upstream calls —
fastest way for both sides to talk about the same incident.
Also returned in the X-Correlation-ID response header.
Got your own request tracing? Send X-Correlation-ID and we'll propagate it:
curl -H "X-Correlation-ID: my-trace-id-123" \
-H "Authorization: Bearer $TOKEN" \
https://api.insurancegateway.gr/...
Retry strategy with code samples¶
For 429, 502, 503, 504 the right strategy is exponential backoff with
jitter. Concrete numbers and ready-to-paste snippets in C#, Node.js and Python
follow.
Backoff parameters¶
| Parameter | Value | Notes |
|---|---|---|
| Initial delay | 1s | First retry after 1 second |
| Multiplier | 2x | Exponential growth (1s → 2s → 4s → 8s → 16s) |
| Max attempts | 5 | After this, escalate / fail |
| Jitter | ±25% | Random offset to avoid thundering herd |
| Max delay cap | 30s | Never wait longer than this between retries |
C# — Polly¶
var policy = Policy
.HandleResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.BadGateway ||
r.StatusCode == HttpStatusCode.GatewayTimeout ||
r.StatusCode == HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: attempt =>
TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, attempt)))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(-250, 250)));
Node.js — axios-retry¶
axiosRetry(axios, {
retries: 5,
retryDelay: (retryCount) => {
const delay = Math.min(30000, Math.pow(2, retryCount) * 1000);
const jitter = Math.random() * 500 - 250;
return delay + jitter;
},
retryCondition: (error) => {
const status = error.response?.status;
return status === 502 || status === 504 || status === 429;
}
});
Python — tenacity¶
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=1, max=30, jitter=0.25),
retry=retry_if_exception(lambda e: e.response.status_code in (502, 504, 429))
)
def call_estia(...): ...
POST/PATCH idempotency — check-before-retry¶
For mutating calls (POST/PATCH) that returned 502/504, don't blind-retry.
Before a second write, do a GET by external reference (e.g. quote ID,
external request ID) to verify whether the upstream already created the entity.
If it did, treat that as success — do not retry.
// Pseudocode: check-before-retry for a POST that returned 502/504
var existing = await api.GetQuoteByExternalRefAsync(externalRef);
if (existing is not null)
return existing; // already created upstream — success
return await api.CreateQuoteAsync(payload); // safe to retry
Retry guide¶
| Status | Retry? | Notes |
|---|---|---|
400, 401, 403, 404, 422 |
No | Fix request or credentials |
409 |
Maybe | Check if it already completed |
429 |
Yes | Respect Retry-After, exponential backoff |
500 |
Limited | A few attempts with spacing |
502, 503, 504 |
Yes | Idempotent calls or careful retries |
Mutating requests
Don't blind-retry POST/PATCH that change state. If the request partly
succeeded upstream, a retry can create duplicates or inconsistent state.