Problem Details
RFC 9457 Problem Details for HTTP APIs — standardized error response envelope with type / title / status / detail / instance plus operator-supplied extensions. Sets Content-Type: application/problem+json per RFC 9457 §3 so clients can branch on the content type rather than scanning ad- hoc JSON shapes. Supersedes RFC 7807 (obsolete).
Operators wire this in three places: 1. b.problemDetails.create({...}) builds a problem object with field validation (type URI, status range, etc.). 2. b.problemDetails.respond(res, problem) serializes + sends. 3. b.problemDetails.fromError(err) converts a thrown FrameworkError into the matching problem document; the err.code (e.g. csv/invalid-record) becomes the type URI suffix (https://blamejs.com/problems/csv/ invalid-record).
b.problemDetails.validate(doc) parses an INBOUND problem response (e.g. when blamejs is the client of an upstream API returning RFC 9457) — refuses non-objects, refuses bad type URIs, refuses status outside 100..599.
Tier choices per feedback_validation_tier_policy.md: - create / fromError / validate — THROW on bad input (config-time / entry-point shape). - respond — THROW on bad input (its first call sets the response shape; silent drop would mask a programming bug).
b.problemDetails.setBase(baseUri) #
Override the base URI prepended to error-code-derived type URIs. Default is https://blamejs.com/problems. Operators running under their own published vocabulary (e.g. an internal status page or an organization-owned problem catalog) point this at the canonical location. Throws problem-details/bad-base for non-https / non- absolute / non-string inputs.
b.problemDetails.setBase("https://api.example.com/problems");
b.problemDetails.fromError(new Error("csv/invalid-record")).type;
// → "https://api.example.com/problems/csv/invalid-record"
b.problemDetails.getBase() #
Read the currently-configured base URI. Useful for diagnostic logging and tests.
b.problemDetails.getBase(); // → "https://blamejs.com/problems"
b.problemDetails.create(opts) #
{
type: string, // problem-type URI reference (default "about:blank")
title: string, // short summary
status: number, // integer 100..599
detail: string, // human-readable explanation
instance: string, // URI reference for this specific occurrence
extensions: object, // keys spread as top-level siblings (§3.2); direct top-level key wins on collision
...extensions // additional top-level keys preserved as-is
}
Build a frozen RFC 9457 problem-details object. Validates the standard fields per §3: - type (optional, defaults to "about:blank") must be a URI reference (string); MAY be relative or absolute. - title (recommended) must be a non-empty string when given. - status (recommended) must be an integer 100..599. - detail (optional) must be a string when given. - instance (optional) must be a URI reference string when given. - Extensions: every additional top-level key whose name is NOT in RESERVED_FIELDS is preserved at the top level. Reserved-name collisions throw problem-details/reserved-extension; prototype-pollution-shaped top-level keys throw the same. - extensions: a plain object whose keys are spread as top-level sibling members (RFC 9457 §3.2) — the literal extensions member is never emitted. Keys colliding with RESERVED_FIELDS are ignored (reserved fields can't be overridden by an extension); prototype-pollution-shaped keys are dropped silently. When the same name appears both as a direct top-level key and inside extensions, the direct top-level key wins.
Returns a frozen plain object suitable for JSON.stringify.
var p = b.problemDetails.create({
type: "https://example.com/problems/out-of-credit",
title: "You do not have enough credit.",
status: 403,
detail: "Your current balance is 30, but that costs 50.",
instance: "/account/12345/msgs/abc",
balance: 30,
accounts: ["/account/12345", "/account/67890"],
});
// → {
// type: "https://example.com/problems/out-of-credit",
// title: "You do not have enough credit.",
// status: 403,
// detail: "Your current balance is 30, but that costs 50.",
// instance: "/account/12345/msgs/abc",
// balance: 30,
// accounts: ["/account/12345", "/account/67890"]
// }
b.problemDetails.fromError(err, opts?) #
{
title: string, // override the derived title
instance: string, // request-instance URI reference
status: number, // override err.statusCode / default 500
}
Convert a thrown FrameworkError (or any error with a code field) into the matching problem-details object. The error's code (e.g. csv/invalid-record) becomes the type-URI suffix (); the error's message becomes detail; the error's statusCode becomes status when present, otherwise defaults to 500. Pass opts.title to override the title default (which is the error class name humanized: CsvError → "CSV Error"). Pass opts.instance to attach a request-instance reference (typically the audit-trail ID).
try {
b.csv.parse(badInput);
} catch (err) {
var problem = b.problemDetails.fromError(err, {
instance: "/audit/" + req.auditId,
});
b.problemDetails.respond(res, problem);
}
b.problemDetails.respond(res, problem, req?) #
Write a problem-details object to the response with the correct RFC 9457 §3 content type (application/problem+json). Sets Cache-Control: no-store (RFC 9111 §5.2.2.5 — error responses are individualized) and writes the JSON body. Status code is taken from problem.status (or 500 when missing). Throws problem-details/bad-res for non-response objects; throws problem-details/bad-problem for non-object problem inputs.
var problem = b.problemDetails.create({
type: "https://blamejs.com/problems/csv/invalid-record",
title: "CSV record validation failed",
status: 400,
detail: "Row 3 column 5 has an unterminated quoted field",
});
b.problemDetails.respond(res, problem);
// res.headers: Content-Type: application/problem+json
// Cache-Control: no-store
// res.body:
// res.statusCode: 400
b.problemDetails.send(res, fields) #
{
status: number, // HTTP status code (100..599); default 500
title: string, // operator-supplied short title
detail: string, // operator-supplied human-readable explanation
type: string, // problem-type URI (defaults to "about:blank")
instance: string, // optional per-occurrence URI
extensions: object, // keys spread as top-level siblings (§3.2); direct top-level key wins on collision
}
Build + emit a problem-details response in one call. Equivalent to respond(res, create(fields)) but lets routes migrate incrementally from inline res.status(400).json({ error: "..." }) shapes without restructuring the handler around an error throw.
The same RFC 9457 §3 application/problem+json content type + Cache-Control: no-store are written; status code defaults to 500 when omitted.
extensions keys are spread as top-level sibling members (RFC 9457 §3.2) via create — the literal extensions member is never emitted. Keys colliding with the reserved type / title / status / detail / instance are ignored; prototype-pollution- shaped keys are dropped. A direct top-level key wins over the same name nested under extensions.
// Migrating from inline JSON-error shape:
// res.status(400).json({ error: "Missing 'name' field" });
// to RFC 9457 problem-details:
b.problemDetails.send(res, {
status: 400,
title: "Missing required field",
detail: "Body field 'name' is required",
});
b.problemDetails.validate(doc) #
Validate an INBOUND problem-details document (e.g. one received from an upstream API). Returns the doc unchanged on success; throws problem-details/bad-inbound on shape violations. Useful when blamejs is the client of a RFC 9457-compliant upstream service — converts a "looks JSON-ish" response into a verified problem object before reading fields.
- Refuses non-object input. - Refuses status outside 100..599 or non-integer. - Refuses type / title / detail / instance of non-string shape when present. - Refuses prototype-pollution-shaped extension keys.
var rsp = await fetch(url);
if (rsp.headers.get("content-type") === "application/problem+json") {
var doc = b.problemDetails.validate(await rsp.json());
console.log(doc.title, doc.status, doc.detail);
}
Last updated 2026-08-08T16:39:49.652Z by seeder.