# REST error responses in 2026: RFC 9457 Problem Details vs the envelope you invented

Error responses are the least designed and most consumed part of most APIs. Teams spend weeks on the happy-path schemas and twenty minutes on the error body, which is what every client's retry logic, support tooling, and user-facing message depends on. The result is a dozen services with a dozen envelopes: `{error: "msg"}`, `{code, message}`, `{errors: [...]}`, `{status, error: {message}}`. In 2026 there is a mature standard — RFC 9457 Problem Details for HTTP APIs — and the case for adopting it is stronger than ever, especially once agents start calling your API.

## What Problem Details looks like

The media type is `application/problem+json`, and the body has well-defined fields:

``` json
{
  "type": "https://api.example.com/errors/plan-limit-reached",
  "title": "Plan limit reached",
  "status": 403,
  "detail": "The free plan allows 1 active project. Archive a project or upgrade to Pro.",
  "instance": "/v1/projects",
  "errors": [
    {
      "detail": "Project count (1) exceeds plan limit (1).",
      "pointer": "#/data/active_projects"
    }
  ]
}
```

The five core fields:

-   `type`: a URI identifying the problem. Resolvable to human documentation is ideal, but even an opaque stable URN works as a machine key.
-   `title`: a short human-readable summary, stable for the `type`.
-   `status`: the HTTP status repeated in the body, for clients that only see the body (logs, intermediaries).
-   `detail`: a human-readable explanation specific to this occurrence.
-   `instance`: the specific request URI or correlation reference.

Extensions are explicitly allowed — `errors` above is an extension for field-level validation detail. This is the standard's best design decision: it gives you a common spine without forbidding domain data.

## Why a standard beats a bespoke envelope

1.  **Generic tooling understands it.** Gateways, API clients, and agent frameworks increasingly render Problem Details natively; a bespoke format always needs custom parsing.
2.  **The distinction between type and detail forces discipline.** `type` is the stable machine code clients branch on; `detail` is the occurrence-specific sentence humans read. Conflating them — one free-text `message` field used for both — is the most common design error and the source of brittle string-matching in clients.
3.  **HTTP status stays authoritative.** The envelope reinforces rather than competes with the status code, so proxies and caches behave.
4.  **AI agents handle it without guessing.** When an MCP tool call fails, an agent reads `type` and `detail` and can correct the request (fix a field, request a different scope) rather than asking the user what a vendor-specific blob means.

## Documenting it in OpenAPI 3.2

Define the problem schema once in components and reference it from every error response:

``` yaml
components:
  responses:
    BadRequest:
      description: Malformed or invalid request.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Problem:
      type: object
      description: RFC 9457 Problem Details.
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: Stable identifier for the problem kind.
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              detail: { type: string }
              pointer: { type: string, description: 'JSON Pointer to the offending field.' }
              parameter: { type: string }
```

Then each operation lists the statuses it actually returns — not a generic "500 for everything." The documented error set is part of the contract: a `409 Conflict` with `type: project-name-taken` tells the client to surface a rename prompt; that flow cannot be built against an undocumented 400.

## The status code conventions worth keeping

Problem Details does not replace HTTP semantics; it sharpens them:

| Status | Meaning the client needs                  | Typical type                 |
|--------|-------------------------------------------|------------------------------|
| 400    | Malformed request; do not retry unchanged | validation-failed            |
| 401    | Missing/invalid authentication            | unauthorized                 |
| 403    | Authenticated but not permitted           | plan-limit-reached           |
| 404    | No such resource, or hidden for security  | not-found                    |
| 409    | Conflict with current state               | name-taken, version-conflict |
| 422    | Well-formed request with semantic errors  | semantic-validation          |
| 429    | Rate limited; honor Retry-After           | rate-limited                 |
| 5xx    | Server fault; retry with backoff          | internal-error               |

Two conventions matter for agents and automation: include a stable, documented `type` for every branch a client might take (retry, prompt, fail), and on 429/503 include `Retry-After` as a header — the spec should document that the client must honor it.

## Migration without a big-bang

Existing APIs usually cannot replace their envelope overnight. A low-risk path:

1.  Add `application/problem+json` as an additional error media type, negotiated via `Accept` or enabled per API version; keep the legacy format for old clients.
2.  Standardize internally first: new services emit Problem Details; a gateway adapter can translate legacy envelopes into it for generic tooling.
3.  Document both during the deprecation window, with the legacy response marked deprecated in the OpenAPI document and a sunset note.
4.  Align SDK error classes on `type` rather than regex-matching messages.

## What not to do

-   **Do not put stack traces or internal service names in `detail`.** It leaks topology and confuses users; log those server-side and return a correlation id in `instance`.
-   **Do not vary `title` per occurrence.** It belongs to the type; per-occurrence text goes in `detail`.
-   **Do not return 200 with an error body.** A surprising amount of legacy API behavior does this, and it makes agents and caches unrecoverably wrong.
-   **Do not forget validation arrays.** A form with five bad fields needs five entries; a single top-level error forces five round trips.

Designing the error model in the spec — and generating mocks that return realistic problems — lets frontend and agent developers build against failure on day one instead of discovering it in production. In Powerduck the problem schema lives in `components` like any other, scenario tests assert on both status and `type`, and mocks return the documented errors so failure flows are testable before the backend exists. The [demo](https://www.powerduck.com/app/?ref=powerduck.com) shows the workflow on a sample spec.

**What to read next:** [a 200 is not done — run the business scenario](https://www.powerduck.com/blog/a-200-is-not-done-run-the-business-scenario/) makes the case for testing failure branches, and [detect breaking API changes in CI](https://www.powerduck.com/blog/detect-breaking-api-changes-openapi-diff-ci/) catches the moment an error contract changes.

