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.javascript.host.xml;
16  
17  import java.io.IOException;
18  import java.io.Writer;
19  import java.net.URL;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Enumeration;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  
28  import javax.servlet.Servlet;
29  import javax.servlet.http.HttpServlet;
30  import javax.servlet.http.HttpServletRequest;
31  import javax.servlet.http.HttpServletResponse;
32  
33  import org.htmlunit.CollectingAlertHandler;
34  import org.htmlunit.HttpMethod;
35  import org.htmlunit.MockWebConnection;
36  import org.htmlunit.WebClient;
37  import org.htmlunit.WebRequest;
38  import org.htmlunit.WebResponse;
39  import org.htmlunit.WebServerTestCase;
40  import org.htmlunit.html.DomChangeEvent;
41  import org.htmlunit.html.DomChangeListener;
42  import org.htmlunit.html.DomElement;
43  import org.htmlunit.html.HtmlElement;
44  import org.htmlunit.html.HtmlPage;
45  import org.htmlunit.javascript.host.xml.XMLHttpRequestTest.StreamingServlet;
46  import org.htmlunit.junit.annotation.Alerts;
47  import org.htmlunit.junit.annotation.HtmlUnitNYI;
48  import org.htmlunit.util.MimeType;
49  import org.junit.jupiter.api.Test;
50  
51  /**
52   * Tests for {@link XMLHttpRequest}.
53   *
54   * @author Daniel Gredler
55   * @author Marc Guillemot
56   * @author Ahmed Ashour
57   * @author Stuart Begg
58   * @author Sudhan Moghe
59   * @author Frank Danek
60   * @author Ronald Brill
61   */
62  public class XMLHttpRequest3Test extends WebServerTestCase {
63  
64      private static final String MSG_NO_CONTENT = "no Content";
65      private static final String MSG_PROCESSING_ERROR = "error processing";
66  
67      /**
68       * Tests asynchronous use of XMLHttpRequest, where the XHR request fails due to IOException (Connection refused).
69       * @throws Exception if the test fails
70       */
71      @Test
72      @Alerts({"0", "1", "4", MSG_NO_CONTENT, MSG_PROCESSING_ERROR})
73      public void asyncUseWithNetworkConnectionFailure() throws Exception {
74          final String html = DOCTYPE_HTML
75              + "<html>\n"
76              + "<head>\n"
77              + "<title>XMLHttpRequest Test</title>\n"
78              + "<script>\n"
79              + "var request;\n"
80              + "function testAsync() {\n"
81              + "  request = new XMLHttpRequest();\n"
82              + "  request.onreadystatechange = onReadyStateChange;\n"
83              + "  request.onerror = onError;\n"
84              + "  alert(request.readyState);\n"
85              + "  request.open('GET', '" + URL_SECOND + "', true);\n"
86              + "  request.send('');\n"
87              + "}\n"
88              + "function onError() {\n"
89              + "  alert('" + MSG_PROCESSING_ERROR + "');\n"
90              + "}\n"
91              + "function onReadyStateChange() {\n"
92              + "  alert(request.readyState);\n"
93              + "  if (request.readyState == 4) {\n"
94              + "    if (request.responseText == null)\n"
95              + "      alert('" + MSG_NO_CONTENT + "');\n"
96              + "    else\n"
97              + "      throw 'Unexpected content, should be zero length but is: \"' + request.responseText + '\"';\n"
98              + "  }\n"
99              + "}\n"
100             + "</script>\n"
101             + "</head>\n"
102             + "<body onload='testAsync()'>\n"
103             + "</body>\n"
104             + "</html>";
105 
106         final WebClient client = getWebClient();
107         final MockWebConnection conn = new DisconnectedMockWebConnection();
108         conn.setResponse(URL_FIRST, html);
109         client.setWebConnection(conn);
110         client.getPage(URL_FIRST);
111 
112         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(1000));
113         assertEquals(getExpectedAlerts(), getCollectedAlerts(null));
114     }
115 
116     /**
117      * Connection refused WebConnection for URL_SECOND.
118      */
119     private static final class DisconnectedMockWebConnection extends MockWebConnection {
120 
121         DisconnectedMockWebConnection() {
122         }
123 
124         /** {@inheritDoc} */
125         @Override
126         public WebResponse getResponse(final WebRequest request) throws IOException {
127             if (URL_SECOND.equals(request.getUrl())) {
128                 throw new IOException("Connection refused");
129             }
130             return super.getResponse(request);
131         }
132     }
133 
134     /**
135      * Asynchronous callback should be called in "main" js thread and not parallel to other js execution.
136      * See http://sourceforge.net/p/htmlunit/bugs/360/.
137      * @throws Exception if the test fails
138      */
139     @Test
140     public void noParallelJSExecutionInPage() throws Exception {
141         final String content = DOCTYPE_HTML
142             + "<html><head><script>\n"
143             + "var j = 0;\n"
144             + "function test() {\n"
145             + "  req = new XMLHttpRequest();\n"
146             + "  req.onreadystatechange = handler;\n"
147             + "  req.open('post', 'foo.xml', true);\n"
148             + "  req.send('');\n"
149             + "  alert('before long loop');\n"
150             + "  for (var i = 0; i < 5000; i++) {\n"
151             + "    j = j + 1;\n"
152             + "  }\n"
153             + "  alert('after long loop');\n"
154             + "}\n"
155             + "function handler() {\n"
156             + "  if (req.readyState == 4) {\n"
157             + "    alert('ready state handler, content loaded: j=' + j);\n"
158             + "  }\n"
159             + "}\n"
160             + "</script></head>\n"
161             + "<body onload='test()'></body></html>";
162 
163         final WebClient client = getWebClient();
164         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
165         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
166         final MockWebConnection conn = new MockWebConnection() {
167             @Override
168             public WebResponse getResponse(final WebRequest webRequest) throws IOException {
169                 collectedAlerts.add(webRequest.getUrl().toExternalForm());
170                 return super.getResponse(webRequest);
171             }
172         };
173         conn.setResponse(URL_FIRST, content);
174         final URL urlPage2 = new URL(URL_FIRST, "foo.xml");
175         conn.setResponse(urlPage2, "<foo/>\n", MimeType.TEXT_XML);
176         client.setWebConnection(conn);
177         client.getPage(URL_FIRST);
178 
179         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(1000));
180 
181         final String[] alerts = {URL_FIRST.toExternalForm(), "before long loop", "after long loop",
182             urlPage2.toExternalForm(), "ready state handler, content loaded: j=5000" };
183         assertEquals(alerts, collectedAlerts);
184     }
185 
186     /**
187      * Tests that the different HTTP methods are supported.
188      * @throws Exception if an error occurs
189      */
190     @Test
191     public void methods() throws Exception {
192         testMethod(HttpMethod.GET);
193         testMethod(HttpMethod.HEAD);
194         testMethod(HttpMethod.DELETE);
195         testMethod(HttpMethod.POST);
196         testMethod(HttpMethod.PUT);
197         testMethod(HttpMethod.OPTIONS);
198         testMethod(HttpMethod.TRACE);
199         testMethod(HttpMethod.PATCH);
200     }
201 
202     /**
203      * @throws Exception if the test fails
204      */
205     private void testMethod(final HttpMethod method) throws Exception {
206         final String content = DOCTYPE_HTML
207             + "<html><head><script>\n"
208             + "function test() {\n"
209             + "  var req = new XMLHttpRequest();\n"
210             + "  req.open('" + method.name().toLowerCase(Locale.ROOT) + "', 'foo.xml', false);\n"
211             + "  req.send('');\n"
212             + "}\n"
213             + "</script></head>\n"
214             + "<body onload='test()'></body></html>";
215 
216         final WebClient client = getWebClient();
217         final MockWebConnection conn = new MockWebConnection();
218         conn.setResponse(URL_FIRST, content);
219         final URL urlPage2 = new URL(URL_FIRST, "foo.xml");
220         conn.setResponse(urlPage2, "<foo/>\n", MimeType.TEXT_XML);
221         client.setWebConnection(conn);
222         client.getPage(URL_FIRST);
223 
224         final WebRequest request = conn.getLastWebRequest();
225         assertEquals(urlPage2, request.getUrl());
226         assertSame(method, request.getHttpMethod());
227     }
228 
229     /**
230      * Was causing a deadlock on 03.11.2007 (and probably with release 1.13 too).
231      * @throws Exception if the test fails
232      */
233     @Test
234     public void xmlHttpRequestWithDomChangeListenerDeadlock() throws Exception {
235         final String content = DOCTYPE_HTML
236             + "<html><head><title>foo</title>\n"
237             + "<script>\n"
238             + "  function test() {\n"
239             + "    frames[0].test('foo1.txt', true);\n"
240             + "    frames[0].test('foo2.txt', false);\n"
241             + "  }\n"
242             + "</script>\n"
243             + "</head>\n"
244             + "<body>\n"
245             + "<p id='p1' title='myTitle' onclick='test()'></p>\n"
246             + "<iframe src='page2.html'></iframe>\n"
247             + "</body></html>";
248 
249         final String content2 = DOCTYPE_HTML
250             + "<html><head><title>foo</title>\n"
251             + "<script>\n"
252             + "function test(_src, _async)\n"
253             + "{\n"
254             + "  var request = new XMLHttpRequest();\n"
255             + "  request.onreadystatechange = onReadyStateChange;\n"
256             + "  request.open('GET', _src, _async);\n"
257             + "  request.send('');\n"
258             + "}\n"
259             + "function onReadyStateChange() {\n"
260             + "  parent.document.getElementById('p1').title = 'new title';\n"
261             + "}\n"
262             + "</script>\n"
263             + "</head>\n"
264             + "<body>\n"
265             + "<p id='p1' title='myTitle'></p>\n"
266             + "</body></html>";
267 
268         final MockWebConnection connection = new MockWebConnection() {
269             private boolean gotFoo1_ = false;
270 
271             @Override
272             public WebResponse getResponse(final WebRequest webRequest) throws IOException {
273                 final String url = webRequest.getUrl().toExternalForm();
274 
275                 synchronized (this) {
276                     while (!gotFoo1_ && url.endsWith("foo2.txt")) {
277                         try {
278                             wait(100);
279                         }
280                         catch (final InterruptedException e) {
281                             e.printStackTrace();
282                         }
283                     }
284                 }
285                 if (url.endsWith("foo1.txt")) {
286                     gotFoo1_ = true;
287                 }
288                 return super.getResponse(webRequest);
289             }
290         };
291         connection.setDefaultResponse("");
292         connection.setResponse(URL_FIRST, content);
293         connection.setResponse(new URL(URL_FIRST, "page2.html"), content2);
294 
295         final WebClient webClient = getWebClient();
296         webClient.setWebConnection(connection);
297 
298         final HtmlPage page = webClient.getPage(URL_FIRST);
299         final DomChangeListener listener = new DomChangeListener() {
300             @Override
301             public void nodeAdded(final DomChangeEvent event) {
302                 // Empty.
303             }
304             @Override
305             public void nodeDeleted(final DomChangeEvent event) {
306                 // Empty.
307             }
308         };
309         page.addDomChangeListener(listener);
310         page.getHtmlElementById("p1").click();
311     }
312 
313     /**
314      * Regression test for bug 1209686 (onreadystatechange not called with partial data when emulating FF).
315      * @throws Exception if an error occurs
316      */
317     @Test
318     @Alerts({"0", "10"})
319     @HtmlUnitNYI(CHROME = {"0", "1"},
320             EDGE = {"0", "1"},
321             FF = {"0", "1"},
322             FF_ESR = {"0", "1"})
323     public void streaming() throws Exception {
324         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
325         servlets.put("/test", StreamingServlet.class);
326 
327         final String resourceBase = "./src/test/resources/org/htmlunit/javascript/host";
328         startWebServer(resourceBase, null, servlets);
329         final WebClient client = getWebClient();
330         final HtmlPage page = client.getPage(URL_FIRST + "XMLHttpRequestTest_streaming.html");
331         assertEquals(Integer.parseInt(getExpectedAlerts()[0]), client.waitForBackgroundJavaScriptStartingBefore(1000));
332         final HtmlElement body = page.getBody();
333         assertEquals(Integer.parseInt(getExpectedAlerts()[1]), body.asNormalizedText().split("\n").length);
334     }
335 
336     /**
337      * Tests the value of "this" in handler.
338      * @throws Exception if the test fails
339      */
340     @Test
341     @Alerts("this == request")
342     public void thisValueInHandler() throws Exception {
343         final String html = DOCTYPE_HTML
344             + "<html>\n"
345             + "  <head>\n"
346             + "    <title>XMLHttpRequest Test</title>\n"
347             + "    <script>\n"
348             + "      var request;\n"
349             + "      function testAsync() {\n"
350             + "        request = new XMLHttpRequest();\n"
351             + "        request.onreadystatechange = onReadyStateChange;\n"
352             + "        request.open('GET', 'foo.xml', true);\n"
353             + "        request.send('');\n"
354             + "      }\n"
355             + "      function onReadyStateChange() {\n"
356             + "        if (request.readyState == 4) {\n"
357             + "          if (this == request)\n"
358             + "            alert('this == request');\n"
359             + "          else if (this == onReadyStateChange)\n"
360             + "            alert('this == handler');\n"
361             + "          else alert('not expected: ' + this)\n"
362             + "        }\n"
363             + "      }\n"
364             + "    </script>\n"
365             + "  </head>\n"
366             + "  <body onload='testAsync()'>\n"
367             + "  </body>\n"
368             + "</html>";
369 
370         final WebClient client = getWebClient();
371         final MockWebConnection conn = new MockWebConnection();
372         conn.setResponse(URL_FIRST, html);
373         conn.setDefaultResponse("");
374         client.setWebConnection(conn);
375         client.getPage(URL_FIRST);
376 
377         assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(1000));
378         assertEquals(getExpectedAlerts(), getCollectedAlerts(null));
379     }
380 
381     /**
382      * Test for a strange error we found: An ajax running
383      * in parallel shares the additional headers with a form
384      * submit.
385      *
386      * @throws Exception if an error occurs
387      */
388     @Test
389     public void ajaxInfluencesSubmitHeaders() throws Exception {
390         final Map<String, Class<? extends Servlet>> servlets = new HashMap<>();
391         servlets.put("/content.html", ContentServlet.class);
392         servlets.put("/ajax_headers.html", AjaxHeaderServlet.class);
393         servlets.put("/form_headers.html", FormHeaderServlet.class);
394         startWebServer("./", null, servlets);
395 
396         COLLECTED_HEADERS.clear();
397         XMLHttpRequest3Test.STATE_ = 0;
398         final WebClient client = getWebClient();
399 
400         final List<String> collectedAlerts = Collections.synchronizedList(new ArrayList<String>());
401         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
402 
403         final HtmlPage page = client.getPage(URL_FIRST + "content.html");
404         final DomElement elem = page.getElementById("doIt");
405         while (STATE_ < 1) {
406             Thread.sleep(42);
407         }
408         elem.click();
409 
410         client.waitForBackgroundJavaScript(DEFAULT_WAIT_TIME.toMillis());
411         assertEquals(COLLECTED_HEADERS.toString(), 2, COLLECTED_HEADERS.size());
412 
413         String headers = COLLECTED_HEADERS.get(0);
414         if (!headers.startsWith("Form: ")) {
415             headers = COLLECTED_HEADERS.get(1);
416         }
417         assertTrue(headers, headers.startsWith("Form: "));
418         assertFalse(headers, headers.contains("Html-Unit=is great,;"));
419 
420         headers = COLLECTED_HEADERS.get(0);
421         if (!headers.startsWith("Ajax: ")) {
422             headers = COLLECTED_HEADERS.get(1);
423         }
424         assertTrue(headers, headers.startsWith("Ajax: "));
425         assertTrue(headers, headers.contains("Html-Unit=is great,;"));
426     }
427 
428     static final List<String> COLLECTED_HEADERS = Collections.synchronizedList(new ArrayList<String>());
429     static int STATE_ = 0;
430 
431     /**
432      * First servlet for testNoContent().
433      */
434     public static class ContentServlet extends HttpServlet {
435         /**
436          * {@inheritDoc}
437          */
438         @Override
439         protected void doGet(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
440             final String html = DOCTYPE_HTML
441                     + "<html><head><script>\n"
442                     + "  function test() {\n"
443                     + "    xhr = new XMLHttpRequest();\n"
444                     + "    xhr.open('POST', 'ajax_headers.html', true);\n"
445                     + "    xhr.setRequestHeader('Html-Unit', 'is great');\n"
446                     + "    xhr.send('');\n"
447                     + "  }\n"
448                     + "</script></head>\n"
449                     + "<body onload='test()'>\n"
450                     + "  <form action='form_headers.html' name='myForm'>\n"
451                     + "    <input name='myField' value='some value'>\n"
452                     + "    <input type='submit' id='doIt' value='Do It'>\n"
453                     + "  </form>\n"
454                     + "</body></html>";
455 
456             res.setContentType(MimeType.TEXT_HTML);
457             final Writer writer = res.getWriter();
458             writer.write(html);
459         }
460     }
461 
462     /**
463      * Servlet for setRequestHeader().
464      */
465     public static class AjaxHeaderServlet extends HttpServlet {
466         /**
467          * {@inheritDoc}
468          */
469         @Override
470         protected void doPost(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
471             doGet(req, res);
472         }
473 
474         /**
475          * {@inheritDoc}
476          */
477         @Override
478         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
479             final String header = headers(request);
480             STATE_ = 1;
481             try {
482                 // do not return before the form request is also sent
483                 while (STATE_ < 2) {
484                     Thread.sleep(42);
485                 }
486             }
487             catch (final InterruptedException e) {
488                 e.printStackTrace();
489             }
490 
491             COLLECTED_HEADERS.add("Ajax: " + header);
492             response.setContentType(MimeType.TEXT_PLAIN);
493             final Writer writer = response.getWriter();
494             writer.write(header);
495             writer.flush();
496         }
497     }
498 
499     /**
500      * Servlet for setRequestHeader().
501      */
502     public static class FormHeaderServlet extends HttpServlet {
503         /**
504          * {@inheritDoc}
505          */
506         @Override
507         protected void doPost(final HttpServletRequest req, final HttpServletResponse res) throws IOException {
508             doGet(req, res);
509         }
510 
511         /**
512          * {@inheritDoc}
513          */
514         @Override
515         protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
516             final String header = headers(request);
517             STATE_ = 2;
518 
519             final String html = DOCTYPE_HTML
520                     + "<html><head></head>\n"
521                     + "<body>\n"
522                     + "<p>Form: " + header + "</p<\n"
523                     + "</body></html>";
524 
525             COLLECTED_HEADERS.add("Form: " + header);
526             response.setContentType(MimeType.TEXT_HTML);
527             final Writer writer = response.getWriter();
528             writer.write(html);
529             writer.flush();
530         }
531     }
532 
533     static String headers(final HttpServletRequest request) {
534         final StringBuilder text = new StringBuilder();
535         text.append("Headers: ");
536         final Enumeration<String> headerNames = request.getHeaderNames();
537         while (headerNames.hasMoreElements()) {
538             final String name = headerNames.nextElement();
539             text.append(name);
540             text.append('=');
541             final Enumeration<String> headers = request.getHeaders(name);
542             while (headers.hasMoreElements()) {
543                 final String header = headers.nextElement();
544                 text.append(header);
545                 text.append(',');
546             }
547             text.append(';');
548         }
549         return text.toString();
550     }
551 }