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.IOException;
20  import java.io.Writer;
21  import java.net.SocketTimeoutException;
22  import java.util.HashMap;
23  import java.util.Map;
24  
25  import javax.servlet.Servlet;
26  import javax.servlet.http.HttpServlet;
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  import org.htmlunit.html.HtmlPage;
31  import org.htmlunit.util.Cookie;
32  import org.htmlunit.util.MimeType;
33  import org.junit.jupiter.api.Test;
34  
35  /**
36   * Tests for {@link WebClient} that run with BrowserRunner.
37   *
38   * @author Ahmed Ashour
39   * @author Ronald Brill
40   */
41  public class WebClient4Test extends WebServerTestCase {
42  
43      /**
44       * Verifies that a WebClient can be serialized and deserialized after it has been used.
45       * @throws Exception if an error occurs
46       */
47      @Test
48      public void serialization_afterUse() throws Exception {
49          startWebServer("./");
50  
51          try (WebClient client = getWebClient()) {
52              TextPage textPage = client.getPage(URL_FIRST + "LICENSE.txt");
53              assertTrue(textPage.getContent().contains("Apache License"));
54  
55              try (WebClient copy = clone(client)) {
56                  assertNotNull(copy);
57  
58                  final WebWindow window = copy.getCurrentWindow();
59                  assertNotNull(window);
60  
61                  final WebWindow topWindow = window.getTopWindow();
62                  assertNotNull(topWindow);
63  
64                  final Page page = topWindow.getEnclosedPage();
65                  assertNotNull(page);
66  
67                  final WebResponse response = page.getWebResponse();
68                  assertNotNull(response);
69  
70                  final String content = response.getContentAsString();
71                  assertNotNull(content);
72                  assertTrue(content.contains("Apache License"));
73  
74                  textPage = copy.getPage(URL_FIRST + "LICENSE.txt");
75                  assertTrue(textPage.getContent().contains("Apache License"));
76              }
77          }
78      }
79  
80      /**
81       * Verifies that a redirect limit kicks in even if the redirects aren't for the same URL
82       * and don't use the same redirect HTTP status code (see bug 2915453).
83       * @throws Exception if an error occurs
84       */
85      @Test
86      public void redirectInfinite303And307() throws Exception {
87          final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
88          servlets.put(RedirectServlet307.URL, RedirectServlet307.class);
89          servlets.put(RedirectServlet303.URL, RedirectServlet303.class);
90          startWebServer("./", new String[0], servlets);
91  
92          final WebClient client = getWebClient();
93  
94          try {
95              client.getPage("http://localhost:" + PORT + RedirectServlet307.URL);
96          }
97          catch (final Exception e) {
98              assertTrue(e.getMessage(), e.getMessage().contains("Too much redirect"));
99          }
100     }
101 
102     /**
103      * Helper class for {@link #redirectInfinite303And307}.
104      */
105     public static class RedirectServlet extends HttpServlet {
106         private int count_;
107         private int status_;
108         private String location_;
109 
110         /**
111          * Creates a new instance.
112          * @param status the HTTP status to return
113          * @param location the location to redirect to
114          */
115         public RedirectServlet(final int status, final String location) {
116             count_ = 0;
117             status_ = status;
118             location_ = location;
119         }
120 
121         /**
122          * {@inheritDoc}
123          */
124         @Override
125         protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
126             count_++;
127             resp.setStatus(status_);
128             resp.setHeader("Location", location_);
129             resp.getWriter().write(status_ + " " + req.getContextPath() + " " + count_);
130         }
131     }
132 
133     /**
134      * Helper class for {@link #redirectInfinite303And307}.
135      */
136     public static class RedirectServlet303 extends RedirectServlet {
137         static final String URL = "/test";
138         /** Creates a new instance. */
139         public RedirectServlet303() {
140             super(303, RedirectServlet307.URL);
141         }
142     }
143 
144     /**
145      * Helper class for {@link #redirectInfinite303And307}.
146      */
147     public static class RedirectServlet307 extends RedirectServlet {
148         static final String URL = "/test2";
149         /** Creates a new instance. */
150         public RedirectServlet307() {
151             super(307, RedirectServlet303.URL);
152         }
153     }
154 
155     /**
156      * Regression test for bug 2903223: body download time was not taken into account
157      * in {@link WebResponse#getLoadTime()}.
158      * @throws Exception if an error occurs
159      */
160     @Test
161     public void bodyDowloadTime() throws Exception {
162         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
163         servlets.put("/*", ServeBodySlowlyServlet.class);
164         startWebServer("./", new String[0], servlets);
165 
166         final Page page = getWebClient().getPage(URL_FIRST);
167         final long loadTime = page.getWebResponse().getLoadTime();
168         assertTrue("Load time: " + loadTime + ", last request time: " + ServeBodySlowlyServlet.LastRequestTime_,
169                 loadTime >= ServeBodySlowlyServlet.LastRequestTime_);
170     }
171 
172     /**
173      * Helper class for {@link #bodyDowloadTime}.
174      */
175     public static class ServeBodySlowlyServlet extends HttpServlet {
176         private static volatile long LastRequestTime_ = -1;
177 
178         @Override
179         protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
180             final long before = System.currentTimeMillis();
181             final Writer writer = resp.getWriter();
182             writeSomeContent(writer); // some content quickly
183             writer.flush();
184             try {
185                 Thread.sleep(500);
186             }
187             catch (final InterruptedException e) {
188                 throw new RuntimeException(e);
189             }
190             writeSomeContent(writer); // and some content later
191             LastRequestTime_ = System.currentTimeMillis() - before;
192         }
193 
194         private static void writeSomeContent(final Writer writer) throws IOException {
195             for (int i = 0; i < 1000; i++) {
196                 writer.append((char) ('a' + (i % 26)));
197             }
198         }
199     }
200 
201     /**
202      * @throws Exception if the test fails
203      */
204     @Test
205     public void useProxy() throws Exception {
206         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
207         servlets.put("/test", UseProxyHeaderServlet.class);
208         startWebServer("./", null, servlets);
209 
210         final WebClient client = getWebClient();
211         final HtmlPage page = client.getPage(URL_FIRST + "test");
212         assertEquals("Going anywhere?", page.asNormalizedText());
213     }
214 
215     /**
216      * Servlet for {@link #useProxy()}.
217      */
218     public static class UseProxyHeaderServlet extends HttpServlet {
219         /**
220          * {@inheritDoc}
221          */
222         @Override
223         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
224             response.setStatus(HttpServletResponse.SC_USE_PROXY);
225             //Won't matter!
226             //response.setHeader("Location", "http://www.google.com");
227             response.setContentType(MimeType.TEXT_HTML);
228             final Writer writer = response.getWriter();
229             writer.write(DOCTYPE_HTML + "<html><body>Going anywhere?</body></html>");
230         }
231     }
232 
233     /**
234      * Regression test for bug 2803378: GET or POST to a URL that returns HTTP 204 (No Content).
235      * @throws Exception if an error occurs
236      */
237     @Test
238     public void noContent() throws Exception {
239         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
240         servlets.put("/test1", NoContentServlet1.class);
241         servlets.put("/test2", NoContentServlet2.class);
242         startWebServer("./", null, servlets);
243         final WebClient client = getWebClient();
244         final HtmlPage page = client.getPage(URL_FIRST + "test1");
245         final HtmlPage page2 = page.getHtmlElementById("submit").click();
246         assertEquals(page, page2);
247     }
248 
249     /**
250      * First servlet for {@link #noContent()}.
251      */
252     public static class NoContentServlet1 extends HttpServlet {
253         /**
254          * {@inheritDoc}
255          */
256         @Override
257         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
258             res.setContentType(MimeType.TEXT_HTML);
259             final Writer writer = res.getWriter();
260             writer.write(DOCTYPE_HTML
261                     + "<html><body><form action='test2'>\n"
262                     + "<input id='submit' type='submit' value='submit'></input>\n"
263                     + "</form></body></html>");
264         }
265     }
266 
267     /**
268      * Second servlet for {@link #noContent()}.
269      */
270     public static class NoContentServlet2 extends HttpServlet {
271         /**
272          * {@inheritDoc}
273          */
274         @Override
275         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) {
276             res.setStatus(HttpServletResponse.SC_NO_CONTENT);
277         }
278     }
279 
280     /**
281      * Regression test for bug 2821888: HTTP 304 (Not Modified) was being treated as a redirect. Note that a 304
282      * response doesn't really make sense because we're not sending any If-Modified-Since headers, but we want to
283      * at least make sure that we're not throwing exceptions when we receive one of these responses.
284      * @throws Exception if an error occurs
285      */
286     @Test
287     public void notModified() throws Exception {
288         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
289         servlets.put("/test", NotModifiedServlet.class);
290         startWebServer("./", null, servlets);
291         final WebClient client = getWebClient();
292         final HtmlPage page = client.getPage(URL_FIRST + "test");
293         final TextPage page2 = client.getPage(URL_FIRST + "test");
294         assertNotNull(page);
295         assertNotNull(page2);
296     }
297 
298     /**
299      * Servlet for {@link #notModified()}.
300      */
301     public static class NotModifiedServlet extends HttpServlet {
302         private boolean first_ = true;
303 
304         /**
305          * {@inheritDoc}
306          */
307         @Override
308         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
309             if (first_) {
310                 first_ = false;
311                 res.setContentType(MimeType.TEXT_HTML);
312                 final Writer writer = res.getWriter();
313                 writer.write(DOCTYPE_HTML + "<html><body>foo</body></html>");
314             }
315             else {
316                 res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
317             }
318         }
319     }
320 
321     /**
322      * @throws Exception if an error occurs
323      */
324     @Test
325     public void timeout() throws Exception {
326         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
327         servlets.put("/*", DelayDeliverServlet.class);
328         startWebServer("./", null, servlets);
329 
330         final WebClient client = getWebClient();
331         client.getOptions().setTimeout(500);
332 
333         try {
334             client.getPage(URL_FIRST);
335             fail("timeout expected!");
336         }
337         catch (final SocketTimeoutException e) {
338             // as expected
339         }
340 
341         // now configure higher timeout allowing to get the page
342         client.getOptions().setTimeout(5000);
343         client.getPage(URL_FIRST);
344     }
345 
346     /**
347      * Make sure cookies set for the request are overwriting the cookieManager.
348      *
349      * @throws Exception if something goes wrong
350      */
351     @Test
352     public void requestHeaderCookieFromRequest() throws Exception {
353         final String content = DOCTYPE_HTML + "<html></html>";
354         final MockWebConnection webConnection = new MockWebConnection();
355         webConnection.setDefaultResponse(content);
356 
357         startWebServer(webConnection);
358 
359         final WebClient client = getWebClient();
360 
361         // no cookie sent
362         client.getPage(URL_FIRST);
363         assertNull(webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
364 
365         // process web request with cookie
366         WebRequest wr = new WebRequest(URL_FIRST, HttpMethod.GET);
367         wr.setAdditionalHeader(HttpHeader.COOKIE, "yummy_cookie=choco");
368         client.getPage(wr);
369         assertEquals("yummy_cookie=choco", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
370 
371         // add cookie to the cookie manager and test if sent
372         final CookieManager mgr = client.getCookieManager();
373         mgr.addCookie(new Cookie(URL_FIRST.getHost(), "my_key", "my_value", "/", null, false));
374         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
375         client.getPage(wr);
376         assertEquals("my_key=my_value", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
377 
378         // request page again, now the the request provides his own cookies
379         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
380         wr.setAdditionalHeader(HttpHeader.COOKIE, "yummy_cookie=choco");
381         client.getPage(wr);
382         assertEquals("yummy_cookie=choco", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
383 
384         // request page again, now the the request provides his own cookies as part of a map
385         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
386         final Map<String, String> headers = new HashMap<>();
387         headers.put("accept-language", "es-ES,es;q=0.9");
388         headers.put(HttpHeader.COOKIE, "tasty_cookie=strawberry");
389         wr.setAdditionalHeaders(headers);
390         client.getPage(wr);
391         assertEquals("tasty_cookie=strawberry", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
392 
393         // and finally to make sure the cookies from the store are still there
394         wr = new WebRequest(URL_FIRST, HttpMethod.GET);
395         client.getPage(wr);
396         assertEquals("my_key=my_value", webConnection.getLastAdditionalHeaders().get(HttpHeader.COOKIE));
397     }
398 
399     /**
400      * Servlet for {@link #timeout()}.
401      */
402     public static class DelayDeliverServlet extends HttpServlet {
403         /**
404          * {@inheritDoc}
405          */
406         @Override
407         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
408             try {
409                 Thread.sleep(1000);
410             }
411             catch (final InterruptedException e) {
412                 throw new RuntimeException(e);
413             }
414             res.setContentType(MimeType.TEXT_HTML);
415             final Writer writer = res.getWriter();
416             writer.write(DOCTYPE_HTML + "<html><head><title>hello</title></head><body>foo</body></html>");
417         }
418     }
419 
420     /**
421      * @throws Exception if an error occurs
422      */
423     @Test
424     public void redirectInfiniteMeta() throws Exception {
425         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
426         servlets.put("/test1", RedirectMetaServlet1.class);
427         servlets.put("/test2", RedirectMetaServlet2.class);
428         startWebServer("./", new String[0], servlets);
429 
430         final WebClient client = getWebClient();
431 
432         try {
433             client.getPage(URL_FIRST + "test1");
434         }
435         catch (final Exception e) {
436             assertTrue(e.getMessage(), e.getMessage().contains("Too much redirect"));
437         }
438     }
439 
440     /**
441      * Servlet for {@link #redirectInfiniteMeta()}.
442      */
443     public static class RedirectMetaServlet1 extends HttpServlet {
444         /**
445          * {@inheritDoc}
446          */
447         @Override
448         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
449             res.setContentType(MimeType.TEXT_HTML);
450             final Writer writer = res.getWriter();
451             writer.write(DOCTYPE_HTML
452                     + "<html><head>\n"
453                     + "  <meta http-equiv='refresh' content='0;URL=test2'>\n"
454                     + "</head><body>foo</body></html>");
455         }
456     }
457 
458     /**
459      * Servlet for {@link #redirectInfiniteMeta()}.
460      */
461     public static class RedirectMetaServlet2 extends HttpServlet {
462         /**
463          * {@inheritDoc}
464          */
465         @Override
466         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
467             res.setContentType(MimeType.TEXT_HTML);
468             final Writer writer = res.getWriter();
469             writer.write(DOCTYPE_HTML
470                     + "<html><head>\n"
471                     + "  <meta http-equiv='refresh' content='0;URL=test1'>\n"
472                     + "</head><body>foo</body></html>");
473         }
474     }
475 }