View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static org.junit.jupiter.api.Assertions.fail;
18  
19  import java.io.BufferedInputStream;
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.Writer;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.Method;
29  import java.net.URL;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  
35  import javax.servlet.Servlet;
36  import javax.servlet.ServletException;
37  import javax.servlet.http.HttpServlet;
38  import javax.servlet.http.HttpServletRequest;
39  import javax.servlet.http.HttpServletResponse;
40  
41  import org.apache.commons.io.IOUtils;
42  import org.apache.commons.lang3.StringUtils;
43  import org.apache.http.HttpEntity;
44  import org.apache.http.HttpResponse;
45  import org.apache.http.ProtocolVersion;
46  import org.apache.http.StatusLine;
47  import org.apache.http.client.methods.HttpUriRequest;
48  import org.apache.http.entity.StringEntity;
49  import org.apache.http.entity.mime.MultipartEntityBuilder;
50  import org.apache.http.impl.client.HttpClientBuilder;
51  import org.apache.http.message.BasicHttpResponse;
52  import org.apache.http.message.BasicStatusLine;
53  import org.htmlunit.html.HtmlPage;
54  import org.htmlunit.http.HttpStatus;
55  import org.htmlunit.util.KeyDataPair;
56  import org.htmlunit.util.MimeType;
57  import org.htmlunit.util.NameValuePair;
58  import org.htmlunit.util.ServletContentWrapper;
59  import org.junit.jupiter.api.Test;
60  
61  /**
62   * Tests methods in {@link HttpWebConnection}.
63   *
64   * @author David D. Kilzer
65   * @author Marc Guillemot
66   * @author Ahmed Ashour
67   * @author Ronald Brill
68   * @author Carsten Steul
69   */
70  public class HttpWebConnectionTest extends WebServerTestCase {
71  
72      /**
73       * Assert that the two byte arrays are equal.
74       * @param expected the expected value
75       * @param actual the actual value
76       */
77      public static void assertEquals(final byte[] expected, final byte[] actual) {
78          assertEquals(null, expected, actual, expected.length);
79      }
80  
81      /**
82       * Assert that the two byte arrays are equal.
83       * @param message the message to display on failure
84       * @param expected the expected value
85       * @param actual the actual value
86       * @param length How many characters at the beginning of each byte array will be compared
87       */
88      public static void assertEquals(
89              final String message, final byte[] expected, final byte[] actual, final int length) {
90          if (expected == null && actual == null) {
91              return;
92          }
93          if (expected == null || expected.length < length) {
94              fail(message);
95          }
96          if (actual == null || actual.length < length) {
97              fail(message);
98          }
99          for (int i = 0; i < length; i++) {
100             assertEquals(message, expected[i], actual[i]);
101         }
102     }
103 
104     /**
105      * Assert that the two input streams are the same.
106      * @param expected the expected value
107      * @param actual the actual value
108      * @throws IOException if an IO problem occurs during comparison
109      */
110     public static void assertEquals(final InputStream expected, final InputStream actual) throws IOException {
111         assertEquals(null, expected, actual);
112     }
113 
114     /**
115      * Assert that the two input streams are the same.
116      * @param message the message to display on failure
117      * @param expected the expected value
118      * @param actual the actual value
119      * @throws IOException if an IO problem occurs during comparison
120      */
121     public static void assertEquals(final String message, final InputStream expected,
122             final InputStream actual) throws IOException {
123 
124         if (expected == null && actual == null) {
125             return;
126         }
127 
128         if (expected == null || actual == null) {
129             try {
130                 fail(message);
131             }
132             finally {
133                 try {
134                     if (expected != null) {
135                         expected.close();
136                     }
137                 }
138                 finally {
139                     if (actual != null) {
140                         actual.close();
141                     }
142                 }
143             }
144         }
145 
146         try (InputStream expectedBuf = new BufferedInputStream(expected)) {
147             try (InputStream actualBuf = new BufferedInputStream(actual)) {
148 
149                 final byte[] expectedArray = new byte[2048];
150                 final byte[] actualArray = new byte[2048];
151 
152                 int expectedLength = expectedBuf.read(expectedArray);
153                 while (true) {
154 
155                     final int actualLength = actualBuf.read(actualArray);
156                     assertEquals(message, expectedLength, actualLength);
157 
158                     if (expectedLength == -1) {
159                         break;
160                     }
161 
162                     assertEquals(message, expectedArray, actualArray, expectedLength);
163                     expectedLength = expectedBuf.read(expectedArray);
164                 }
165             }
166         }
167     }
168 
169     /**
170      * Tests Jetty.
171      * @throws Exception on failure
172      */
173     @Test
174     public void jettyProofOfConcept() throws Exception {
175         startWebServer("./");
176 
177         final WebClient client = getWebClient();
178         final Page page = client.getPage(URL_FIRST + "src/test/resources/event_coordinates.html");
179         final WebConnection defaultConnection = client.getWebConnection();
180         assertTrue(
181                 "HttpWebConnection should be the default",
182                 HttpWebConnection.class.isInstance(defaultConnection));
183         assertTrue("Response should be valid HTML", HtmlPage.class.isInstance(page));
184     }
185 
186     /**
187      * Test for feature request 1438216: HttpWebConnection should allow extension to create the HttpClient.
188      * @throws Exception if the test fails
189      */
190     @Test
191     public void designedForExtension() throws Exception {
192         startWebServer("./");
193 
194         final WebClient webClient = getWebClient();
195         final boolean[] tabCalled = {false};
196         final WebConnection myWebConnection = new HttpWebConnection(webClient) {
197             @Override
198             protected HttpClientBuilder createHttpClientBuilder() {
199                 tabCalled[0] = true;
200 
201                 final HttpClientBuilder builder = HttpClientBuilder.create();
202                 builder.setConnectionManagerShared(true);
203                 return builder;
204             }
205         };
206 
207         webClient.setWebConnection(myWebConnection);
208         webClient.getPage(URL_FIRST + "LICENSE.txt");
209         assertTrue("createHttpClient has not been called", tabCalled[0]);
210     }
211 
212     /**
213      * Test that the HttpClient is reinitialised after being shutdown.
214      * @throws Exception if the test fails
215      */
216     @Test
217     public void reinitialiseAfterClosing() throws Exception {
218         startWebServer("./");
219 
220         final WebClient webClient = getWebClient();
221         try (HttpWebConnection webConnection = new HttpWebConnection(webClient)) {
222             webClient.setWebConnection(webConnection);
223             webClient.getPage(URL_FIRST + "LICENSE.txt");
224             webConnection.close();
225             webClient.getPage(URL_FIRST + "pom.xml");
226         }
227     }
228 
229     /**
230      * Test that the right file part is built for a file that doesn't exist.
231      * @throws Exception if the test fails
232      */
233     @Test
234     public void buildFilePart() throws Exception {
235         final String encoding = "ISO8859-1";
236         final KeyDataPair pair = new KeyDataPair("myFile", new File("this/doesnt_exist.txt"), "something",
237                 MimeType.TEXT_PLAIN, encoding);
238         final MultipartEntityBuilder builder = MultipartEntityBuilder.create().setLaxMode();
239         try (HttpWebConnection webConnection = new HttpWebConnection(getWebClient())) {
240             webConnection.buildFilePart(pair, builder);
241         }
242         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
243         builder.build().writeTo(baos);
244         final String part = baos.toString(encoding);
245 
246         final String expected = "--(.*)\r\n"
247                 + "Content-Disposition: form-data; name=\"myFile\"; filename=\"doesnt_exist.txt\"\r\n"
248                 + "Content-Type: text/plain\r\n"
249                 + "\r\n"
250                 + "\r\n"
251                 + "--\\1--\r\n";
252         assertTrue(part, part.matches(expected));
253     }
254 
255     /**
256      * @throws Exception on failure
257      */
258     @Test
259     public void unicode() throws Exception {
260         startWebServer("./");
261         final WebClient client = getWebClient();
262         client.getPage(URL_FIRST + "src/test/resources/event_coordinates.html?param=\u00F6");
263     }
264 
265     /**
266      * @throws Exception if an error occurs
267      */
268     @Test
269     public void emptyPut() throws Exception {
270         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
271         servlets.put("/test", EmptyPutServlet.class);
272         startWebServer("./", null, servlets);
273 
274         final String[] expectedAlerts = {"1"};
275         final WebClient client = getWebClient();
276         client.setAjaxController(new NicelyResynchronizingAjaxController());
277         final List<String> collectedAlerts = new ArrayList<>();
278         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
279 
280         assertEquals(0, client.getCookieManager().getCookies().size());
281         client.getPage(URL_FIRST + "test");
282         assertEquals(expectedAlerts, collectedAlerts);
283         assertEquals(1, client.getCookieManager().getCookies().size());
284     }
285 
286     /**
287      * Servlet for {@link #emptyPut()}.
288      */
289     public static class EmptyPutServlet extends ServletContentWrapper {
290         /** Constructor. */
291         public EmptyPutServlet() {
292             super(DOCTYPE_HTML
293                 + "<html>\n"
294                 + "<head>\n"
295                 + "  <script>\n"
296                 + "    function test() {\n"
297                 + "      var xhr = window.ActiveXObject?new ActiveXObject('Microsoft.XMLHTTP'):new XMLHttpRequest();\n"
298                 + "      xhr.open('PUT', '" + URL_FIRST + "test" + "', true);\n"
299                 + "      xhr.send();\n"
300                 + "      alert(1);\n"
301                 + "    }\n"
302                 + "  </script>\n"
303                 + "</head>\n"
304                 + "<body onload='test()'></body>\n"
305                 + "</html>");
306         }
307 
308         @Override
309         protected void doGet(final HttpServletRequest request,
310                 final HttpServletResponse response)
311             throws ServletException, IOException {
312             request.getSession().setAttribute("trigger", "session");
313             super.doGet(request, response);
314         }
315     }
316 
317     /**
318      * @throws Exception if the test fails
319      */
320     @Test
321     public void cookiesEnabledAfterDisable() throws Exception {
322         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
323         servlets.put("/test1", Cookie1Servlet.class);
324         servlets.put("/test2", Cookie2Servlet.class);
325         startWebServer("./", null, servlets);
326 
327         final WebClient client = getWebClient();
328 
329         client.getCookieManager().setCookiesEnabled(false);
330         HtmlPage page = client.getPage(URL_FIRST + "test1");
331         assertTrue(page.asNormalizedText().contains("No Cookies"));
332 
333         client.getCookieManager().setCookiesEnabled(true);
334         page = client.getPage(URL_FIRST + "test1");
335         assertTrue(page.asNormalizedText().contains("key1=value1"));
336     }
337 
338     /**
339      * Servlet for {@link #cookiesEnabledAfterDisable()}.
340      */
341     public static class Cookie1Servlet extends HttpServlet {
342 
343         /**
344          * {@inheritDoc}
345          */
346         @Override
347         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
348             response.addCookie(new javax.servlet.http.Cookie("key1", "value1"));
349             response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
350             final String location = request.getRequestURL().toString().replace("test1", "test2");
351             response.setHeader("Location", location);
352         }
353     }
354 
355     /**
356      * Servlet for {@link #cookiesEnabledAfterDisable()}.
357      */
358     public static class Cookie2Servlet extends HttpServlet {
359 
360         /**
361          * {@inheritDoc}
362          */
363         @Override
364         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
365             response.setContentType(MimeType.TEXT_HTML);
366             final Writer writer = response.getWriter();
367             if (request.getCookies() == null || request.getCookies().length == 0) {
368                 writer.write("No Cookies");
369             }
370             else {
371                 for (final javax.servlet.http.Cookie c : request.getCookies()) {
372                     writer.write(c.getName() + '=' + c.getValue());
373                 }
374             }
375         }
376     }
377 
378     /**
379      * @throws Exception if the test fails
380      */
381     @Test
382     public void remotePort() throws Exception {
383         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
384         servlets.put("/test", RemotePortServlet.class);
385         startWebServer("./", null, servlets);
386 
387         final WebClient client = getWebClient();
388 
389         String firstPort = null;
390 
391         for (int i = 0; i < 5; i++) {
392             final HtmlPage page = client.getPage(URL_FIRST + "test");
393             final String port = page.asNormalizedText();
394             if (firstPort == null) {
395                 firstPort = port;
396             }
397             assertEquals(firstPort, port);
398         }
399     }
400 
401     /**
402      * Servlet for {@link #remotePort()}.
403      */
404     public static class RemotePortServlet extends HttpServlet {
405 
406         /**
407          * {@inheritDoc}
408          */
409         @Override
410         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
411             response.setContentType(MimeType.TEXT_HTML);
412             response.getWriter().write(String.valueOf(request.getRemotePort()));
413         }
414     }
415 
416     /**
417      * @throws Exception if an error occurs
418      */
419     @Test
420     public void contentLengthSmallerThanContent() throws Exception {
421         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
422         servlets.put("/contentLengthSmallerThanContent", ContentLengthSmallerThanContentServlet.class);
423         startWebServer("./", null, servlets);
424 
425         final WebClient client = getWebClient();
426         final HtmlPage page = client.getPage(URL_FIRST + "contentLengthSmallerThanContent");
427         assertEquals("visible text", page.asNormalizedText());
428     }
429 
430     /**
431      * Servlet for {@link #contentLengthSmallerThanContent()}.
432      */
433     public static class ContentLengthSmallerThanContentServlet extends ServletContentWrapper {
434 
435         /** Constructor. */
436         public ContentLengthSmallerThanContentServlet() {
437             super(DOCTYPE_HTML
438                 + "<html>\n"
439                 + "<body>\n"
440                 + "  <p>visible text</p>\n"
441                 + "  <p>missing text</p>\n"
442                 + "</body>\n"
443                 + "</html>");
444         }
445 
446         @Override
447         protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
448             throws IOException, ServletException {
449             response.setContentLength(getContent().indexOf("<p>missing text</p>"));
450             super.doGet(request, response);
451         }
452     }
453 
454     /**
455      * @throws Exception if an error occurs
456      */
457     @Test
458     public void contentLengthSmallerThanContentLargeContent() throws Exception {
459         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
460         servlets.put("/contentLengthSmallerThanContent", ContentLengthSmallerThanContentLargeContentServlet.class);
461         startWebServer("./", null, servlets);
462 
463         final WebClient client = getWebClient();
464         final HtmlPage page = client.getPage(URL_FIRST + "contentLengthSmallerThanContent");
465         assertTrue(page.asNormalizedText(), page.asNormalizedText().endsWith("visible text"));
466     }
467 
468     /**
469      * Servlet for {@link #contentLengthSmallerThanContentLargeContent()}.
470      */
471     public static class ContentLengthSmallerThanContentLargeContentServlet extends ServletContentWrapper {
472 
473         /** Constructor. */
474         public ContentLengthSmallerThanContentLargeContentServlet() {
475             super(DOCTYPE_HTML
476                 + "<html>\n"
477                 + "<body>\n"
478                 + "  <p>"
479                 + StringUtils.repeat("HtmlUnit  ", 1024 * 1024)
480                 + "</p>\n"
481                 + "  <p>visible text</p>\n"
482                 + "  <p>missing text</p>\n"
483                 + "</body>\n"
484                 + "</html>");
485         }
486 
487         @Override
488         protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
489             throws IOException, ServletException {
490             response.setContentLength(getContent().indexOf("<p>missing text</p>"));
491             super.doGet(request, response);
492         }
493     }
494 
495     /**
496      * @throws Exception if an error occurs
497      */
498     @Test
499     public void contentLengthLargerThanContent() throws Exception {
500         final String response = "HTTP/1.1 200 OK\r\n"
501                 + "Content-Length: 2000\r\n"
502                 + "Content-Type: text/html\r\n"
503                 + "\r\n"
504                 + DOCTYPE_HTML + "<html><body><p>visible text</p></body></html>";
505 
506         try (PrimitiveWebServer primitiveWebServer = new PrimitiveWebServer(null, response, null)) {
507             final WebClient client = getWebClient();
508 
509             final HtmlPage page = client.getPage("http://localhost:" + primitiveWebServer.getPort());
510             assertEquals("visible text", page.asNormalizedText());
511         }
512     }
513 
514     /**
515      * Test for bug #1861.
516      *
517      * @throws Exception if the test fails
518      */
519     @Test
520     public void userAgent() throws Exception {
521         final WebClient webClient = getWebClient();
522         final HttpWebConnection connection = (HttpWebConnection) webClient.getWebConnection();
523         final HttpClientBuilder builder = connection.getHttpClientBuilder();
524         final String userAgent = get(builder, "userAgent");
525         assertEquals(webClient.getBrowserVersion().getUserAgent(), userAgent);
526     }
527 
528     @SuppressWarnings("unchecked")
529     private static <T> T get(final Object o, final String fieldName) throws Exception {
530         final Field field = o.getClass().getDeclaredField(fieldName);
531         field.setAccessible(true);
532         return (T) field.get(o);
533     }
534 
535     /**
536      * Tests creation of a web response.
537      * @throws Exception if the test fails
538      */
539     @Test
540     public void makeWebResponse() throws Exception {
541         final URL url = new URL("http://htmlunit.sourceforge.net/");
542         final String content = DOCTYPE_HTML + "<html><head></head><body></body></html>";
543         final DownloadedContent downloadedContent = new DownloadedContent.InMemory(content.getBytes());
544         final long loadTime = 500L;
545 
546         final ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 0);
547         final StatusLine statusLine = new BasicStatusLine(protocolVersion, HttpStatus.OK_200, null);
548         final HttpResponse httpResponse = new BasicHttpResponse(statusLine);
549 
550         final HttpEntity responseEntity = new StringEntity(content);
551         httpResponse.setEntity(responseEntity);
552 
553         try (HttpWebConnection connection = new HttpWebConnection(getWebClient())) {
554             final Method method = connection.getClass().getDeclaredMethod("makeWebResponse",
555                     HttpResponse.class, WebRequest.class, DownloadedContent.class, long.class);
556             final WebResponse response = (WebResponse) method.invoke(connection,
557                     httpResponse, new WebRequest(url), downloadedContent, Long.valueOf(loadTime));
558 
559             assertEquals(HttpStatus.OK_200, response.getStatusCode());
560             assertEquals(url, response.getWebRequest().getUrl());
561             assertEquals(loadTime, response.getLoadTime());
562             assertEquals(content, response.getContentAsString());
563             try (InputStream is = response.getContentAsStream()) {
564                 assertEquals(content.getBytes(), IOUtils.toByteArray(is));
565             }
566             try (InputStream is = response.getContentAsStream()) {
567                 assertEquals(new ByteArrayInputStream(content.getBytes()), is);
568             }
569         }
570     }
571 
572     /**
573      * Test for overwriting the
574      * {@link HttpWebConnection#downloadResponse(HttpUriRequest, WebRequest, HttpResponse, long)}
575      * method.
576      *
577      * @throws Exception if the test fails
578      */
579     @Test
580     public void contentBlocking() throws Exception {
581         final byte[] content = new byte[] {77, 44};
582         final List<NameValuePair> headers = new ArrayList<>();
583         headers.add(new NameValuePair("Content-Encoding", "gzip"));
584         headers.add(new NameValuePair(HttpHeader.CONTENT_LENGTH, String.valueOf(content.length)));
585 
586         final MockWebConnection conn = getMockWebConnection();
587         conn.setResponse(URL_FIRST, content, 200, "OK", MimeType.APPLICATION_JSON, headers);
588 
589         startWebServer(conn);
590 
591         final WebClient client = getWebClient();
592         client.setWebConnection(new HttpWebConnection(client) {
593             @Override
594             protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
595                     final WebRequest webRequest, final HttpResponse httpResponse,
596                     final long startTime) {
597 
598                 httpMethod.abort();
599 
600                 final DownloadedContent downloaded = new DownloadedContent.InMemory(null);
601                 final long endTime = System.currentTimeMillis();
602                 final WebResponse response = makeWebResponse(httpResponse, webRequest, downloaded, endTime - startTime);
603                 response.markAsBlocked("test blocking");
604                 return response;
605             }
606         });
607 
608         final UnexpectedPage page = client.getPage(URL_FIRST);
609         assertTrue(page.getWebResponse().wasBlocked());
610         assertEquals("test blocking", page.getWebResponse().getBlockReason());
611     }
612 
613     /**
614      * Test for overwriting the
615      * {@link HttpWebConnection#downloadResponse(HttpUriRequest, WebRequest, HttpResponse, long)}
616      * method.
617      *
618      * @throws Exception if the test fails
619      */
620     @Test
621     public void contentSizeBlocking() throws Exception {
622         stopWebServer();
623 
624         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
625         servlets.put("/big", BigContentServlet.class);
626         startWebServer("./", null, servlets);
627 
628         final WebClient client = getWebClient();
629         client.setWebConnection(new HttpWebConnection(client) {
630             @Override
631             protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
632                     final WebRequest webRequest, final HttpResponse httpResponse,
633                     final long startTime) throws IOException {
634 
635                 final int contentLenght = Integer.parseInt(
636                         httpResponse.getFirstHeader(HttpHeader.CONTENT_LENGTH).getValue());
637 
638                 if (contentLenght < 1_000) {
639                     return super.downloadResponse(httpMethod, webRequest, httpResponse, startTime);
640                 }
641 
642                 httpMethod.abort();
643 
644                 final DownloadedContent downloaded = new DownloadedContent.InMemory(null);
645                 final long endTime = System.currentTimeMillis();
646                 final WebResponse response = makeWebResponse(httpResponse, webRequest, downloaded, endTime - startTime);
647                 response.markAsBlocked("blocking " + contentLenght);
648                 return response;
649             }
650         });
651 
652         final TextPage page = client.getPage(URL_FIRST + "big");
653         assertTrue(page.getWebResponse().wasBlocked());
654         assertEquals("blocking 10240000", page.getWebResponse().getBlockReason());
655         assertTrue("blocks sent " + BigContentServlet.SENT_, BigContentServlet.SENT_ < 5000);
656 
657         BigContentServlet.CANCEL_ = true;
658     }
659 
660     /**
661      * Servlet for bigContent().
662      */
663     public static class BigContentServlet extends HttpServlet {
664 
665         /** Helper. */
666         public static int SENT_;
667         /** Helper. */
668         public static boolean CANCEL_;
669 
670         /**
671          * {@inheritDoc}
672          */
673         @Override
674         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
675             final int blockSize = 1024;
676             final int blockCount = 10_000;
677 
678             response.setHeader(HttpHeader.CONTENT_LENGTH, String.valueOf(blockSize * blockCount));
679 
680             final byte[] buffer = new byte[blockSize];
681             try (OutputStream out = response.getOutputStream()) {
682                 for (int i = 0; i < blockCount && !CANCEL_; i++) {
683                     SENT_++;
684                     out.write(buffer);
685                 }
686             }
687         }
688     }
689 }