A Shared Singleton JAX-RS Resource and a Mapper That Never Runs
A telecom exposes a provisioning API built with JAX-RS 2.0 (Jersey 2) on GlassFish 4. The root resource AccountResource is annotated @Singleton because "it must not be recreated per request" and holds a shared SimpleDateFormat plus an ArrayList cache. Mobile clients send Accept: application/json; a billing partner sends Accept: application/xml. Both worked until release 3 added @Produces({"application/json", "application/xml", "application/vnd.acme.v2+json"}) to the invoice method, after which the mobile app receives 406 and the partner occasionally receives malformed dates or HTTP 500 with NumberFormatException.
A POST endpoint for top-ups returns 415 when the partner sends Content-Type: text/plain, and the team's plan is to accept anything. Their ExceptionMapper, annotated @Provider and meant to produce a uniform JSON error envelope, never runs: every error still comes back as the container's HTML page. The application class RestApplication extends Application and its getClasses() returns a set containing only AccountResource.class.
1. Explain the 406 on the invoice method and the 415 on the top-up POST, including which side of the exchange each one blames and how to fix each.
Read the worked answer
406 Not Acceptable - the client's Accept cannot be satisfied. Content negotiation runs before the method body: the runtime intersects the media types the client will accept with the @Produces set on the matched method (or class) and picks the best match by specificity and q-value. Adding application/vnd.acme.v2+json does not widen the set for a client that sends only Accept: application/json - that type is still declared, so 406 is not automatic here. The realistic causes are: (a) the method (or class) now declares only the vendor type in a different revision, (b) the client sends Accept: application/json;q=0, or (c) a MessageBodyWriter for the selected type is absent. Diagnose by logging the resolved @Produces: 406 always means "the server had no representation the client declared it would accept".
Fixes: keep application/json in @Produces alongside the vendor type, or set qs to express preference (@Produces({"application/vnd.acme.v2+json;qs=0.9","application/json;qs=0.5"})), or have the mobile client send the vendor type. Note that Content-Type is irrelevant to a GET's negotiation - that is the usual confusion on this kind of question.
415 Unsupported Media Type - the server cannot read what the client sent. For a POST, the runtime matches the request's Content-Type against @Consumes; with no match it returns 415 before invoking the method. It is the mirror image of 406.
Do not "accept anything". Declaring @Consumes(MediaType.WILDCARD) pushes the problem into the body reader: an unmatched type still needs a MessageBodyReader, and you lose the contract. Better: declare exactly the supported types (application/json, application/xml) and document the 415, or add a custom MessageBodyReader<Charge> for text/plain if the partner is genuinely sending CSV-like text.
2. Why is @Singleton on the resource class a defect here, and what is the correct fix consistent with JAX-RS lifecycle rules?
Read the worked answer
Default lifecycle. A JAX-RS root resource class is, by specification, instantiated per request (and with CDI, the default scope is @RequestScoped). That means ordinary instance fields are effectively request-local and need no synchronization - which is exactly why the class worked before. Annotating the class @Singleton changes it into one shared instance for the application's lifetime: instance fields become global mutable state, visible to every concurrent request.
What breaks.
SimpleDateFormatis not thread-safe. Its internalCalendaris mutated by bothformatandparse; concurrent use produces garbled output, wrong dates (the partner's "malformed dates"), orNumberFormatException/ArrayIndexOutOfBoundsExceptionsurfacing as 500s. This is the exact symptom described, and it is intermittent because it depends on timing.- The
ArrayListcache is a second hazard: concurrentaddcan corrupt the internal array or lose elements, and iteration during a resize can throwConcurrentModificationException.
Fixes.
- Remove
@Singleton. The per-request instance has no shared state and the JVM allocation is trivial compared with the request cost. - Replace
SimpleDateFormatwithjava.time.format.DateTimeFormatter(immutable and thread-safe) and use it as astatic finalconstant. - If the cache genuinely must be shared, make it an
@ApplicationScopedCDI bean holding aConcurrentHashMap, injected into the resource - separation of concerns, and a clear concurrency contract. - If you keep a shared formatter in legacy code, wrap it in a
ThreadLocalor synchronize, but that is a workaround, not a design.
Remember that sub-resource locators return objects whose lifecycle is also per-request unless annotated; a @Singleton returned from a locator is invalid in JAX-RS 2.0 and the runtime will report an error.
3. Why does the ExceptionMapper<Throwable> never execute, and what will happen once it does? Include the provider registration rules and the pitfalls for WebApplicationException.
Read the worked answer
Root cause: scanning is disabled. When an Application subclass overrides getClasses() and returns a non-empty set, the JAX-RS runtime uses exactly that set and does not perform class-path scanning for @Provider classes. If getClasses() returns an empty set, the implementation falls back to scanning. Because RestApplication returns only AccountResource.class, the mapper is never registered - it is not a mapper, it is just an unregistered class. This is the single most common cause of "my @Provider is ignored".
Fixes.
- Return the providers too:
getClasses()returnsAccountResource.classandErrorEnvelopeMapper.class(and anyMessageBodyReader/Writer,ContainerRequestFilter,Feature). - Or return
Collections.emptySet()fromgetClasses()and let the container scan, keeping@ApplicationPathfor the base URI. - Or register in
web.xmlvia the Jersey servlet'sjersey.config.server.provider.classnamesinit-param.
Once it runs, mind these traps.
- A mapper for
ThrowableorExceptionalso catchesWebApplicationExceptionin most implementations. If you return a generic 500 envelope, a legitimate 404 or 401 becomes a 500. Preserve the status:if (e instanceof WebApplicationException) { status = ((WebApplicationException) e).getResponse().getStatus(); }. - Precedence is by nearest superclass: a
BankRejectionExceptionwill be handled byExceptionMapper<BankRejectionException>if present, elseExceptionMapper<Exception>, elseThrowable. - Never leak stack traces or SQL into the envelope; log with a correlation id and return that id.
- Mappers cannot be used to alter responses produced before JAX-RS is reached (container-level 404 for an unmapped URL, or errors from an authentication constraint), which are still rendered by the servlet container's error pages; configure
<error-page>separately for those.