Jersey 3.1.10: ContainerResponse.close() triggers responseWriter.failure(e) → sendError(500, "Request failed."), losing exception message (regression vs 2.35)
Environment: jersey-server: 3.1.10 (previously 2.35) Spring Boot: 3.x Servlet container: Tomcat 9.0.108 Java: 21
Problem: After upgrading from Jersey 2.35 to 3.1.10, failures routed through ContainerResponse.close() now invoke responseWriter.failure(e), which results in HttpServletResponse.sendError(500, "Request failed."). The client receives a generic message instead of the original exception message or our structured error body. In 2.35 the exception message reached the client. This change broke part of our automated tests in a synthetic test application that validates detailed error text/stack trace.
Questions: Was this behavioral change intentional, and is it documented in 2.x → 3.x migration notes? What is the official, supported way to restore 2.35-style behavior where the exception message/body is returned to the client? Is there a recommended flag/configuration or extension hook to customize the behavior of failures (e.g., avoid the fixed “Request failed.” text) in a supported manner, suitable for test environments? If this is an unintended regression, would you consider adjusting the default or providing a dedicated configuration switch?
Hi @LordOfTheHouse do you have a small reproducer?. I will find what commit made that issue and I will investigate from there.
Hi @LordOfTheHouse, the 3.1 versions of Jersey implement JAXRS 3.1 spec, which introduces a default Exception Mapper requirement for every JAXRS implementation. That is why the exception is not thrown directly but is wrapped by the default exception mapper. There are 2 ways to work with this - either implement your custom exception mapper to precede the default or set the jersey.config.server.response.setStatusOverSendError property to true . I was thinking this is documented in the migration section of the user guide, but this obviously must be added when a new version of 3.1 is released.
In addition to this, we see one exception in Grizzly that seems to be related with this. Maybe in Tomcat it is happening too:
Oct 17, 2025 9:34:51 AM org.glassfish.jersey.server.ServerRuntime$Responder writeResponse
SEVERE: Error while closing the output stream in order to commit response.
java.lang.IllegalStateException: Illegal attempt to call getOutputStream() after getWriter() has already been called.
at org.glassfish.grizzly.http.server.Response.getNIOOutputStream(Response.java:578)
at org.glassfish.grizzly.http.server.Response.getOutputStream(Response.java:602)
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer$ResponseWriter.writeResponseStatusAndHeaders(GrizzlyHttpContainer.java:250)
at org.glassfish.jersey.server.ServerRuntime$Responder$1.getOutputStream(ServerRuntime.java:681)
at org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(CommittingOutputStream.java:174)
at org.glassfish.jersey.message.internal.CommittingOutputStream.flushBuffer(CommittingOutputStream.java:279)
at org.glassfish.jersey.message.internal.CommittingOutputStream.commit(CommittingOutputStream.java:235)
at org.glassfish.jersey.message.internal.CommittingOutputStream.close(CommittingOutputStream.java:250)
at org.glassfish.jersey.message.internal.OutboundMessageContext.close(OutboundMessageContext.java:569)
at org.glassfish.jersey.server.ContainerResponse.close(ContainerResponse.java:406)
at org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(ServerRuntime.java:763)
at org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(ServerRuntime.java:398)
at org.glassfish.jersey.server.ServerRuntime$Responder.processResponseWithDefaultExceptionMapper(ServerRuntime.java:647)
at org.glassfish.jersey.server.ServerRuntime$Responder.process(ServerRuntime.java:475)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:266)
at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:253)
at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:696)
at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(GrizzlyHttpContainer.java:367)
at org.glassfish.grizzly.http.server.HttpHandler$1.run(HttpHandler.java:190)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:535)
at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:515)
at java.base/java.lang.Thread.run(Thread.java:1583)
It happens when closing after responding with an error. For the case of Grizzly it complains because of one order of invocations that sets a usingWriter boolean to true and it should be false.
For the record, these are the stacks when it sends an error and why the flag is set to true:
usingWriter = false
Thread [grizzly-http-server-0] (Suspended (breakpoint at line 1253 in org.glassfish.grizzly.http.server.Response))
org.glassfish.grizzly.http.server.Response.sendError(int, java.lang.String) line: 1253
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer$ResponseWriter.failure(java.lang.Throwable) line: 266
org.glassfish.jersey.server.ServerRuntime$Responder.process(java.lang.Throwable) line: 467
org.glassfish.jersey.server.ServerRuntime$1.run() line: 282
org.glassfish.jersey.internal.Errors$1.call() line: 248
org.glassfish.jersey.internal.Errors$1.call() line: 244
org.glassfish.jersey.internal.Errors.process(java.util.concurrent.Callable<T>, boolean) line: 292
org.glassfish.jersey.internal.Errors.process(org.glassfish.jersey.internal.util.Producer<T>, boolean) line: 274
org.glassfish.jersey.internal.Errors.process(java.lang.Runnable) line: 244
org.glassfish.jersey.inject.hk2.Hk2RequestScope(org.glassfish.jersey.process.internal.RequestScope).runInScope(org.glassfish.jersey.process.internal.RequestContext, java.lang.Runnable) line: 266
org.glassfish.jersey.server.ServerRuntime.process(org.glassfish.jersey.server.ContainerRequest) line: 253
org.glassfish.jersey.server.ApplicationHandler.handle(org.glassfish.jersey.server.ContainerRequest) line: 696
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response) line: 387
org.glassfish.grizzly.http.server.HttpHandler$1.run() line: 190
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).doWork() line: 535
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).run() line: 515
java.lang.Thread.runWith(java.lang.Object, java.lang.Runnable) line: 1596
java.lang.Thread.run() line: 1583
usingWriter = true
Thread [grizzly-http-server-0] (Suspended (breakpoint at line 660 in org.glassfish.grizzly.http.server.Response))
org.glassfish.grizzly.http.server.Response.getNIOWriter() line: 660
org.glassfish.grizzly.http.server.Response.getWriter() line: 630
org.glassfish.grizzly.http.server.util.HtmlHelper.sendErrorPage(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response, org.glassfish.grizzly.http.server.ErrorPageGenerator, int, java.lang.String, java.lang.String, java.lang.Throwable) line: 64
org.glassfish.grizzly.http.server.Response.sendError(int, java.lang.String) line: 1268
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer$ResponseWriter.failure(java.lang.Throwable) line: 266
org.glassfish.jersey.server.ServerRuntime$Responder.process(java.lang.Throwable) line: 467
org.glassfish.jersey.server.ServerRuntime$1.run() line: 282
org.glassfish.jersey.internal.Errors$1.call() line: 248
org.glassfish.jersey.internal.Errors$1.call() line: 244
org.glassfish.jersey.internal.Errors.process(java.util.concurrent.Callable<T>, boolean) line: 292
org.glassfish.jersey.internal.Errors.process(org.glassfish.jersey.internal.util.Producer<T>, boolean) line: 274
org.glassfish.jersey.internal.Errors.process(java.lang.Runnable) line: 244
org.glassfish.jersey.inject.hk2.Hk2RequestScope(org.glassfish.jersey.process.internal.RequestScope).runInScope(org.glassfish.jersey.process.internal.RequestContext, java.lang.Runnable) line: 266
org.glassfish.jersey.server.ServerRuntime.process(org.glassfish.jersey.server.ContainerRequest) line: 253
org.glassfish.jersey.server.ApplicationHandler.handle(org.glassfish.jersey.server.ContainerRequest) line: 696
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response) line: 387
org.glassfish.grizzly.http.server.HttpHandler$1.run() line: 190
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).doWork() line: 535
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).run() line: 515
java.lang.Thread.runWith(java.lang.Object, java.lang.Runnable) line: 1596
java.lang.Thread.run() line: 1583
usingWriter == true -> Error
Thread [grizzly-http-server-0] (Suspended (breakpoint at line 577 in org.glassfish.grizzly.http.server.Response))
org.glassfish.grizzly.http.server.Response.getNIOOutputStream() line: 577
org.glassfish.grizzly.http.server.Response.getOutputStream() line: 602
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer$ResponseWriter.writeResponseStatusAndHeaders(long, org.glassfish.jersey.server.ContainerResponse) line: 250
org.glassfish.jersey.server.ServerRuntime$Responder$1.getOutputStream(int) line: 681
org.glassfish.jersey.message.internal.CommittingOutputStream.commitStream(int) line: 174
org.glassfish.jersey.message.internal.CommittingOutputStream.flushBuffer(boolean) line: 279
org.glassfish.jersey.message.internal.CommittingOutputStream.commit() line: 235
org.glassfish.jersey.message.internal.CommittingOutputStream.close() line: 250
org.glassfish.jersey.message.internal.OutboundMessageContext.close() line: 569
org.glassfish.jersey.server.ContainerResponse.close() line: 406
org.glassfish.jersey.server.ServerRuntime$Responder.writeResponse(org.glassfish.jersey.server.ContainerResponse) line: 763
org.glassfish.jersey.server.ServerRuntime$Responder.processResponse(org.glassfish.jersey.server.ContainerResponse, java.lang.Throwable) line: 398
org.glassfish.jersey.server.ServerRuntime$Responder.processResponseWithDefaultExceptionMapper(java.lang.Throwable, org.glassfish.jersey.server.ContainerRequest) line: 647
org.glassfish.jersey.server.ServerRuntime$Responder.process(java.lang.Throwable) line: 475
org.glassfish.jersey.server.ServerRuntime$1.run() line: 282
org.glassfish.jersey.internal.Errors$1.call() line: 248
org.glassfish.jersey.internal.Errors$1.call() line: 244
org.glassfish.jersey.internal.Errors.process(java.util.concurrent.Callable<T>, boolean) line: 292
org.glassfish.jersey.internal.Errors.process(org.glassfish.jersey.internal.util.Producer<T>, boolean) line: 274
org.glassfish.jersey.internal.Errors.process(java.lang.Runnable) line: 244
org.glassfish.jersey.inject.hk2.Hk2RequestScope(org.glassfish.jersey.process.internal.RequestScope).runInScope(org.glassfish.jersey.process.internal.RequestContext, java.lang.Runnable) line: 266
org.glassfish.jersey.server.ServerRuntime.process(org.glassfish.jersey.server.ContainerRequest) line: 253
org.glassfish.jersey.server.ApplicationHandler.handle(org.glassfish.jersey.server.ContainerRequest) line: 696
org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.service(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response) line: 387
org.glassfish.grizzly.http.server.HttpHandler$1.run() line: 190
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).doWork() line: 535
org.glassfish.grizzly.threadpool.FixedThreadPool$BasicWorker(org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker).run() line: 515
java.lang.Thread.runWith(java.lang.Object, java.lang.Runnable) line: 1596
java.lang.Thread.run() line: 1583
I was checking Grizzly to fix it, and although it is possible, it seems Jersey is not doing some invocations in the order it should be done.
Issue is gone when jersey.config.server.response.setStatusOverSendError = true that is evaluated here in the case of Grizzly.
The reason is that, when it is false (this is the default), it writes the response immediately. Later within the same request, it closes it, and this also writes a response. I guess some web containers will be fine, but in the case of Grizzly it writes an error.
Closing the container should be aware about that config value, to avoid to write again.