Skip to content

Commit 61e31e9

Browse files
committed
Fix for the InputStream caching cases for Servlets
Signed-off-by: Maxim Nesen <maxim.nesen@oracle.com>
1 parent ddee608 commit 61e31e9

File tree

4 files changed

+265
-6
lines changed

4 files changed

+265
-6
lines changed

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

Lines changed: 11 additions & 5 deletions
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
@@ -38,6 +38,7 @@
3838
import java.util.logging.Logger;
3939
import java.util.stream.Collectors;
4040

41+
import javax.servlet.ServletInputStream;
4142
import javax.ws.rs.RuntimeType;
4243
import javax.ws.rs.core.Form;
4344
import javax.ws.rs.core.GenericType;
@@ -425,13 +426,18 @@ private void initContainerRequest(
425426

426427
try {
427428
requestContext.setEntityStream(new InputStreamWrapper() {
429+
430+
private ServletInputStream wrappedStream;
428431
@Override
429432
protected InputStream getWrapped() {
430-
try {
431-
return servletRequest.getInputStream();
432-
} catch (IOException e) {
433-
throw new UncheckedIOException(e);
433+
if (wrappedStream == null) {
434+
try {
435+
wrappedStream = servletRequest.getInputStream();
436+
} catch (IOException e) {
437+
throw new UncheckedIOException(e);
438+
}
434439
}
440+
return wrappedStream;
435441
}
436442
});
437443
} catch (UncheckedIOException e) {

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2199,7 +2199,7 @@
21992199
<!--required for spring (ext) modules integration -->
22002200
<aspectj.weaver.version>1.9.22.1</aspectj.weaver.version>
22012201
<!-- <bnd.plugin.version>2.3.6</bnd.plugin.version>-->
2202-
<commons.io.version>2.16.1</commons.io.version>
2202+
<commons.io.version>2.19.0</commons.io.version>
22032203
<!-- <commons-lang3.version>3.3.2</commons-lang3.version>-->
22042204
<commons.logging.version>1.3.3</commons.logging.version>
22052205
<fasterxml.classmate.version>1.7.0</fasterxml.classmate.version>

tests/e2e-server/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,19 @@
205205
<scope>test</scope>
206206
</dependency>
207207

208+
<dependency>
209+
<groupId>org.eclipse.jetty</groupId>
210+
<artifactId>jetty-servlet</artifactId>
211+
<version>${jetty.version}</version>
212+
<scope>test</scope>
213+
</dependency>
214+
215+
<dependency>
216+
<groupId>commons-io</groupId>
217+
<artifactId>commons-io</artifactId>
218+
<version>${commons.io.version}</version>
219+
</dependency>
220+
208221
<dependency>
209222
<groupId>org.hamcrest</groupId>
210223
<artifactId>hamcrest</artifactId>
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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.tests.e2e.server;
18+
19+
import org.apache.commons.io.IOUtils;
20+
import org.eclipse.jetty.server.Server;
21+
import org.eclipse.jetty.server.ServerConnector;
22+
import org.eclipse.jetty.servlet.ServletContextHandler;
23+
import org.eclipse.jetty.servlet.ServletHolder;
24+
import org.glassfish.jersey.client.ClientConfig;
25+
import org.glassfish.jersey.internal.InternalProperties;
26+
import org.glassfish.jersey.jackson.JacksonFeature;
27+
import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
28+
import org.glassfish.jersey.server.ResourceConfig;
29+
import org.glassfish.jersey.servlet.ServletContainer;
30+
import org.glassfish.jersey.test.JerseyTest;
31+
import org.glassfish.jersey.test.spi.TestContainer;
32+
import org.glassfish.jersey.test.spi.TestContainerException;
33+
import org.glassfish.jersey.test.spi.TestContainerFactory;
34+
import org.junit.jupiter.api.Test;
35+
36+
import javax.servlet.DispatcherType;
37+
import javax.servlet.Filter;
38+
import javax.servlet.FilterChain;
39+
import javax.servlet.ReadListener;
40+
import javax.servlet.ServletException;
41+
import javax.servlet.ServletInputStream;
42+
import javax.servlet.ServletRequest;
43+
import javax.servlet.ServletResponse;
44+
import javax.servlet.http.HttpServletRequest;
45+
import javax.servlet.http.HttpServletRequestWrapper;
46+
import javax.ws.rs.Consumes;
47+
import javax.ws.rs.POST;
48+
import javax.ws.rs.Path;
49+
import javax.ws.rs.Produces;
50+
import javax.ws.rs.client.Entity;
51+
import javax.ws.rs.client.Invocation;
52+
import javax.ws.rs.core.Application;
53+
import javax.ws.rs.core.MediaType;
54+
import javax.ws.rs.core.Response;
55+
import java.io.BufferedReader;
56+
import java.io.ByteArrayInputStream;
57+
import java.io.IOException;
58+
import java.io.InputStreamReader;
59+
import java.net.URI;
60+
import java.util.Collections;
61+
import java.util.EnumSet;
62+
63+
import static org.junit.jupiter.api.Assertions.assertEquals;
64+
65+
public class SimilarInputStreamTest extends JerseyTest {
66+
67+
@Override
68+
protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
69+
return (baseUri, deploymentContext) -> {
70+
final Server server = JettyHttpContainerFactory.createServer(baseUri, false);
71+
final ServerConnector connector = new ServerConnector(server);
72+
connector.setPort(9001);
73+
server.addConnector(connector);
74+
75+
final ServletContainer jerseyServletContainer = new ServletContainer(deploymentContext.getResourceConfig());
76+
final ServletHolder jettyServletHolder = new ServletHolder(jerseyServletContainer);
77+
78+
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
79+
context.setContextPath("/");
80+
81+
// filter which will change the http servlet request to have a reply-able input stream
82+
context.addFilter(FilterSettingMultiReadRequest.class,
83+
"/*", EnumSet.allOf(DispatcherType.class));
84+
context.addServlet(jettyServletHolder, "/api/*");
85+
86+
server.setHandler(context);
87+
return new TestContainer() {
88+
@Override
89+
public ClientConfig getClientConfig() {
90+
return new ClientConfig();
91+
}
92+
93+
@Override
94+
public URI getBaseUri() {
95+
return baseUri;
96+
}
97+
98+
@Override
99+
public void start() {
100+
try {
101+
server.start();
102+
} catch (Exception e) {
103+
throw new RuntimeException(e);
104+
}
105+
}
106+
107+
@Override
108+
public void stop() {
109+
try {
110+
server.stop();
111+
} catch (Exception e) {
112+
throw new RuntimeException(e);
113+
}
114+
}
115+
};
116+
};
117+
}
118+
119+
@Override
120+
protected Application configure() {
121+
ResourceConfig resourceConfig = new ResourceConfig(TestResource.class);
122+
// force jersey to use jackson for deserialization
123+
resourceConfig.addProperties(
124+
Collections.singletonMap(InternalProperties.JSON_FEATURE, JacksonFeature.class.getSimpleName()));
125+
return resourceConfig;
126+
}
127+
128+
@Test
129+
public void readJsonWithReplayableInputStreamFailsTest() {
130+
final Invocation.Builder requestBuilder = target("/api/v1/echo").request();
131+
final MyDto myDto = new MyDto();
132+
myDto.setMyField("Something");
133+
try (Response response = requestBuilder.post(Entity.entity(myDto, MediaType.APPLICATION_JSON))) {
134+
// fixed from failure with a 400 as jackson can never finish reading the input stream
135+
assertEquals(200, response.getStatus());
136+
final MyDto resultDto = response.readEntity(MyDto.class);
137+
assertEquals("Something", resultDto.getMyField()); //verify we still get Something
138+
}
139+
}
140+
141+
@Path("/v1")
142+
public static class TestResource {
143+
144+
@POST
145+
@Path("/echo")
146+
@Produces(MediaType.APPLICATION_JSON)
147+
@Consumes(MediaType.APPLICATION_JSON)
148+
public MyDto echo(MyDto input) {
149+
return input;
150+
}
151+
}
152+
153+
public static class MyDto {
154+
private String myField;
155+
156+
public String getMyField() {
157+
return myField;
158+
}
159+
160+
public void setMyField(String myField) {
161+
this.myField = myField;
162+
}
163+
164+
@Override
165+
public String toString() {
166+
return "MyDto{"
167+
+ "myField='" + myField + '\''
168+
+ '}';
169+
}
170+
}
171+
172+
173+
public static class FilterSettingMultiReadRequest implements Filter {
174+
@Override
175+
public void doFilter(ServletRequest request, ServletResponse response,
176+
FilterChain chain) throws IOException, ServletException {
177+
/* wrap the request in order to read the inputstream multiple times */
178+
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
179+
chain.doFilter(multiReadRequest, response);
180+
}
181+
}
182+
183+
static class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
184+
private byte[] cachedBytes;
185+
186+
public MultiReadHttpServletRequest(HttpServletRequest request) {
187+
super(request);
188+
}
189+
190+
@Override
191+
public ServletInputStream getInputStream() throws IOException {
192+
if (cachedBytes == null) {
193+
cacheInputStream();
194+
}
195+
196+
return new CachedServletInputStream(cachedBytes);
197+
}
198+
199+
@Override
200+
public BufferedReader getReader() throws IOException {
201+
return new BufferedReader(new InputStreamReader(getInputStream()));
202+
}
203+
204+
private void cacheInputStream() throws IOException {
205+
// Cache the inputstream in order to read it multiple times.
206+
cachedBytes = IOUtils.toByteArray(super.getInputStream());
207+
}
208+
209+
210+
/* An input stream which reads the cached request body */
211+
private class CachedServletInputStream extends ServletInputStream {
212+
213+
private final ByteArrayInputStream buffer;
214+
215+
public CachedServletInputStream(byte[] contents) {
216+
this.buffer = new ByteArrayInputStream(contents);
217+
}
218+
219+
@Override
220+
public int read() {
221+
return buffer.read();
222+
}
223+
224+
@Override
225+
public boolean isFinished() {
226+
return buffer.available() == 0;
227+
}
228+
229+
@Override
230+
public boolean isReady() {
231+
return true;
232+
}
233+
234+
@Override
235+
public void setReadListener(ReadListener listener) {
236+
throw new RuntimeException("Not implemented");
237+
}
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)