diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java index f3fd445130..d63e0a90e7 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundJaxrsResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -26,6 +26,8 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -200,7 +202,12 @@ public boolean bufferEntity() throws ProcessingException { @Override public void close() throws ProcessingException { closed = true; - context.close(); + try { + context.close(); + } catch (Exception e) { + // Just log the exception + Logger.getLogger(OutboundJaxrsResponse.class.getName()).log(Level.FINE, e.getMessage(), e); + } if (buffered) { // release buffer context.setEntity(null); diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java index 3265a96340..8e10cdffba 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; @@ -27,8 +28,6 @@ import java.util.Locale; import java.util.Set; import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Collectors; import javax.ws.rs.core.Configuration; @@ -557,6 +556,7 @@ public boolean isCommitted() { /** * Closes the context. Flushes and closes the entity stream. + * @throws UncheckedIOException if IO errors */ public void close() { if (hasEntity()) { @@ -567,11 +567,7 @@ public void close() { } es.close(); } catch (IOException e) { - // Happens when the client closed connection before receiving the full response. - // This is OK and not interesting in the vast majority of the cases - // hence the log level set to FINE to make sure it does not flood the log unnecessarily - // (especially for clients disconnecting from SSE listening, which is very common). - Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); + throw new UncheckedIOException(e); } finally { // In case some of the output stream wrapper does not delegate close() call we // close the root stream manually to make sure it commits the data. @@ -579,8 +575,7 @@ public void close() { try { committingOutputStream.close(); } catch (IOException e) { - // Just log the exception - Logger.getLogger(OutboundMessageContext.class.getName()).log(Level.FINE, e.getMessage(), e); + throw new UncheckedIOException(e); } } } diff --git a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java index cf6799ed25..99731d07c2 100644 --- a/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java +++ b/core-server/src/main/java/org/glassfish/jersey/server/ContainerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -26,6 +26,8 @@ import java.net.URI; import java.util.Date; import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.Map; import java.util.Set; @@ -400,9 +402,14 @@ public boolean isCommitted() { public void close() { if (!closed) { closed = true; - messageContext.close(); - requestContext.getResponseWriter().commit(); - requestContext.setWorkers(null); + try { + messageContext.close(); + requestContext.setWorkers(null); + requestContext.getResponseWriter().commit(); + } catch (Exception e) { + Logger.getLogger(ContainerResponse.class.getName()).log(Level.FINE, e.getMessage(), e); + requestContext.getResponseWriter().failure(e); + } } } diff --git a/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java new file mode 100644 index 0000000000..cc1c873b13 --- /dev/null +++ b/tests/e2e-server/src/test/java/org/glassfish/jersey/tests/e2e/server/Issue5783Test.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.tests.e2e.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.spi.ContainerResponseWriter; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + +public class Issue5783Test extends JerseyTest { + + private static final String ERROR = "Intentional issue5783 exception"; + private static volatile String exceptionMessage; + + @Override + protected Application configure() { + return new ResourceConfig(Resource.class, ResponseFilter.class); + } + + @Test + public void closeException() throws InterruptedException { + target("/test").request().get(); + assertEquals(ERROR, exceptionMessage); + } + + @Path("/test") + public static class Resource { + + @GET + public Response closeException(@Context ContainerRequest request) { + // Save the exception when response.getRequestContext().getResponseWriter().failure(e) + ContainerResponseWriter writer = request.getResponseWriter(); + ContainerResponseWriter proxy = (ContainerResponseWriter) Proxy.newProxyInstance( + ContainerResponseWriter.class.getClassLoader(), + new Class[]{ContainerResponseWriter.class}, new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("failure".equals(method.getName())) { + exceptionMessage = ((Throwable) args[0]).getCause().getMessage(); + } + return method.invoke(writer, args); + } + }); + request.setWriter(proxy); + return Response.ok().build(); + } + } + + @Provider + public static class ResponseFilter implements ContainerResponseFilter { + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + // Hack it to make ContainerResponse#close throws one exception + responseContext.setEntity("something"); + responseContext.setEntityStream(new ByteArrayOutputStream() { + @Override + public void close() throws IOException { + throw new IOException(ERROR); + } + }); + } + } +}