skip to content

What does an OData 4.01 JSON error response body contain, and how does a service signal an error that occurs mid-response?

level: middleimportance: should knowfreq 15%

answer

  1. one member wrapping everything
  2. a sub-status beneath the HTTP code
  3. language of the message
  4. three optional members
  5. truncate, then maybe a trailer

basics

~20 s

One JSON object whose single error member holds code (service-defined, language-independent) and message (human-readable), plus optional target, details and innererror. An error after 200 OK is signalled by leaving the payload malformed, optionally with an OData-Error trailer.

solid answer

~50 s

An OData JSON error response is a single object with one member, `error`. It **must** hold `code`, a non-empty, language-independent, service-defined string acting as a sub-status of the HTTP status, and `message`, a non-empty human-readable string whose language the `Content-Language` header must state. It **may** add `target` (what failed, such as a property name), `details` (an array of objects with their own `code`, `message` and optional `target`, for example one per invalid field) and `innererror` (a service-defined object, usually debugging data, which services should vet for information disclosure). If a service fails **after** sending a success status, it cannot change the status: OData 4.01 requires it to leave the response **malformed** - stop serialising so the JSON never closes - and clients must treat the whole response as an error. Where the transport supports trailers, the service may add an `OData-Error` trailer carrying the error object.

go deeper

for a junior

Recall the shape: an error object with a required code and message, and optional target, details and innererror.

for a middle

Explain code as a stable sub-status, message as localised text tied to Content-Language, and details with targets for field-level validation errors.

for a senior

Handle in-stream failures: a 200 response that fails to parse is an error by rule, the OData-Error trailer may carry the cause, and innererror must not leak internals.

for a principal

Set an error-code catalogue that clients can rely on across services, deciding what goes into innererror per environment without breaking clients.

## The error object When an OData request fails before the response starts, the service returns an error status with a JSON body that is **a single object with a single member named `error`**. Its value is the **OData error object**: ```json { "error": { "code": "ORD-VALIDATION", "message": "The order could not be saved.", "details": [ { "code": "ORD-QTY", "target": "Quantity", "message": "Quantity must be at least 1." }, { "code": "ORD-DATE", "target": "ShipDate", "message": "Ship date is in the past." } ], "innererror": { "correlationId": "8f2c41" } } } ``` | Member | Required | Content | |---|---|---| | `code` | yes | non-empty, language-independent, service-defined string; a **sub-status** of the HTTP status code; never null | | `message` | yes | non-empty, language-dependent, human-readable text; `Content-Language` must name its language; never null | | `target` | no | what the error is about, for example a property name; may be empty or null | | `details` | no | array of objects, each with `code` and `message` and optionally `target` | | `innererror` | no | an object with **service-defined** content, usually debugging help | Any of these objects may also carry annotations. ## How a client should use it 1. **Branch on the HTTP status first** - it gives the class of failure (404, 412, 428, 501 and so on). 2. **Branch on `code` next**, never on `message`. The code is stable and language-independent; the message is meant for people and is language-dependent. 3. **Map `details` to the user interface**: each entry's `target` lets a client mark the right field, which is why validation failures usually come as a top-level code plus one detail per problem. 4. **Treat `innererror` as opaque.** Its content is up to the service. The specification warns services to consider carefully what they include in production, because stack traces and internal identifiers there are an information-disclosure risk. A service that does not implement requested functionality answers `501 Not Implemented` and should describe the missing functionality in the body. ## Errors after the status line: in-stream errors Large responses are often streamed, so a service may have sent `200 OK` and half a collection before something fails. The status cannot be changed any more. OData 4.01's rule: - The service **must leave the response malformed** according to its content type - for JSON, typically by stopping serialisation so that, among other things, the closing brace of the top-level object never arrives. - Clients **must treat the entire response as being in error**, even though the status said success. - If the transport supports **trailing headers** (HTTP/1.1 with chunked transfer encoding, or HTTP/2), the service **may** add an **`OData-Error`** trailer holding the error object on one line, with optional whitespace removed and control characters and characters beyond `00FF` escaped. The point of malformation is safety: a payload that parses cleanly but is silently incomplete would be mistaken for the full result. OData 4.0 already required clients to treat such a response as failed, but only said the service must generate an error within the payload, which may leave it malformed, and prescribed no format; deliberate malformation and the trailer arrived in 4.01. ## Error information inside a success payload A third case is error information inside a `200` payload. A primitive value in error is annotated with `Core.ValueException`. Structured values in error, and partial collections, appear only when the client asked for partial results with the continue-on-error preference: such items are annotated with `Core.ResourceException`, and a partial collection must end with a next link the client can use to try fetching the rest. ## Designing codes clients can rely on The specification fixes the shape; the values are the service's. Practices that keep the shape useful: 1. Treat `code` values as part of the contract: document them, keep them stable, and never reuse one for a different condition. 2. Use one top-level code for the failure and one `details` entry per problem, each with a `target`, so a client can show all validation errors at once. 3. Decide per environment what `innererror` may carry; a correlation identifier is usually enough in production. ## A common confusion The OData error body is its own shape - an `error` wrapper with `code` and `message` - and is not the problem-details document some HTTP APIs return. A generic error handler expecting one will not find its fields in the other.

  • Why should a client branch on code rather than on message?
    `code` is a language-independent, service-defined value that acts as a sub-status of the HTTP status, so it stays stable. `message` is language-dependent text for humans: its language, which `Content-Language` must name, can differ between requests, and nothing makes its wording stable.
  • Why can't a service simply switch to 500 when a streamed collection fails halfway?
    The status line and part of the body were already sent, so the status cannot change. OData 4.01 makes the service leave the payload malformed so that no client mistakes a truncated result for a complete one, and lets it add an `OData-Error` trailer where HTTP/1.1 chunked encoding or HTTP/2 supports trailers.

saying these in an interview costs you the question

  • The message member is the stable value clients should switch on.
  • code may be null when the HTTP status already says enough.
  • details is a list of free-form strings describing each problem.
  • Production services should put full stack traces in innererror.
  • After a mid-stream failure the service should close the JSON cleanly and append an error.
  • An OData error body carries type, title and status like a problem-details document.