jersey icon indicating copy to clipboard operation
jersey copied to clipboard

Jersey 3.1.10: ContainerResponse.close() triggers responseWriter.failure(e) → sendError(500, "Request failed."), losing exception message (regression vs 2.35)

Open LordOfTheHouse opened this issue 2 months ago • 4 comments

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?

LordOfTheHouse avatar Oct 13 '25 15:10 LordOfTheHouse

Hi @LordOfTheHouse do you have a small reproducer?. I will find what commit made that issue and I will investigate from there.

jbescos avatar Oct 16 '25 09:10 jbescos

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.

senivam avatar Oct 16 '25 09:10 senivam

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.

jbescos avatar Oct 20 '25 07:10 jbescos

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.

jbescos avatar Oct 20 '25 07:10 jbescos