2020
2121import java .io .ByteArrayOutputStream ;
2222import java .io .IOException ;
23+ import java .io .InputStream ;
2324import java .net .URI ;
25+ import java .nio .ByteBuffer ;
26+ import java .nio .file .Files ;
27+ import java .nio .file .Path ;
28+ import java .util .concurrent .CountDownLatch ;
29+ import java .util .concurrent .TimeUnit ;
30+ import java .util .concurrent .atomic .AtomicLong ;
31+ import java .util .concurrent .atomic .AtomicReference ;
32+ import java .util .zip .GZIPInputStream ;
2433import java .util .zip .GZIPOutputStream ;
2534import javax .servlet .http .HttpServlet ;
2635import javax .servlet .http .HttpServletRequest ;
2736import javax .servlet .http .HttpServletResponse ;
2837
2938import org .eclipse .jetty .client .HttpClient ;
3039import org .eclipse .jetty .client .api .ContentResponse ;
40+ import org .eclipse .jetty .client .api .Response ;
41+ import org .eclipse .jetty .client .api .Result ;
3142import org .eclipse .jetty .client .util .BytesContentProvider ;
43+ import org .eclipse .jetty .client .util .DeferredContentProvider ;
3244import org .eclipse .jetty .http .HttpHeader ;
3345import org .eclipse .jetty .http .HttpMethod ;
46+ import org .eclipse .jetty .server .HttpChannel ;
47+ import org .eclipse .jetty .server .HttpConnection ;
48+ import org .eclipse .jetty .server .HttpInput ;
49+ import org .eclipse .jetty .server .Request ;
3450import org .eclipse .jetty .server .Server ;
3551import org .eclipse .jetty .server .ServerConnector ;
3652import org .eclipse .jetty .server .handler .gzip .GzipHandler ;
3753import org .eclipse .jetty .servlet .ServletContextHandler ;
54+ import org .eclipse .jetty .toolchain .test .MavenTestingUtils ;
3855import org .eclipse .jetty .util .IO ;
3956import org .eclipse .jetty .util .component .LifeCycle ;
4057import org .junit .jupiter .api .AfterEach ;
4158import org .junit .jupiter .api .BeforeEach ;
4259import org .junit .jupiter .api .Test ;
60+ import org .junit .jupiter .params .ParameterizedTest ;
61+ import org .junit .jupiter .params .provider .ValueSource ;
4362
4463import static java .nio .charset .StandardCharsets .UTF_8 ;
4564import static org .hamcrest .MatcherAssert .assertThat ;
4665import static org .hamcrest .Matchers .containsString ;
66+ import static org .hamcrest .Matchers .greaterThan ;
67+ import static org .hamcrest .Matchers .greaterThanOrEqualTo ;
68+ import static org .hamcrest .Matchers .is ;
69+ import static org .hamcrest .Matchers .lessThanOrEqualTo ;
4770import static org .junit .jupiter .api .Assertions .assertEquals ;
71+ import static org .junit .jupiter .api .Assertions .assertNotEquals ;
72+ import static org .junit .jupiter .api .Assertions .assertTrue ;
4873
4974public class GzipWithSendErrorTest
5075{
5176 private Server server ;
5277 private HttpClient client ;
78+ private ServerConnector connector ;
79+
80+ private static void onComplete (Result result )
81+ {
82+ }
5383
5484 @ BeforeEach
5585 public void setup () throws Exception
5686 {
5787 server = new Server ();
5888
59- ServerConnector connector = new ServerConnector (server );
89+ connector = new ServerConnector (server );
6090 connector .setPort (0 );
6191 server .addConnector (connector );
6292
@@ -100,7 +130,6 @@ public void testGzipNormalErrorNormal() throws Exception
100130 response = client .newRequest (serverURI .resolve ("/submit" ))
101131 .method (HttpMethod .POST )
102132 .header (HttpHeader .CONTENT_ENCODING , "gzip" )
103- .header (HttpHeader .ACCEPT_ENCODING , "gzip" )
104133 .content (new BytesContentProvider ("text/plain" , compressed ("normal-A" )))
105134 .send ();
106135
@@ -110,7 +139,6 @@ public void testGzipNormalErrorNormal() throws Exception
110139 response = client .newRequest (serverURI .resolve ("/fail" ))
111140 .method (HttpMethod .POST )
112141 .header (HttpHeader .CONTENT_ENCODING , "gzip" )
113- .header (HttpHeader .ACCEPT_ENCODING , "gzip" )
114142 .content (new BytesContentProvider ("text/plain" , compressed ("normal-B" )))
115143 .send ();
116144
@@ -120,7 +148,6 @@ public void testGzipNormalErrorNormal() throws Exception
120148 response = client .newRequest (serverURI .resolve ("/submit" ))
121149 .method (HttpMethod .POST )
122150 .header (HttpHeader .CONTENT_ENCODING , "gzip" )
123- .header (HttpHeader .ACCEPT_ENCODING , "gzip" )
124151 .content (new BytesContentProvider ("text/plain" , compressed ("normal-C" )))
125152 .send ();
126153
@@ -139,15 +166,219 @@ private byte[] compressed(String content) throws IOException
139166 }
140167 }
141168
169+ /**
170+ * Make request with compressed content.
171+ * <p>
172+ * Request contains (roughly) 1 MB of request network data.
173+ * Which unpacks to 1 GB of zeros.
174+ * </p>
175+ * <p>
176+ * This test is to ensure that consumeAll only reads the network data,
177+ * and doesn't process it through the interceptors.
178+ * </p>
179+ */
180+ @ Test
181+ public void testGzipConsumeAllContentLengthBlocking () throws Exception
182+ {
183+ URI serverURI = server .getURI ();
184+
185+ CountDownLatch serverRequestCompleteLatch = new CountDownLatch (1 );
186+ // count of bytes against network read
187+ AtomicLong inputBytesIn = new AtomicLong (0L );
188+ AtomicLong inputContentReceived = new AtomicLong (0L );
189+ // count of bytes against API read
190+ AtomicLong inputContentConsumed = new AtomicLong (0L );
191+
192+ connector .addBean (new HttpChannel .Listener ()
193+ {
194+ @ Override
195+ public void onComplete (Request request )
196+ {
197+ HttpConnection connection = (HttpConnection )request .getHttpChannel ().getConnection ();
198+ HttpInput httpInput = request .getHttpInput ();
199+ inputContentConsumed .set (httpInput .getContentConsumed ());
200+ inputContentReceived .set (httpInput .getContentReceived ());
201+ inputBytesIn .set (connection .getBytesIn ());
202+ serverRequestCompleteLatch .countDown ();
203+ }
204+ });
205+
206+ // This is a doubly-compressed (with gzip) test resource.
207+ // There's no point putting into SCM the full 1MB file, when the
208+ // 3KB version is adequate.
209+ Path zerosCompressed = MavenTestingUtils .getTestResourcePathFile ("zeros.gz.gz" );
210+ byte [] compressedRequest ;
211+ try (InputStream in = Files .newInputStream (zerosCompressed );
212+ GZIPInputStream gzipIn = new GZIPInputStream (in );
213+ ByteArrayOutputStream out = new ByteArrayOutputStream ())
214+ {
215+ IO .copy (gzipIn , out );
216+ compressedRequest = out .toByteArray ();
217+ }
218+
219+ int sizeActuallySent = compressedRequest .length / 2 ;
220+ ByteBuffer start = ByteBuffer .wrap (compressedRequest , 0 , sizeActuallySent );
221+ DeferredContentProvider contentProvider = new DeferredContentProvider (start )
222+ {
223+ @ Override
224+ public long getLength ()
225+ {
226+ return compressedRequest .length ;
227+ }
228+ };
229+ AtomicReference <Response > clientResponseRef = new AtomicReference <>();
230+ CountDownLatch clientResponseSuccessLatch = new CountDownLatch (1 );
231+ CountDownLatch clientResultComplete = new CountDownLatch (1 );
232+
233+ client .newRequest (serverURI .resolve ("/fail" ))
234+ .method (HttpMethod .POST )
235+ .header (HttpHeader .CONTENT_TYPE , "application/octet-stream" )
236+ .header (HttpHeader .CONTENT_ENCODING , "gzip" )
237+ .content (contentProvider )
238+ .onResponseSuccess ((response ) ->
239+ {
240+ clientResponseRef .set (response );
241+ clientResponseSuccessLatch .countDown ();
242+ })
243+ .send ((result ) -> clientResultComplete .countDown ());
244+
245+ assertTrue (clientResponseSuccessLatch .await (5 , TimeUnit .SECONDS ), "Result not received" );
246+ Response response = clientResponseRef .get ();
247+ assertEquals (400 , response .getStatus (), "Response status on /fail" );
248+
249+ assertEquals ("close" , response .getHeaders ().get (HttpHeader .CONNECTION ), "Response Connection header" );
250+
251+ // Await for server side to complete the request
252+ assertTrue (serverRequestCompleteLatch .await (5 , TimeUnit .SECONDS ), "Request complete never occurred?" );
253+
254+ // System.out.printf("Input Content Consumed: %,d%n", inputContentConsumed.get());
255+ // System.out.printf("Input Content Received: %,d%n", inputContentReceived.get());
256+ // System.out.printf("Input BytesIn Count: %,d%n", inputBytesIn.get());
257+
258+ // Servlet didn't read body content
259+ assertThat ("Request Input Content Consumed not have been used" , inputContentConsumed .get (), is (0L ));
260+ // Network reads
261+ assertThat ("Request Input Content Received should have seen content" , inputContentReceived .get (), greaterThan (0L ));
262+ assertThat ("Request Input Content Received less then initial buffer" , inputContentReceived .get (), lessThanOrEqualTo ((long )sizeActuallySent ));
263+ assertThat ("Request Connection BytesIn should have some minimal data" , inputBytesIn .get (), greaterThanOrEqualTo (1024L ));
264+ assertThat ("Request Connection BytesIn read should not have read all of the data" , inputBytesIn .get (), lessThanOrEqualTo ((long )sizeActuallySent ));
265+
266+ // Now provide rest
267+ contentProvider .offer (ByteBuffer .wrap (compressedRequest , sizeActuallySent , compressedRequest .length - sizeActuallySent ));
268+ contentProvider .close ();
269+
270+ assertTrue (clientResultComplete .await (5 , TimeUnit .SECONDS ));
271+ }
272+
273+ /**
274+ * Make request with compressed content.
275+ * <p>
276+ * Request contains (roughly) 1 MB of request network data.
277+ * Which unpacks to 1 GB of zeros.
278+ * </p>
279+ * <p>
280+ * This test is to ensure that consumeAll only reads the network data,
281+ * and doesn't process it through the interceptors.
282+ * </p>
283+ */
284+ @ ParameterizedTest
285+ @ ValueSource (booleans = {true , false })
286+ public void testGzipConsumeAllChunkedBlockingOnLastBuffer (boolean read ) throws Exception
287+ {
288+ URI serverURI = server .getURI ();
289+
290+ CountDownLatch serverRequestCompleteLatch = new CountDownLatch (1 );
291+ // count of bytes against network read
292+ AtomicLong inputBytesIn = new AtomicLong (0L );
293+ AtomicLong inputContentReceived = new AtomicLong (0L );
294+ // count of bytes against API read
295+ AtomicLong inputContentConsumed = new AtomicLong (0L );
296+
297+ connector .addBean (new HttpChannel .Listener ()
298+ {
299+ @ Override
300+ public void onComplete (Request request )
301+ {
302+ HttpConnection connection = (HttpConnection )request .getHttpChannel ().getConnection ();
303+ HttpInput httpInput = request .getHttpInput ();
304+ inputContentConsumed .set (httpInput .getContentConsumed ());
305+ inputContentReceived .set (httpInput .getContentReceived ());
306+ inputBytesIn .set (connection .getBytesIn ());
307+ serverRequestCompleteLatch .countDown ();
308+ }
309+ });
310+
311+ // This is a doubly-compressed (with gzip) test resource.
312+ // There's no point putting into SCM the full 1MB file, when the
313+ // 3KB version is adequate.
314+ Path zerosCompressed = MavenTestingUtils .getTestResourcePathFile ("zeros.gz.gz" );
315+ byte [] compressedRequest ;
316+ try (InputStream in = Files .newInputStream (zerosCompressed );
317+ GZIPInputStream gzipIn = new GZIPInputStream (in );
318+ ByteArrayOutputStream out = new ByteArrayOutputStream ())
319+ {
320+ IO .copy (gzipIn , out );
321+ compressedRequest = out .toByteArray ();
322+ }
323+
324+ int sizeActuallySent = compressedRequest .length / 2 ;
325+ ByteBuffer start = ByteBuffer .wrap (compressedRequest , 0 , sizeActuallySent );
326+ DeferredContentProvider contentProvider = new DeferredContentProvider (start );
327+ AtomicReference <Response > clientResponseRef = new AtomicReference <>();
328+ CountDownLatch clientResponseSuccessLatch = new CountDownLatch (1 );
329+ CountDownLatch clientResultComplete = new CountDownLatch (1 );
330+
331+ URI uri = serverURI .resolve ("/fail?read=" + read );
332+
333+ client .newRequest (uri )
334+ .method (HttpMethod .POST )
335+ .header (HttpHeader .CONTENT_TYPE , "application/octet-stream" )
336+ .header (HttpHeader .CONTENT_ENCODING , "gzip" )
337+ .content (contentProvider )
338+ .onResponseSuccess ((response ) ->
339+ {
340+ clientResponseRef .set (response );
341+ clientResponseSuccessLatch .countDown ();
342+ })
343+ .send ((result ) -> clientResultComplete .countDown ());
344+
345+ assertTrue (clientResponseSuccessLatch .await (5 , TimeUnit .SECONDS ), "Result not received" );
346+ Response response = clientResponseRef .get ();
347+ assertEquals (400 , response .getStatus (), "Response status on /fail" );
348+
349+ assertEquals ("close" , response .getHeaders ().get (HttpHeader .CONNECTION ), "Response Connection header" );
350+
351+ // Await for server side to complete the request
352+ assertTrue (serverRequestCompleteLatch .await (5 , TimeUnit .SECONDS ), "Request complete never occurred?" );
353+
354+ // System.out.printf("Input Content Consumed: %,d%n", inputContentConsumed.get());
355+ // System.out.printf("Input Content Received: %,d%n", inputContentReceived.get());
356+ // System.out.printf("Input BytesIn Count: %,d%n", inputBytesIn.get());
357+
358+ long readCount = read ? 1L : 0L ;
359+
360+ // Servlet read of body content
361+ assertThat ("Request Input Content Consumed not have been used" , inputContentConsumed .get (), is (readCount ));
362+ // Network reads
363+ assertThat ("Request Input Content Received should have seen content" , inputContentReceived .get (), greaterThan (0L ));
364+ assertThat ("Request Input Content Received less then initial buffer" , inputContentReceived .get (), lessThanOrEqualTo ((long )sizeActuallySent ));
365+ assertThat ("Request Connection BytesIn should have some minimal data" , inputBytesIn .get (), greaterThanOrEqualTo (1024L ));
366+ assertThat ("Request Connection BytesIn read should not have read all of the data" , inputBytesIn .get (), lessThanOrEqualTo ((long )sizeActuallySent ));
367+
368+ // Now provide rest
369+ contentProvider .offer (ByteBuffer .wrap (compressedRequest , sizeActuallySent , compressedRequest .length - sizeActuallySent ));
370+ contentProvider .close ();
371+
372+ assertTrue (clientResultComplete .await (5 , TimeUnit .SECONDS ));
373+ }
374+
142375 public static class PostServlet extends HttpServlet
143376 {
144377 @ Override
145378 protected void doPost (HttpServletRequest req , HttpServletResponse resp ) throws IOException
146379 {
147380 resp .setCharacterEncoding ("utf-8" );
148381 resp .setContentType ("text/plain" );
149- resp .setHeader ("X-Servlet" , req .getServletPath ());
150-
151382 String reqBody = IO .toString (req .getInputStream (), UTF_8 );
152383 resp .getWriter ().append (reqBody );
153384 }
@@ -158,7 +389,12 @@ public static class FailServlet extends HttpServlet
158389 @ Override
159390 protected void service (HttpServletRequest req , HttpServletResponse resp ) throws IOException
160391 {
161- resp .setHeader ("X-Servlet" , req .getServletPath ());
392+ boolean read = Boolean .parseBoolean (req .getParameter ("read" ));
393+ if (read )
394+ {
395+ int val = req .getInputStream ().read ();
396+ assertNotEquals (-1 , val );
397+ }
162398 // intentionally do not read request body here.
163399 resp .sendError (400 );
164400 }
0 commit comments