HomeJournalThis post

HTTP Problem Details in TypeScript

Parse RFC 9457 errors into typed recovery and redacted telemetry without trusting network JSON.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

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.

Problem payload through a trusted boundaryAn untrusted HTTP response passes content, standard-field, type-URI, and extension checks before splitting into user recovery and operator evidence.responsestandardfieldstype +extensionsnarrowuserops
  • Parse: Bound and decode the representation.
  • Validate: Check standard member shapes.
  • Narrow: Match known URI and extensions.
  • Adapt: Create user recovery and operator evidence.
Figure 1: Runtime validation creates the TypeScript narrowing boundary; network JSON never crosses it by assertion.

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.

MemberPurposeControl
typeMachine identityURI catalog
titleShort summaryNot control flow
detailOccurrence contextEscape + redact
extensionsTyped recoveryPer-type validation
Figure 2: Standard and extension members have different trust roles.

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.

  1. 1Parse

    Bound and decode the representation.

  2. 2Validate

    Check standard member shapes.

  3. 3Narrow

    Match known URI and extensions.

  4. 4Adapt

    Create user recovery and operator evidence.

Figure 3: Unknown types remain safe without freezing server evolution.

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.