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