Skip to content

Commit 55a981b

Browse files
committed
Prevent NPE in the servlet container by writing the entity when the servlet container is shutdown
Signed-off-by: jansupol <jan.supol@oracle.com>
1 parent bbaa587 commit 55a981b

File tree

4 files changed

+156
-3
lines changed

4 files changed

+156
-3
lines changed

containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/internal/ResponseWriter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2024 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -116,6 +116,10 @@ public void setSuspendTimeout(final long timeOut, final TimeUnit timeUnit) throw
116116
@Override
117117
public OutputStream writeResponseStatusAndHeaders(final long contentLength, final ContainerResponse responseContext)
118118
throws ContainerException {
119+
if (asyncExt.isCompleted()) {
120+
return null;
121+
}
122+
119123
this.responseContext.complete(responseContext);
120124

121125
// first set the content length, so that if headers have an explicit value, it takes precedence over this one

containers/jersey-servlet-core/src/main/java/org/glassfish/jersey/servlet/spi/AsyncContextDelegate.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -42,4 +42,17 @@ public interface AsyncContextDelegate {
4242
* Invoked upon a response writing completion when the response write is either committed or canceled.
4343
*/
4444
public void complete();
45+
46+
/**
47+
* <p>
48+
* Return {@code true} when the AsyncContext is completed, such as when {@link #complete()} has been called.
49+
* </p>
50+
* <p>
51+
* For compatibility, the default is {@code false}.
52+
* </p>
53+
* @return {@code true} when the AsyncContext is completed.
54+
*/
55+
public default boolean isCompleted() {
56+
return false;
57+
}
4558
}

containers/jersey-servlet/src/main/java/org/glassfish/jersey/servlet/async/AsyncContextDelegateProviderImpl.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2025 Oracle and/or its affiliates. All rights reserved.
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Eclipse Public License v. 2.0, which is available at
@@ -16,12 +16,15 @@
1616

1717
package org.glassfish.jersey.servlet.async;
1818

19+
import java.io.IOException;
1920
import java.util.concurrent.atomic.AtomicBoolean;
2021
import java.util.concurrent.atomic.AtomicReference;
2122
import java.util.logging.Level;
2223
import java.util.logging.Logger;
2324

2425
import javax.servlet.AsyncContext;
26+
import javax.servlet.AsyncEvent;
27+
import javax.servlet.AsyncListener;
2528
import javax.servlet.http.HttpServletRequest;
2629
import javax.servlet.http.HttpServletResponse;
2730

@@ -70,10 +73,35 @@ private ExtensionImpl(final HttpServletRequest request, final HttpServletRespons
7073
public void suspend() throws IllegalStateException {
7174
// Suspend only if not completed and not suspended before.
7275
if (!completed.get() && asyncContextRef.get() == null) {
76+
AsyncContext asyncContext = getAsyncContext();
77+
asyncContext.addListener(new CompletedAsyncContextListener());
7378
asyncContextRef.set(getAsyncContext());
7479
}
7580
}
7681

82+
private class CompletedAsyncContextListener implements AsyncListener {
83+
84+
@Override
85+
public void onComplete(AsyncEvent event) throws IOException {
86+
complete();
87+
}
88+
89+
@Override
90+
public void onTimeout(AsyncEvent event) throws IOException {
91+
92+
}
93+
94+
@Override
95+
public void onError(AsyncEvent event) throws IOException {
96+
complete();
97+
}
98+
99+
@Override
100+
public void onStartAsync(AsyncEvent event) throws IOException {
101+
102+
}
103+
}
104+
77105
private AsyncContext getAsyncContext() {
78106
final AsyncContext asyncContext;
79107
if (request.isAsyncStarted()) {
@@ -102,5 +130,10 @@ public void complete() {
102130
asyncContext.complete();
103131
}
104132
}
133+
134+
@Override
135+
public boolean isCompleted() {
136+
return completed.get();
137+
}
105138
}
106139
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0, which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the
10+
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11+
* version 2 with the GNU Classpath Exception, which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
*/
16+
17+
package org.glassfish.jersey.servlet.async;
18+
19+
import org.glassfish.jersey.server.spi.ContainerResponseWriter;
20+
import org.glassfish.jersey.servlet.internal.ResponseWriter;
21+
import org.junit.jupiter.api.Test;
22+
23+
import javax.servlet.AsyncContext;
24+
import javax.servlet.AsyncListener;
25+
import javax.servlet.http.HttpServletRequest;
26+
import javax.servlet.http.HttpServletResponse;
27+
import java.io.IOException;
28+
import java.lang.reflect.InvocationHandler;
29+
import java.lang.reflect.Method;
30+
import java.lang.reflect.Proxy;
31+
import java.util.ArrayList;
32+
import java.util.List;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.TimeUnit;
35+
36+
public class AsyncContextClosedTest {
37+
@Test
38+
public void testClosedAsyncContext() {
39+
List<AsyncListener> asyncListeners = new ArrayList<>(1);
40+
AsyncContext async = (AsyncContext) Proxy.newProxyInstance(getClass().getClassLoader(),
41+
new Class[]{AsyncContext.class}, new InvocationHandler() {
42+
@Override
43+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
44+
switch (method.getName()) {
45+
case "addListener":
46+
asyncListeners.add((AsyncListener) args[0]);
47+
case "complete":
48+
asyncListeners.forEach((asyncListener -> {
49+
try {
50+
asyncListener.onComplete(null);
51+
} catch (IOException e) {
52+
throw new RuntimeException(e);
53+
}
54+
}));
55+
}
56+
return null;
57+
}
58+
});
59+
60+
HttpServletRequest request = (HttpServletRequest) Proxy.newProxyInstance(getClass().getClassLoader(),
61+
new Class[]{HttpServletRequest.class}, new InvocationHandler() {
62+
boolean asyncStarted = false;
63+
64+
@Override
65+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
66+
switch (method.getName()) {
67+
case "isAsyncStarted":
68+
return asyncStarted;
69+
case "startAsync":
70+
asyncStarted = true;
71+
return async;
72+
case "getAsyncContext":
73+
return async;
74+
75+
}
76+
return null;
77+
}
78+
});
79+
80+
HttpServletResponse response = (HttpServletResponse) Proxy.newProxyInstance(getClass().getClassLoader(),
81+
new Class[]{HttpServletResponse.class}, new InvocationHandler() {
82+
@Override
83+
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
84+
return null;
85+
}
86+
});
87+
88+
// Create writer
89+
ResponseWriter writer = new ResponseWriter(false, false, response,
90+
new AsyncContextDelegateProviderImpl().createDelegate(request, response),
91+
Executors.newSingleThreadScheduledExecutor());
92+
writer.suspend(10, TimeUnit.SECONDS, new ContainerResponseWriter.TimeoutHandler() {
93+
@Override
94+
public void onTimeout(ContainerResponseWriter responseWriter) {
95+
throw new IllegalStateException();
96+
}
97+
});
98+
// Simulate completion by the Servlet Container;
99+
request.getAsyncContext().complete();
100+
// Check write is ignored
101+
writer.writeResponseStatusAndHeaders(10, null);
102+
}
103+
}

0 commit comments

Comments
 (0)