HTTP Problem Details in TypeScript
Parse RFC 9457 errors into typed recovery and redacted telemetry without trusting network JSON.
HTTP Problem Details give an API a standard error envelope, but a TypeScript union is trustworthy only after the network payload has been validated at runtime.
This tutorial builds that boundary around type URIs, status consistency, extension members, user-safe recovery, telemetry redaction, and forward-compatible tests.
HTTP Problem Details begins at untrusted JSON
A response carrying application/problem+json may come from the intended API, a proxy, an older service, or a broken gateway. Parse the body defensively, cap its size, and treat every member as unknown until checked. If JSON parsing fails, retain the HTTP status, request correlation headers, and a bounded body sample rather than throwing away the original failure.
TypeScript types disappear at runtime, so casting response.json() to ProblemDetails creates confidence without evidence. The boundary should always return either a validated problem or a distinct transport/parse failure.
For HTTP Problem Details, the working artifact is a total network-error parser. It records URL, method, response status, content type, size, parse outcome, safe sample, and correlation ID. I would stop the release when a type assertion replaces runtime validation; that failure means the evidence cannot support this step's claim.
An unknown extension member should pass through a total network-error parser; capture URL, method, response status, content type, size, parse outcome, safe sample, and correlation ID. Stop when a type assertion replaces runtime validation, because that outcome breaks the first boundary under test.
The exact implementation vocabulary here includes RFC 9457, so the term remains connected to a concrete decision rather than hidden in metadata.
Model the standard fields precisely
RFC 9457 defines type, status, title, detail, and instance with specific roles and defaults. Validate URI references where required, finite integer status values, and string fields without treating title as a stable machine code. Compare a present problem status with the actual HTTP status and record mismatches; the HTTP response remains authoritative for protocol behavior.
application/problem+json identifies the representation, not a single business error. Keep optional members optional and preserve unknown extensions in a bounded record after excluding prototype-sensitive keys.
The decision surface for HTTP Problem Details is a runtime standard-member validator. Its compact receipt contains raw value classes, normalized members, HTTP mismatch, discarded keys, and parser version. If title text drives program control, the route stays unresolved and returns to design before polish.
A stale client union should encounter a runtime standard-member validator; an uninvolved reviewer must recover raw value classes, normalized members, HTTP mismatch, discarded keys, and parser version. Hold the next action when title text drives program control.
The primary references for this decision are RFC 9457 Problem Details, IANA HTTP Problem Types registry, and TypeScript narrowing handbook. RFC 9457 defines the interoperable envelope, the problem-type registry guidance shapes durable identifiers, and TypeScript narrowing governs client handling. Product-specific extension members still need schemas, privacy review, and retry semantics of their own.
The exact implementation vocabulary here includes application/problem+json, so the term remains connected to a concrete decision rather than hidden in metadata.
- Parse: Bound and decode the representation.
- Validate: Check standard member shapes.
- Narrow: Match known URI and extensions.
- Adapt: Create user recovery and operator evidence.
Use stable problem type URIs for narrowing
Define a catalog of owned problem type URIs such as out-of-credit or version-conflict, then narrow by the normalized absolute URI. Each known type gets a validator for its extension members and a recovery policy. Unknown types remain valid generic problems when their standard fields pass; clients must not crash because a server adds a new type.
A dereferenceable URI can document semantics, but runtime behavior should not require fetching it during an error. Problem type URI identity is a contract that needs version and compatibility review.
I would review HTTP Problem Details through a discriminated URI catalog, not a slide assembled after implementation. The saved evidence is type URI, owner, extension schema, user action, retry rule, documentation revision, and unknown fallback. The explicit rejection rule is simple: a closed union makes new server errors fatal.
Sensitive validation input should be absent from a discriminated URI catalog, with type URI, owner, extension schema, user action, retry rule, documentation revision, and unknown fallback retained for comparison. Reopen the design if a closed union makes new server errors fatal.
The exact implementation vocabulary here includes problem type URI, so the term remains connected to a concrete decision rather than hidden in metadata.
Validate extensions inside each type
An out-of-credit problem might add balance, required amount, currency, and a top-up URL. Validate those fields only after the type is known and keep money as decimal strings or minor units under an explicit currency contract. A validation problem may carry field pointers and messages; cap array length and reject unsafe paths before mapping them to controls.
Extension members should help recovery without duplicating secrets or entire domain records. If a known type arrives with invalid extensions, preserve its standard explanation but downgrade typed automation.
This part of HTTP Problem Details becomes testable through a per-type extension validator. Preserve type, extension checks, caps, redactions, typed recovery eligibility, and generic fallback. Treat the step as failed whenever unknown extension values flow into commands or markup, even when the visual result appears convincing.
A non-JSON upstream failure should normalize through a per-type extension validator; the fallback receipt is type, extension checks, caps, redactions, typed recovery eligibility, and generic fallback. Treat unknown extension values flow into commands or markup as an explicit failed state.
The exact implementation vocabulary here includes TypeScript error handling, so the term remains connected to a concrete decision rather than hidden in metadata.
Separate user copy from operator evidence
The server's detail can contain debugging context or wording inappropriate for the interface. Map validated type and safe extensions to product-owned recovery copy, while telemetry retains a redacted problem digest, request ID, service, status, and parser result. Never render arbitrary detail as HTML.
The user needs what happened, what remains safe, and the next action; the operator needs correlation and contract evidence. TypeScript error handling becomes cleaner when those audiences are separate adapters over one validated object rather than competing interpretations in every component.
For HTTP Problem Details, the working artifact is a two-audience error adapter. It records problem digest, user message key, safe interpolation, operator fields, redaction decision, and next action. I would stop the release when server detail is displayed or logged verbatim everywhere; that failure means the evidence cannot support this step's claim.
An unknown extension member should pass through a two-audience error adapter; capture problem digest, user message key, safe interpolation, operator fields, redaction decision, and next action. Stop when server detail is displayed or logged verbatim everywhere, because that outcome breaks the first boundary under test.
Make retry semantics type-specific
A timeout, rate limit, validation issue, conflict, and insufficient credit should not share one Retry button. Combine HTTP method safety, idempotency key state, Retry-After, problem type, and known effect receipt before deciding. A payment request that timed out may need reconciliation, not replay.
A version conflict may reload and preserve local edits. Document whether the problem is transient, user-correctable, authorization-bound, or final. The error boundary can recommend an action, but the domain command owns whether repeating is safe.
The decision surface for HTTP Problem Details is a problem-to-recovery decision table. Its compact receipt contains method, effect key, type, status, retry hint, known outcome, chosen action, and owner. If all 4xx or 5xx responses expose the same retry behavior, the route stays unresolved and returns to design before polish.
A stale client union should encounter a problem-to-recovery decision table; an uninvolved reviewer must recover method, effect key, type, status, retry hint, known outcome, chosen action, and owner. Hold the next action when all 4xx or 5xx responses expose the same retry behavior.
| Member | Purpose | Control |
|---|---|---|
| type | Machine identity | URI catalog |
| title | Short summary | Not control flow |
| detail | Occurrence context | Escape + redact |
| extensions | Typed recovery | Per-type validation |
Test proxies and compatibility drift
Replay correct problems, missing content type, HTML gateway errors, status mismatch, relative type URI, unknown type, invalid extension, oversized detail, duplicate field pointers, Unicode, empty body, and an old client facing a new server type. Assert that parsing never throws outside the boundary and that no unsafe value reaches markup or automation.
Store golden payloads from controlled fixtures rather than private production errors. Schema evolution should add behavior without changing the meaning of established type URIs silently.
I would review HTTP Problem Details through a hostile problem-response corpus, not a slide assembled after implementation. The saved evidence is fixture ID, response headers, body digest, parser result, typed branch, user action, and telemetry output. The explicit rejection rule is simple: tests cover only ideal server-generated JSON.
Sensitive validation input should be absent from a hostile problem-response corpus, with fixture ID, response headers, body digest, parser result, typed branch, user action, and telemetry output retained for comparison. Reopen the design if tests cover only ideal server-generated JSON.
- 1Parse
Bound and decode the representation.
- 2Validate
Check standard member shapes.
- 3Narrow
Match known URI and extensions.
- 4Adapt
Create user recovery and operator evidence.
Connect errors to product state contracts
Human error design supplies recovery language, schema evolution supplies compatibility discipline, resilient loading states keep prior content safe, and idempotency contracts prevent duplicate effects. Link those articles in the implementation note so HTTP Problem Details stays an API envelope rather than the entire product experience.
A well-formed problem can still lead to a poor interface if focus disappears, edits are erased, or an ambiguous mutation is retried. The complete route joins protocol evidence to domain and interaction ownership.
This part of HTTP Problem Details becomes testable through an error-responsibility map. Preserve protocol parser, domain policy, UI state, focus recovery, effect reconciliation, and support route. Treat the step as failed whenever the presence of RFC fields is treated as complete error UX, even when the visual result appears convincing.
A non-JSON upstream failure should normalize through an error-responsibility map; the fallback receipt is protocol parser, domain policy, UI state, focus recovery, effect reconciliation, and support route. Treat the presence of RFC fields is treated as complete error UX as an explicit failed state.
Related implementation evidence lives in API error design for humans, agent tool schema evolution, resilient loading states, and idempotency lifecycle contracts. Schema validation, client generation, telemetry, and API error UX consume the same problem type for different purposes. Keep their fixtures aligned on the type URI without collapsing transport status, domain cause, and user recovery into one string.
Publish the problem catalog as a contract
Ship the parser with a catalog of owned types, fixtures, extension schemas, recovery mappings, redaction policy, and metrics for unknown or invalid problems. Review unknown-type rate and status mismatches before tightening unions.
Keep the generic fallback useful and preserve request correlation without exposing protected data. HTTP Problem Details succeeds when new server errors degrade safely, established errors produce precise recovery, and an engineer can reconstruct why the client chose a branch from a compact receipt.
For HTTP Problem Details, the working artifact is a versioned problem-details catalog. It records catalog revision, parser revision, known types, compatibility policy, fixtures, unknown rate, and review owner. I would stop the release when client behavior exists only as scattered switch statements; that failure means the evidence cannot support this step's claim.
An unknown extension member should pass through a versioned problem-details catalog; capture catalog revision, parser revision, known types, compatibility policy, fixtures, unknown rate, and review owner. Stop when client behavior exists only as scattered switch statements, because that outcome breaks the first boundary under test.
The artifact validates standard members, canonicalizes the type URI, and retains a correlation extension.
Runnable artifact — problem-details-contract.test.mjs
import assert from "node:assert/strict";
const parse=x=>({type:new URL(x.type).href,status:Number(x.status),title:String(x.title),requestId:String(x.requestId||"")});
const p=parse({type:"https://api.example/problems/out-of-credit",status:403,title:"Out of credit",requestId:"r1"});
assert.equal(p.status,403);assert.match(p.type,/out-of-credit$/);assert.equal(p.requestId,"r1");
console.log("PASS: problem details contract stable");
Run node problem-details-contract.test.mjs. Expected receipt: PASS: problem details contract stable.
Use HTTP Problem Details as a stable typed error contract whose URI identifies semantics and whose extensions remain narrowly documented. Reopen each TypeScript mapping when a type or retry rule changes, and preserve unknown members so independent clients can evolve without unsafe guesses.