Returning JSON From AEM Custom Error Handlers for API Endpoints
When a mobile app or single-page front-end hits an Adobe Experience Manager endpoint expecting JSON, the worst outcome is an HTML stack trace in the response body. Teams in Sydney, Melbourne and Brisbane integrating AEM into larger digital estates for organisations like NAB or IAG have seen this firsthand, where a misrouted request can derail a release. Getting the error pipeline right early separates a graceful fallback from a tangled support queue.
This is the kind of plumbing question that engineers explored at the CIRCUIT developer conference sessions, and the patterns have aged well. Returning JSON for API failures, rather than letting the default Sling handler render HTML, requires deliberate work in OSGi services and JCR error scripts. The rest of this piece walks through building that behaviour reliably.
Why API Error Handling Matters in AEM
AEM ships with a default error pipeline rooted in Sling's SlingErrorHandler, which writes responses suited to HTML browsers. That works for content authors previewing a broken page, but it actively harms anything consuming AEM through a JSON contract. A SPA or a mobile app downloaded in Adelaide expects a structured payload with a status code, an error key, and a developer-friendly message.
Beyond developer experience, there are compliance reasons to tighten this loop. The Notifiable Data Breaches scheme under the Office of the Australian Information Commissioner expects prompt error reporting in systems touching personal information, and a miscoded 500 exposing internal class names can become a privacy finding. Australian API clients often differ from peers elsewhere: many local teams wire AEM into gateway layers for open banking APIs, ATO integrations via myGov-style flows, or Telstra-powered messaging backends, where clients rarely retry on HTML 500s.
Anatomy of an AEM Custom Error Handler
A custom error handler takes one of two forms. The first is a JCR script under /apps/sling/servlet/errorhandler, where each code maps to a file such as 500.jsp or 404.jsp. The second, more flexible approach for API work, is an OSGi service implementing the Sling ErrorHandler interface, bound with a higher service ranking than the default.
For JSON responses, the OSGi route is almost always cleaner. You get full control over response headers, character encoding, and the serialised body, and you can use Jackson annotations if your team already uses Jackson elsewhere. The script route still has uses, particularly when you want different behaviour per site or run mode. You also need to think about the dispatcher cache on the edge, since a cached error response is a notorious source of confusion during cutovers from older CQ5 instances into modern AEMaaCS setups.
Detecting API Requests and Switching Content Types
The crucial trick is knowing when to switch from HTML to JSON. A common pattern is to inspect the URL suffix or a request header. Endpoints under /api/ are an obvious signal, and teams often encode the contract in the URL itself, such as /content/myproject/api/products.json. Looking for the .json suffix in the path gives a clean and obvious signal.
Request headers are equally reliable. Clients following REST conventions usually send Accept: application/json, and honouring it is what most integrators expect. Some Australian teams also rely on X-Requested-With or a custom header such as X-AEM-Client to disambiguate, particularly when the same servlet serves both editorial previews and programmatic consumers. Inside the handler, read either signal and set the response content type accordingly, branching early so JSON-only paths never touch the JSP rendering pipeline.
Crafting Structured JSON Error Responses
Once you know you are producing JSON, the shape of the payload matters more than the wire format itself. A predictable envelope, something like { "error": { "code": 500, "message": "Internal error", "requestId": "..." } }, lets front-end teams write a single error renderer that works across every endpoint. Consistency saves an on-call rotation at 2am during a Melbourne thunderstorm when the NBN drops and your mobile app fails in waves.
Including a request identifier is small effort but pays for itself many times over. The identifier can come from a request header, a generated UUID, or an MDC entry if you have request tracing wired through SLF4J. Australian teams often pipe this identifier into Splunk or ELK in a Sydney or Canberra region, where correlation between AEM logs and upstream gateway logs becomes far less painful.
Where teams get into trouble is mixing human-readable and developer messages in the same field. Keep the user-facing message in one field and technical detail in another, clearly marked. Never expose stack traces or class names in production, which is a common finding during reviews with partners such as the Australian Cyber Security Centre and far easier to get right from the start than retrofit under audit pressure.
Logging and Monitoring JSON Errors
Logging JSON error responses well benefits from a bit of forethought. Log the response payload at WARN for 4xx and ERROR for 5xx, and include the request method, path, and status code on every entry. Structured logging through Logback, with fields your log shipper can index directly, is worth the small setup cost once you are dealing with production volumes.
Integrating with external monitoring is another matter in Australian enterprise contexts. Teams running AEM for telcos such as Optus or for government services often pipe errors into PagerDuty or Opsgenie, with routing keys that match run mode and environment. A spike in 500s on the API path should page the AEM on-call rotation, while 404s on retired content can stay as dashboard noise.
For those building alerting maturity, linking out to deeper integrations like the work covered in integrating AEM with single sign-on providers via SAML 2.0 shows how error paths can sit cleanly alongside authentication flows in the same monitoring view.
Testing Error Handlers Across Environments
Testing deserves its own section because error handlers are easy to ship broken. Unit tests should cover every status code branch, with assertions on both response content type and JSON shape. Integration tests against a running AEM author instance, or against AEMaaCS in a cloud environment, should cover the dispatcher boundary, where behaviour often surprises people.
Local Australian teams often stage these tests against a sandbox in the asia-southeast region to keep round-trip latency realistic for users in Perth or Hobart. That staging step is also a chance to verify error responses do not leak through caching layers unexpectedly, which is a common finding during cutovers from older dispatcher setups to newer CDN-tied ones.
Finally, do not forget chaos testing. Drop a downstream dependency, force a null pointer in a service, return a malformed payload from an integration, and confirm the handler produces sensible JSON every time. Pair this with the companion mobile app workflow to validate the consumer side renders errors gracefully rather than spinning forever.
Practical Recommendations for Robust JSON Error Handlers
- Default to JSON for any path under /api/ or with a .json suffix, and let Accept header negotiation steer ambiguous cases.
- Use an OSGi ErrorHandler service with a higher service ranking rather than relying solely on JCR scripts, so behaviour is testable and consistent across run modes.
- Return a predictable envelope with at least a code, a message, and a request identifier; keep developer detail in a separate, clearly labelled field.
- Strip stack traces and internal class names from any production response, aligned with guidance from the Australian Cyber Security Centre.
- Log structured fields for every API error and wire 5xx spikes into your paging rotation while letting 4xx errors stay as dashboard telemetry.
- Verify behaviour through dispatcher and CDN layers, and rehearse error handling in staging regions close to your Australian user base.
If you are building or maintaining AEM integrations, take a moment to revisit your error handler configuration before the next release window. A small amount of effort here pays back in calmer on-call shifts, cleaner audit trails, and front-end teams who can ship features without second-guessing what AEM will hand them when something goes wrong.