Error Handling
The library maps PHP exceptions to OData-compliant error responses.
Error response format
All errors follow the OData error format:
{
"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:
| Exception | HTTP Status | OData Code | When thrown |
|---|---|---|---|
BadRequestException | 400 | bad_request | Malformed query options, invalid filter syntax, unknown entity sets and properties, write verbs (method_not_allowed) |
ForbiddenException | 403 | (from the denial) | A read authorizer recorded a hard denial for the target set |
NotFoundException | 404 | not_found | No entity matches the key (entity_not_found) |
NotImplementedException | 501 | not_implemented | Unsupported operations ($apply, missing EntityResolverInterface) |
InternalServerErrorException | 500 | internal_server_error | Unexpected 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:
use LaravelUi5\OData\Exception\NotFoundException;
throw (new NotFoundException())
->code('entity_not_found')
->message("Product with id=42 not found.")
->target('Products(42)');Adding details
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:
{
"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:
- The response starts streaming with HTTP 200 and announces a
trailer: odata-errorheader - If an exception occurs while yielding rows, the stream stops where it is
- 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:
throw (new BadRequestException())
->message('Rate limit exceeded')
->header('Retry-After', '60');