Skip to content

Error Handling

The library maps PHP exceptions to OData-compliant error responses.

Error response format

All errors follow the OData error format:

json
{
  "error": {
    "code": "entity_not_found",
    "message": "No entity found in \"Products\" matching the given key.",
    "target": null,
    "details": [],
    "innererror": {}
  }
}

target, details and innererror are always present, empty when the exception set none.

Exception hierarchy

All protocol exceptions extend ProtocolException:

ExceptionHTTP StatusOData CodeWhen thrown
BadRequestException400bad_requestMalformed query options, invalid filter syntax, unknown entity sets and properties, write verbs (method_not_allowed)
ForbiddenException403(from the denial)A read authorizer recorded a hard denial for the target set
NotFoundException404not_foundNo entity matches the key (entity_not_found)
NotImplementedException501not_implementedUnsupported operations ($apply, missing EntityResolverInterface)
InternalServerErrorException500internal_server_errorUnexpected server errors

ForbiddenException is raised by the read-authorization gate, not thrown by application code: an authorizer records a hard denial and the engine converts it. See Read Authorization.

Building error responses

Exceptions use a fluent API:

php
use LaravelUi5\OData\Exception\NotFoundException;

throw (new NotFoundException())
    ->code('entity_not_found')
    ->message("Product with id=42 not found.")
    ->target('Products(42)');

Adding details

php
throw (new BadRequestException())
    ->code('invalid_filter')
    ->message('The $filter expression is invalid.')
    ->addDetail('syntax_error', 'Unexpected token at position 15', '$filter')
    ->addInnerError('expression', "name eq 'Widget' and");

Produces:

json
{
  "error": {
    "code": "invalid_filter",
    "message": "The $filter expression is invalid.",
    "target": null,
    "details": [
      {"code": "syntax_error", "message": "Unexpected token at position 15", "target": "$filter"}
    ],
    "innererror": {
      "expression": "name eq 'Widget' and"
    }
  }
}

Streaming error handling

A streamed response has already sent its headers — including 200 OK — by the time the first row is written. An exception raised while the rows are still being yielded therefore cannot change the status code. Something has to signal the failure in the part of the response that has not been sent yet.

When streaming is enabled in config (the default), that happens as follows:

  1. The response starts streaming with HTTP 200 and announces a trailer: odata-error header
  2. If an exception occurs while yielding rows, the stream stops where it is
  3. The error JSON is written at the end of the response, introduced by OData-error:

When streaming is disabled, the whole response is buffered. If an exception occurs, the buffer is discarded and a proper error response with the correct status code is sent instead.

A known flaw, and the way around it

Step 3 is not an HTTP trailer, although step 1 announces one. A real trailer field requires chunked transfer encoding and is sent after the message body; what the engine does is write OData-error: {…} into the body, behind the bytes already delivered.

Two consequences for a client: the announced trailer never arrives, and the body is no longer valid JSON — a strict parser fails on a response whose status code says 200. The truncated payload is the symptom you will see first.

The cure, until this is fixed: set streaming to false for the affected service (see Configuration). The response is then buffered, the exception produces a real error response with the right status code, and nothing is appended to a partial body. The cost is memory on large result sets, which is exactly what streaming exists to avoid — so treat it as a trade, not a default.

The fix is tracked in the package roadmap: either a genuine trailer, or the announcement is dropped and the in-body marker is documented as what it is. Whichever way it goes, a header promising something that never comes will not survive it.

Custom error responses

You can add custom headers to error responses:

php
throw (new BadRequestException())
    ->message('Rate limit exceeded')
    ->header('Retry-After', '60');