Skip to content

Commit 48c43f2

Browse files
committed
Fix for the InputStream caching cases for Servlets
Signed-off-by: Maxim Nesen <[email protected]>
1 parent ddee608 commit 48c43f2

File tree

4 files changed

+266
-7
lines changed

4 files changed

+266
-7
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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<!--
33
4-
Copyright (c) 2017, 2024 Oracle and/or its affiliates. All rights reserved.
4+
Copyright (c) 2017, 2025 Oracle and/or its affiliates. All rights reserved.
55
66
This program and the accompanying materials are made available under the
77
terms of the Eclipse Public License v. 2.0, which is available at
@@ -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)