View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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 static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.XHR_HANDLE_SYNC_NETWORK_ERRORS;
19  import static org.htmlunit.BrowserVersionFeatures.XHR_LOAD_ALWAYS_AFTER_DONE;
20  import static org.htmlunit.BrowserVersionFeatures.XHR_RESPONSE_TEXT_EMPTY_UNSENT;
21  import static org.htmlunit.BrowserVersionFeatures.XHR_SEND_NETWORK_ERROR_IF_ABORTED;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.StringWriter;
26  import java.net.MalformedURLException;
27  import java.net.SocketTimeoutException;
28  import java.net.URL;
29  import java.nio.charset.Charset;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Map.Entry;
36  import java.util.TreeMap;
37  
38  import javax.xml.transform.OutputKeys;
39  import javax.xml.transform.Transformer;
40  import javax.xml.transform.TransformerFactory;
41  import javax.xml.transform.dom.DOMSource;
42  import javax.xml.transform.stream.StreamResult;
43  
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  import org.htmlunit.AjaxController;
48  import org.htmlunit.BrowserVersion;
49  import org.htmlunit.FormEncodingType;
50  import org.htmlunit.HttpHeader;
51  import org.htmlunit.HttpMethod;
52  import org.htmlunit.SgmlPage;
53  import org.htmlunit.WebClient;
54  import org.htmlunit.WebRequest;
55  import org.htmlunit.WebRequest.HttpHint;
56  import org.htmlunit.WebResponse;
57  import org.htmlunit.WebWindow;
58  import org.htmlunit.corejs.javascript.Context;
59  import org.htmlunit.corejs.javascript.ContextAction;
60  import org.htmlunit.corejs.javascript.Function;
61  import org.htmlunit.corejs.javascript.ScriptableObject;
62  import org.htmlunit.corejs.javascript.json.JsonParser;
63  import org.htmlunit.corejs.javascript.json.JsonParser.ParseException;
64  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
65  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
66  import org.htmlunit.html.HtmlPage;
67  import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
68  import org.htmlunit.javascript.HtmlUnitContextFactory;
69  import org.htmlunit.javascript.JavaScriptEngine;
70  import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
71  import org.htmlunit.javascript.background.JavaScriptJob;
72  import org.htmlunit.javascript.configuration.JsxClass;
73  import org.htmlunit.javascript.configuration.JsxConstant;
74  import org.htmlunit.javascript.configuration.JsxConstructor;
75  import org.htmlunit.javascript.configuration.JsxFunction;
76  import org.htmlunit.javascript.configuration.JsxGetter;
77  import org.htmlunit.javascript.configuration.JsxSetter;
78  import org.htmlunit.javascript.host.Element;
79  import org.htmlunit.javascript.host.URLSearchParams;
80  import org.htmlunit.javascript.host.Window;
81  import org.htmlunit.javascript.host.dom.DOMException;
82  import org.htmlunit.javascript.host.dom.DOMParser;
83  import org.htmlunit.javascript.host.dom.Document;
84  import org.htmlunit.javascript.host.event.Event;
85  import org.htmlunit.javascript.host.event.ProgressEvent;
86  import org.htmlunit.javascript.host.file.Blob;
87  import org.htmlunit.javascript.host.html.HTMLDocument;
88  import org.htmlunit.util.EncodingSniffer;
89  import org.htmlunit.util.MimeType;
90  import org.htmlunit.util.NameValuePair;
91  import org.htmlunit.util.StringUtils;
92  import org.htmlunit.util.UrlUtils;
93  import org.htmlunit.util.WebResponseWrapper;
94  import org.htmlunit.util.XUserDefinedCharset;
95  import org.htmlunit.xml.XmlPage;
96  import org.w3c.dom.DocumentType;
97  
98  /**
99   * A JavaScript object for an {@code XMLHttpRequest}.
100  *
101  * @author Daniel Gredler
102  * @author Marc Guillemot
103  * @author Ahmed Ashour
104  * @author Stuart Begg
105  * @author Ronald Brill
106  * @author Sebastian Cato
107  * @author Frank Danek
108  * @author Jake Cobb
109  * @author Thorsten Wendelmuth
110  * @author Lai Quang Duong
111  * @author Sven Strickroth
112  *
113  * @see <a href="http://www.w3.org/TR/XMLHttpRequest/">W3C XMLHttpRequest</a>
114  * @see <a href="http://developer.apple.com/internet/webcontent/xmlhttpreq.html">Safari documentation</a>
115  */
116 @JsxClass
117 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
118 
119     private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
120 
121     /** The object has been created, but not initialized (the open() method has not been called). */
122     @JsxConstant
123     public static final int UNSENT = 0;
124 
125     /** The object has been created, but the send() method has not been called. */
126     @JsxConstant
127     public static final int OPENED = 1;
128 
129     /** The send() method has been called, but the status and headers are not yet available. */
130     @JsxConstant
131     public static final int HEADERS_RECEIVED = 2;
132 
133     /** Some data has been received. */
134     @JsxConstant
135     public static final int LOADING = 3;
136 
137     /** All the data has been received; the complete data is available in responseBody and responseText. */
138     @JsxConstant
139     public static final int DONE = 4;
140 
141     private static final String RESPONSE_TYPE_DEFAULT = "";
142     private static final String RESPONSE_TYPE_ARRAYBUFFER = "arraybuffer";
143     private static final String RESPONSE_TYPE_BLOB = "blob";
144     private static final String RESPONSE_TYPE_DOCUMENT = "document";
145     private static final String RESPONSE_TYPE_JSON = "json";
146     private static final String RESPONSE_TYPE_TEXT = "text";
147 
148     private static final String ALLOW_ORIGIN_ALL = "*";
149 
150     private static final HashSet<String> PROHIBITED_HEADERS_ = new HashSet<>(Arrays.asList(
151         "accept-charset", HttpHeader.ACCEPT_ENCODING_LC,
152         HttpHeader.CONNECTION_LC, HttpHeader.CONTENT_LENGTH_LC, HttpHeader.COOKIE_LC, "cookie2",
153         "content-transfer-encoding", "date", "expect",
154         HttpHeader.HOST_LC, "keep-alive", HttpHeader.REFERER_LC, "te", "trailer", "transfer-encoding",
155         "upgrade", HttpHeader.USER_AGENT_LC, "via"));
156 
157     private int state_;
158     private WebRequest webRequest_;
159     private boolean async_;
160     private int jobID_;
161     private WebResponse webResponse_;
162     private String overriddenMimeType_;
163     private boolean withCredentials_;
164     private boolean isSameOrigin_;
165     private int timeout_;
166     private boolean aborted_;
167     private String responseType_;
168 
169     private Document responseXML_;
170 
171     /**
172      * Creates a new instance.
173      */
174     public XMLHttpRequest() {
175         state_ = UNSENT;
176         responseType_ = RESPONSE_TYPE_DEFAULT;
177     }
178 
179     /**
180      * JavaScript constructor.
181      */
182     @Override
183     @JsxConstructor
184     public void jsConstructor() {
185         // don't call super here
186     }
187 
188     /**
189      * Sets the state as specified and invokes the state change handler if one has been set.
190      * @param state the new state
191      */
192     private void setState(final int state) {
193         if (state == UNSENT
194                 || state == OPENED
195                 || state == HEADERS_RECEIVED
196                 || state == LOADING
197                 || state == DONE) {
198             state_ = state;
199             if (LOG.isDebugEnabled()) {
200                 LOG.debug("State changed to : " + state);
201             }
202             return;
203         }
204 
205         LOG.error("Received an unknown state " + state
206                         + ", the state is not implemented, please check setState() implementation.");
207     }
208 
209     private void fireJavascriptEvent(final String eventName) {
210         if (aborted_) {
211             if (LOG.isDebugEnabled()) {
212                 LOG.debug("Firing javascript XHR event: " + eventName + " for an already aborted request - ignored.");
213             }
214 
215             return;
216         }
217         fireJavascriptEventIgnoreAbort(eventName);
218     }
219 
220     private void fireJavascriptEventIgnoreAbort(final String eventName) {
221         if (LOG.isDebugEnabled()) {
222             LOG.debug("Firing javascript XHR event: " + eventName);
223         }
224 
225         final boolean isReadyStateChange = Event.TYPE_READY_STATE_CHANGE.equalsIgnoreCase(eventName);
226         final Event event;
227         if (isReadyStateChange) {
228             event = new Event(this, Event.TYPE_READY_STATE_CHANGE);
229         }
230         else {
231             final ProgressEvent progressEvent = new ProgressEvent(this, eventName);
232 
233             if (webResponse_ != null) {
234                 final long contentLength = webResponse_.getContentLength();
235                 progressEvent.setLoaded(contentLength);
236             }
237             event = progressEvent;
238         }
239 
240         executeEventLocally(event);
241     }
242 
243     /**
244      * Returns the current state of the HTTP request. The possible values are:
245      * <ul>
246      *   <li>0 = unsent</li>
247      *   <li>1 = opened</li>
248      *   <li>2 = headers_received</li>
249      *   <li>3 = loading</li>
250      *   <li>4 = done</li>
251      * </ul>
252      * @return the current state of the HTTP request
253      */
254     @JsxGetter
255     public int getReadyState() {
256         return state_;
257     }
258 
259     /**
260      * @return the {@code responseType} property
261      */
262     @JsxGetter
263     public String getResponseType() {
264         return responseType_;
265     }
266 
267     /**
268      * Sets the {@code responseType} property.
269      * @param responseType the {@code responseType} property.
270      */
271     @JsxSetter
272     public void setResponseType(final String responseType) {
273         if (state_ == LOADING || state_ == DONE) {
274             throw JavaScriptEngine.reportRuntimeError("InvalidStateError");
275         }
276 
277         if (RESPONSE_TYPE_DEFAULT.equals(responseType)
278                 || RESPONSE_TYPE_ARRAYBUFFER.equals(responseType)
279                 || RESPONSE_TYPE_BLOB.equals(responseType)
280                 || RESPONSE_TYPE_DOCUMENT.equals(responseType)
281                 || RESPONSE_TYPE_JSON.equals(responseType)
282                 || RESPONSE_TYPE_TEXT.equals(responseType)) {
283 
284             if (state_ == OPENED && !async_) {
285                 throw JavaScriptEngine.asJavaScriptException(
286                         getWindow(),
287                         "synchronous XMLHttpRequests do not support responseType",
288                         DOMException.INVALID_ACCESS_ERR);
289             }
290 
291             responseType_ = responseType;
292         }
293     }
294 
295     /**
296      * @return returns the response's body content as an ArrayBuffer, Blob, Document, JavaScript Object,
297      *         or DOMString, depending on the value of the request's responseType property.
298      */
299     @JsxGetter
300     public Object getResponse() {
301         if (RESPONSE_TYPE_DEFAULT.equals(responseType_) || RESPONSE_TYPE_TEXT.equals(responseType_)) {
302             if (webResponse_ != null) {
303                 final Charset encoding = webResponse_.getContentCharset();
304                 final String content = webResponse_.getContentAsString(encoding);
305                 if (content == null) {
306                     return "";
307                 }
308                 return content;
309             }
310         }
311 
312         if (state_ != DONE) {
313             return null;
314         }
315 
316         if (webResponse_ instanceof NetworkErrorWebResponse response) {
317             if (LOG.isDebugEnabled()) {
318                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
319                         + response.getError() + ")");
320             }
321             return null;
322         }
323 
324         if (RESPONSE_TYPE_ARRAYBUFFER.equals(responseType_)) {
325             long contentLength = webResponse_.getContentLength();
326             NativeArrayBuffer nativeArrayBuffer = new NativeArrayBuffer(contentLength);
327 
328             try {
329                 final int bufferLength = Math.min(1024, (int) contentLength);
330                 final byte[] buffer = new byte[bufferLength];
331                 int offset = 0;
332                 try (InputStream inputStream = webResponse_.getContentAsStream()) {
333                     int readLen;
334                     while ((readLen = inputStream.read(buffer, 0, bufferLength)) != -1) {
335                         final long newLength = offset + readLen;
336                         // gzip content and the unzipped content is larger
337                         if (newLength > contentLength) {
338                             final NativeArrayBuffer expanded = new NativeArrayBuffer(newLength);
339                             System.arraycopy(nativeArrayBuffer.getBuffer(), 0,
340                                     expanded.getBuffer(), 0, (int) contentLength);
341                             contentLength = newLength;
342                             nativeArrayBuffer = expanded;
343                         }
344                         System.arraycopy(buffer, 0, nativeArrayBuffer.getBuffer(), offset, readLen);
345                         offset = (int) newLength;
346                     }
347                 }
348 
349                 // for small responses the gzipped content might be larger than the original
350                 if (offset < contentLength) {
351                     final NativeArrayBuffer shrinked = new NativeArrayBuffer(offset);
352                     System.arraycopy(nativeArrayBuffer.getBuffer(), 0, shrinked.getBuffer(), 0, offset);
353                     nativeArrayBuffer = shrinked;
354                 }
355 
356                 nativeArrayBuffer.setParentScope(getParentScope());
357                 nativeArrayBuffer.setPrototype(
358                         ScriptableObject.getClassPrototype(getParentScope(), nativeArrayBuffer.getClassName()));
359 
360                 return nativeArrayBuffer;
361             }
362             catch (final IOException e) {
363                 webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
364                 return null;
365             }
366         }
367         else if (RESPONSE_TYPE_BLOB.equals(responseType_)) {
368             try {
369                 if (webResponse_ != null) {
370                     try (InputStream inputStream = webResponse_.getContentAsStream()) {
371                         final Blob blob = new Blob(IOUtils.toByteArray(inputStream), webResponse_.getContentType());
372                         blob.setParentScope(getParentScope());
373                         blob.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), blob.getClassName()));
374 
375                         return blob;
376                     }
377                 }
378             }
379             catch (final IOException e) {
380                 webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
381                 return null;
382             }
383         }
384         else if (RESPONSE_TYPE_DOCUMENT.equals(responseType_)) {
385             if (responseXML_ != null) {
386                 return responseXML_;
387             }
388 
389             if (webResponse_ != null) {
390                 String contentType = webResponse_.getContentType();
391                 if (org.htmlunit.util.StringUtils.isEmptyOrNull(contentType)) {
392                     contentType = MimeType.TEXT_XML;
393                 }
394                 return buildResponseXML(contentType);
395             }
396         }
397         else if (RESPONSE_TYPE_JSON.equals(responseType_)) {
398             if (webResponse_ != null) {
399                 final Charset encoding = webResponse_.getContentCharset();
400                 final String content = webResponse_.getContentAsString(encoding);
401                 if (content == null) {
402                     return null;
403                 }
404 
405                 try {
406                     return new JsonParser(Context.getCurrentContext(), getParentScope()).parseValue(content);
407                 }
408                 catch (final ParseException e) {
409                     webResponse_ = new NetworkErrorWebResponse(webRequest_, new IOException(e));
410                     return null;
411                 }
412             }
413         }
414 
415         return "";
416     }
417 
418     private Document buildResponseXML(final String contentType) {
419         try {
420             if (MimeType.TEXT_XML.equals(contentType)
421                     || MimeType.APPLICATION_XML.equals(contentType)
422                     || MimeType.APPLICATION_XHTML.equals(contentType)
423                     || "image/svg+xml".equals(contentType)) {
424                 final XMLDocument document = new XMLDocument();
425                 document.setParentScope(getParentScope());
426                 document.setPrototype(getPrototype(XMLDocument.class));
427                 final XmlPage page = new XmlPage(webResponse_, getWindow().getWebWindow(), false);
428                 if (!page.hasChildNodes()) {
429                     return null;
430                 }
431                 document.setDomNode(page);
432                 responseXML_ = document;
433                 return responseXML_;
434             }
435 
436             if (MimeType.TEXT_HTML.equals(contentType)) {
437                 responseXML_ = DOMParser.parseHtmlDocument(this, webResponse_, getWindow().getWebWindow());
438                 return responseXML_;
439             }
440             return null;
441         }
442         catch (final IOException e) {
443             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
444             return null;
445         }
446     }
447 
448     /**
449      * Returns a string version of the data retrieved from the server.
450      * @return a string version of the data retrieved from the server
451      */
452     @JsxGetter
453     public String getResponseText() {
454         if ((state_ == UNSENT || state_ == OPENED) && getBrowserVersion().hasFeature(XHR_RESPONSE_TEXT_EMPTY_UNSENT)) {
455             return "";
456         }
457 
458         if (!RESPONSE_TYPE_DEFAULT.equals(responseType_) && !RESPONSE_TYPE_TEXT.equals(responseType_)) {
459             throw JavaScriptEngine.asJavaScriptException(
460                     getWindow(),
461                     "InvalidStateError: Failed to read the 'responseText' property from 'XMLHttpRequest': "
462                             + "The value is only accessible if the object's 'responseType' is '' or 'text' "
463                             + "(was '" + getResponseType() + "').",
464                     DOMException.INVALID_STATE_ERR);
465         }
466 
467         if (state_ == UNSENT || state_ == OPENED) {
468             return "";
469         }
470 
471         if (webResponse_ instanceof NetworkErrorWebResponse resp) {
472             if (LOG.isDebugEnabled()) {
473                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
474                         + resp.getError() + ")");
475             }
476             if (resp.getError() instanceof NoPermittedHeaderException) {
477                 return "";
478             }
479             return null;
480         }
481 
482         if (webResponse_ != null) {
483             final Charset encoding = webResponse_.getContentCharset();
484             final String content = webResponse_.getContentAsString(encoding);
485             if (content == null) {
486                 return "";
487             }
488             return content;
489         }
490 
491         LOG.debug("XMLHttpRequest.responseText was retrieved before the response was available.");
492         return "";
493     }
494 
495     /**
496      * Returns a DOM-compatible document object version of the data retrieved from the server.
497      * @return a DOM-compatible document object version of the data retrieved from the server
498      */
499     @JsxGetter
500     public Object getResponseXML() {
501         if (responseXML_ != null) {
502             return responseXML_;
503         }
504 
505         if (webResponse_ == null) {
506             if (LOG.isDebugEnabled()) {
507                 LOG.debug("XMLHttpRequest.responseXML returns null because there "
508                         + "in no web resonse so far (has send() been called?)");
509             }
510             return null;
511         }
512 
513         if (webResponse_ instanceof NetworkErrorWebResponse response) {
514             if (LOG.isDebugEnabled()) {
515                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
516                         + response.getError() + ")");
517             }
518             return null;
519         }
520 
521         String contentType = webResponse_.getContentType();
522         if (org.htmlunit.util.StringUtils.isEmptyOrNull(contentType)) {
523             contentType = MimeType.TEXT_XML;
524         }
525 
526         if (MimeType.TEXT_HTML.equalsIgnoreCase(contentType)) {
527             if (!async_ || !RESPONSE_TYPE_DOCUMENT.equals(responseType_)) {
528                 return null;
529             }
530         }
531 
532         return buildResponseXML(contentType);
533     }
534 
535     /**
536      * Returns the numeric status returned by the server, such as 404 for "Not Found"
537      * or 200 for "OK".
538      * @return the numeric status returned by the server
539      */
540     @JsxGetter
541     public int getStatus() {
542         if (state_ == UNSENT || state_ == OPENED) {
543             return 0;
544         }
545         if (webResponse_ != null) {
546             return webResponse_.getStatusCode();
547         }
548 
549         if (LOG.isErrorEnabled()) {
550             LOG.error("XMLHttpRequest.status was retrieved without a response available (readyState: "
551                 + state_ + ").");
552         }
553         return 0;
554     }
555 
556     /**
557      * Returns the string message accompanying the status code, such as "Not Found" or "OK".
558      * @return the string message accompanying the status code
559      */
560     @JsxGetter
561     public String getStatusText() {
562         if (state_ == UNSENT || state_ == OPENED) {
563             return "";
564         }
565         if (webResponse_ != null) {
566             return webResponse_.getStatusMessage();
567         }
568 
569         if (LOG.isErrorEnabled()) {
570             LOG.error("XMLHttpRequest.statusText was retrieved without a response available (readyState: "
571                 + state_ + ").");
572         }
573         return null;
574     }
575 
576     /**
577      * Cancels the current HTTP request.
578      */
579     @JsxFunction
580     public void abort() {
581         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
582 
583         if (state_ == OPENED
584                 || state_ == HEADERS_RECEIVED
585                 || state_ == LOADING) {
586             setState(DONE);
587             webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
588             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
589             fireJavascriptEvent(Event.TYPE_ABORT);
590             fireJavascriptEvent(Event.TYPE_LOAD_END);
591         }
592 
593         // JavaScriptEngine.constructError("NetworkError",
594         //         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
595 
596         setState(UNSENT);
597         webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
598         aborted_ = true;
599     }
600 
601     /**
602      * Returns the labels and values of all the HTTP headers.
603      * @return the labels and values of all the HTTP headers
604      */
605     @JsxFunction
606     public String getAllResponseHeaders() {
607         if (state_ == UNSENT || state_ == OPENED) {
608             return "";
609         }
610         if (webResponse_ != null) {
611             final StringBuilder builder = new StringBuilder();
612             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
613                 builder
614                     .append(header.getName())
615                     .append(": ")
616                     .append(header.getValue())
617                     .append("\r\n");
618             }
619             return builder.toString();
620         }
621 
622         if (LOG.isErrorEnabled()) {
623             LOG.error("XMLHttpRequest.getAllResponseHeaders() was called without a response available (readyState: "
624                 + state_ + ").");
625         }
626         return null;
627     }
628 
629     /**
630      * Retrieves the value of an HTTP header from the response body.
631      * @param headerName the (case-insensitive) name of the header to retrieve
632      * @return the value of the specified HTTP header
633      */
634     @JsxFunction
635     public String getResponseHeader(final String headerName) {
636         if (state_ == UNSENT || state_ == OPENED) {
637             return null;
638         }
639         if (webResponse_ != null) {
640             return webResponse_.getResponseHeaderValue(headerName);
641         }
642 
643         if (LOG.isErrorEnabled()) {
644             LOG.error("XMLHttpRequest.getAllResponseHeaders(..) was called without a response available (readyState: "
645                 + state_ + ").");
646         }
647         return null;
648     }
649 
650     /**
651      * Assigns the destination URL, method and other optional attributes of a pending request.
652      * @param method the method to use to send the request to the server (GET, POST, etc)
653      * @param urlParam the URL to send the request to
654      * @param asyncParam Whether or not to send the request to the server asynchronously, defaults to {@code true}
655      * @param user If authentication is needed for the specified URL, the username to use to authenticate
656      * @param password If authentication is needed for the specified URL, the password to use to authenticate
657      */
658     @JsxFunction
659     public void open(final String method, final Object urlParam, final Object asyncParam,
660         final Object user, final Object password) {
661 
662         // async defaults to true if not specified
663         boolean async = true;
664         if (!JavaScriptEngine.isUndefined(asyncParam)) {
665             async = JavaScriptEngine.toBoolean(asyncParam);
666         }
667 
668         final String url = JavaScriptEngine.toString(urlParam);
669 
670         // (URL + Method + User + Password) become a WebRequest instance.
671         final HtmlPage containingPage = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
672 
673         try {
674             final URL pageUrl = containingPage.getUrl();
675             final URL fullUrl = containingPage.getFullyQualifiedUrl(url);
676             final WebRequest request = new WebRequest(fullUrl, getBrowserVersion().getXmlHttpRequestAcceptHeader(),
677                                                                 getBrowserVersion().getAcceptEncodingHeader());
678             request.setCharset(UTF_8);
679             // https://xhr.spec.whatwg.org/#response-body
680             request.setDefaultResponseContentCharset(UTF_8);
681             request.setRefererHeader(pageUrl);
682 
683             try {
684                 HttpMethod.validateHttpMethodName(method);
685             }
686             catch (final IllegalArgumentException e) {
687                 throw JavaScriptEngine.asJavaScriptException(
688                         getWindow(),
689                         e.getMessage(),
690                         DOMException.SYNTAX_ERR);
691             }
692 
693             final String methodUC = method.toUpperCase(Locale.ROOT);
694             if ("TRACE".equals(methodUC)) {
695                 throw JavaScriptEngine.asJavaScriptException(
696                         getWindow(),
697                         "HTTP Method '" + method + "' not allowed.",
698                         DOMException.SECURITY_ERR);
699             }
700 
701             try {
702                 request.setHttpMethod(HttpMethod.valueOf(methodUC));
703             }
704             catch (final IllegalArgumentException e) {
705                 if (LOG.isInfoEnabled()) {
706                     LOG.info("Incorrect HTTP Method '" + method + "'");
707                 }
708                 return;
709             }
710 
711             final boolean isDataUrl = "data".equals(fullUrl.getProtocol());
712             if (isDataUrl) {
713                 isSameOrigin_ = true;
714             }
715             else {
716                 isSameOrigin_ = UrlUtils.isSameOrigin(pageUrl, fullUrl);
717                 final boolean alwaysAddOrigin = HttpMethod.GET != request.getHttpMethod()
718                                                 && HttpMethod.HEAD != request.getHttpMethod();
719                 if (alwaysAddOrigin || !isSameOrigin_) {
720                     final StringBuilder origin = new StringBuilder().append(pageUrl.getProtocol()).append("://")
721                             .append(pageUrl.getHost());
722                     if (pageUrl.getPort() != -1) {
723                         origin.append(':').append(pageUrl.getPort());
724                     }
725                     request.setAdditionalHeader(HttpHeader.ORIGIN, origin.toString());
726                 }
727 
728                 // password is ignored if no user defined
729                 if (user != null && !JavaScriptEngine.isUndefined(user)) {
730                     final String userCred = user.toString();
731 
732                     String passwordCred = "";
733                     if (password != null && !JavaScriptEngine.isUndefined(password)) {
734                         passwordCred = password.toString();
735                     }
736 
737                     request.setCredentials(
738                                 new HtmlUnitUsernamePasswordCredentials(userCred, passwordCred.toCharArray()));
739                 }
740             }
741             webRequest_ = request;
742         }
743         catch (final MalformedURLException e) {
744             if (LOG.isErrorEnabled()) {
745                 LOG.error("Unable to initialize XMLHttpRequest using malformed URL '" + url + "'.");
746             }
747             return;
748         }
749 
750         // Async stays a boolean.
751         async_ = async;
752 
753         // Change the state!
754         setState(OPENED);
755         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
756     }
757 
758     /**
759      * Sends the specified content to the server in an HTTP request and receives the response.
760      * @param content the body of the message being sent with the request
761      */
762     @JsxFunction
763     public void send(final Object content) {
764         responseXML_ = null;
765 
766         if (webRequest_ == null) {
767             return;
768         }
769         if (!async_ && timeout_ > 0) {
770             throw JavaScriptEngine.throwAsScriptRuntimeEx(
771                     new RuntimeException("Synchronous requests must not set a timeout."));
772         }
773 
774         prepareRequestContent(content);
775         if (timeout_ > 0) {
776             webRequest_.setTimeout(timeout_);
777         }
778 
779         final Window w = getWindow();
780         final WebWindow ww = w.getWebWindow();
781         final WebClient client = ww.getWebClient();
782         final AjaxController ajaxController = client.getAjaxController();
783         final HtmlPage page = (HtmlPage) ww.getEnclosedPage();
784         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
785         if (synchron) {
786             doSend();
787         }
788         else {
789             // Create and start a thread in which to execute the request.
790             final HtmlUnitContextFactory cf = client.getJavaScriptEngine().getContextFactory();
791             final ContextAction<Object> action = new ContextAction<>() {
792                 @Override
793                 public Object run(final Context cx) {
794                     doSend();
795                     return null;
796                 }
797 
798                 @Override
799                 public String toString() {
800                     return "XMLHttpRequest " + webRequest_.getHttpMethod() + " '" + webRequest_.getUrl() + "'";
801                 }
802             };
803             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
804                     createJavascriptXMLHttpRequestJob(cf, action);
805             LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
806             jobID_ = ww.getJobManager().addJob(job, page);
807 
808             fireJavascriptEvent(Event.TYPE_LOAD_START);
809         }
810     }
811 
812     /**
813      * Prepares the WebRequest that will be sent.
814      * @param content the content to send
815      */
816     private void prepareRequestContent(final Object content) {
817         if (content != null
818             && (HttpMethod.POST == webRequest_.getHttpMethod()
819                     || HttpMethod.PUT == webRequest_.getHttpMethod()
820                     || HttpMethod.PATCH == webRequest_.getHttpMethod()
821                     || HttpMethod.DELETE == webRequest_.getHttpMethod()
822                     || HttpMethod.OPTIONS == webRequest_.getHttpMethod())
823             && !JavaScriptEngine.isUndefined(content)) {
824 
825             final boolean setEncodingType = webRequest_.getAdditionalHeader(HttpHeader.CONTENT_TYPE) == null;
826 
827             if (content instanceof HTMLDocument document) {
828                 // final String body = ((HTMLDocument) content).getDomNodeOrDie().asXml();
829                 String body = new XMLSerializer().serializeToString(document);
830                 if (LOG.isDebugEnabled()) {
831                     LOG.debug("Setting request body to: " + body);
832                 }
833 
834                 final Element docElement = ((Document) content).getDocumentElement();
835                 final SgmlPage page = docElement.getDomNodeOrDie().getPage();
836                 final DocumentType doctype = page.getDoctype();
837                 if (doctype != null && !StringUtils.isEmptyOrNull(doctype.getName())) {
838                     body = "<!DOCTYPE " + doctype.getName() + ">" + body;
839                 }
840 
841                 webRequest_.setRequestBody(body);
842                 if (setEncodingType) {
843                     webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=UTF-8");
844                 }
845             }
846             else if (content instanceof XMLDocument xmlDocument) {
847                 // this output differs from real browsers but it seems to be a good starting point
848                 try (StringWriter writer = new StringWriter()) {
849 
850                     final Transformer transformer = TransformerFactory.newInstance().newTransformer();
851                     transformer.setOutputProperty(OutputKeys.METHOD, "xml");
852                     transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
853                     transformer.setOutputProperty(OutputKeys.INDENT, "no");
854                     transformer.transform(
855                             new DOMSource(xmlDocument.getDomNodeOrDie().getFirstChild()), new StreamResult(writer));
856 
857                     final String body = writer.toString();
858                     if (LOG.isDebugEnabled()) {
859                         LOG.debug("Setting request body to: " + body);
860                     }
861                     webRequest_.setRequestBody(body);
862                     if (setEncodingType) {
863                         webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE,
864                                         MimeType.APPLICATION_XML + ";charset=UTF-8");
865                     }
866                 }
867                 catch (final Exception e) {
868                     throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
869                 }
870             }
871             else if (content instanceof FormData data) {
872                 data.fillRequest(webRequest_);
873             }
874             else if (content instanceof NativeArrayBufferView view) {
875                 webRequest_.setRequestBody(new String(view.getBuffer().getBuffer(), UTF_8));
876                 if (setEncodingType) {
877                     webRequest_.setEncodingType(null);
878                 }
879             }
880             else if (content instanceof URLSearchParams params) {
881                 params.fillRequest(webRequest_);
882                 webRequest_.addHint(HttpHint.IncludeCharsetInContentTypeHeader);
883             }
884             else if (content instanceof Blob blob) {
885                 blob.fillRequest(webRequest_);
886             }
887             else {
888                 final String body = JavaScriptEngine.toString(content);
889                 if (!body.isEmpty()) {
890                     if (LOG.isDebugEnabled()) {
891                         LOG.debug("Setting request body to: " + body);
892                     }
893                     webRequest_.setRequestBody(body);
894                     webRequest_.setCharset(UTF_8);
895                     if (setEncodingType) {
896                         webRequest_.setEncodingType(FormEncodingType.TEXT_PLAIN);
897                     }
898                 }
899             }
900         }
901     }
902 
903     /**
904      * The real send job.
905      */
906     void doSend() {
907         final WebClient wc = getWindow().getWebWindow().getWebClient();
908 
909         // accessing to local resource is forbidden for security reason
910         if (!wc.getOptions().isFileProtocolForXMLHttpRequestsAllowed()
911                 && "file".equals(webRequest_.getUrl().getProtocol())) {
912 
913             if (async_) {
914                 setState(DONE);
915                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
916                 fireJavascriptEvent(Event.TYPE_ERROR);
917                 fireJavascriptEvent(Event.TYPE_LOAD_END);
918             }
919 
920             if (LOG.isDebugEnabled()) {
921                 LOG.debug("Not allowed to load local resource: " + webRequest_.getUrl());
922             }
923             throw JavaScriptEngine.asJavaScriptException(
924                     getWindow(),
925                     "Not allowed to load local resource: " + webRequest_.getUrl(),
926                     DOMException.NETWORK_ERR);
927         }
928 
929         final BrowserVersion browserVersion = getBrowserVersion();
930         try {
931             if (!isSameOrigin_ && isPreflight()) {
932                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
933 
934                 // preflight request shouldn't have cookies
935                 preflightRequest.addHint(HttpHint.BlockCookies);
936 
937                 // header origin
938                 final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
939                 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
940 
941                 // header request-method
942                 preflightRequest.setAdditionalHeader(
943                         HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
944                         webRequest_.getHttpMethod().name());
945 
946                 // header request-headers
947                 final StringBuilder builder = new StringBuilder();
948                 for (final Entry<String, String> header
949                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
950                     final String name = org.htmlunit.util.StringUtils
951                                             .toRootLowerCase(header.getKey());
952                     if (isPreflightHeader(name, header.getValue())) {
953                         if (builder.length() != 0) {
954                             builder.append(',');
955                         }
956                         builder.append(name);
957                     }
958                 }
959                 preflightRequest.setAdditionalHeader(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
960                 if (timeout_ > 0) {
961                     preflightRequest.setTimeout(timeout_);
962                 }
963 
964                 // do the preflight request
965                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
966                 if (!preflightResponse.isSuccessOrUseProxyOrNotModified()
967                         || !isPreflightAuthorized(preflightResponse)) {
968                     setState(DONE);
969                     if (async_ || browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
970                         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
971                         fireJavascriptEvent(Event.TYPE_ERROR);
972                         fireJavascriptEvent(Event.TYPE_LOAD_END);
973                     }
974 
975                     if (LOG.isDebugEnabled()) {
976                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
977                     }
978                     throw JavaScriptEngine.asJavaScriptException(
979                             getWindow(),
980                             "No permitted \"Access-Control-Allow-Origin\" header.",
981                             DOMException.NETWORK_ERR);
982                 }
983             }
984 
985             if (!isSameOrigin_) {
986                 // Cookies should not be sent for cross-origin requests when withCredentials is false
987                 if (!isWithCredentials()) {
988                     webRequest_.addHint(HttpHint.BlockCookies);
989                 }
990             }
991 
992             webResponse_ = wc.loadWebResponse(webRequest_);
993             LOG.debug("Web response loaded successfully.");
994 
995             boolean allowOriginResponse = true;
996             if (!isSameOrigin_) {
997                 String value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
998                 allowOriginResponse = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(value);
999                 if (isWithCredentials()) {
1000                     // second step: check the allow-credentials header for true
1001                     value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS);
1002                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
1003                 }
1004                 else {
1005                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
1006                 }
1007             }
1008             if (allowOriginResponse) {
1009                 if (overriddenMimeType_ != null) {
1010                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
1011                     String charsetName = "";
1012                     if (index != -1) {
1013                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
1014                     }
1015 
1016                     final String charsetNameFinal = charsetName;
1017                     final Charset charset;
1018                     if (XUserDefinedCharset.NAME.equalsIgnoreCase(charsetName)) {
1019                         charset = XUserDefinedCharset.INSTANCE;
1020                     }
1021                     else {
1022                         charset = EncodingSniffer.toCharset(charsetName);
1023                     }
1024                     webResponse_ = new WebResponseWrapper(webResponse_) {
1025                         @Override
1026                         public String getContentType() {
1027                             return overriddenMimeType_;
1028                         }
1029 
1030                         @Override
1031                         public Charset getContentCharset() {
1032                             if (charsetNameFinal.isEmpty() || charset == null) {
1033                                 return super.getContentCharset();
1034                             }
1035                             return charset;
1036                         }
1037                     };
1038                 }
1039             }
1040             if (!allowOriginResponse) {
1041                 if (LOG.isDebugEnabled()) {
1042                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
1043                 }
1044                 throw new NoPermittedHeaderException("No permitted \"Access-Control-Allow-Origin\" header.");
1045             }
1046 
1047             setState(HEADERS_RECEIVED);
1048             if (async_) {
1049                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1050 
1051                 setState(LOADING);
1052                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1053                 fireJavascriptEvent(Event.TYPE_PROGRESS);
1054             }
1055 
1056             setState(DONE);
1057             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1058 
1059             if (!async_ && aborted_
1060                     && browserVersion.hasFeature(XHR_SEND_NETWORK_ERROR_IF_ABORTED)) {
1061                 throw JavaScriptEngine.constructError("Error",
1062                         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
1063             }
1064 
1065             if (browserVersion.hasFeature(XHR_LOAD_ALWAYS_AFTER_DONE)) {
1066                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD);
1067                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD_END);
1068             }
1069             else {
1070                 fireJavascriptEvent(Event.TYPE_LOAD);
1071                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1072             }
1073         }
1074         catch (final IOException e) {
1075             LOG.debug("IOException: returning a network error response.", e);
1076 
1077             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
1078             if (async_) {
1079                 setState(DONE);
1080                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1081                 if (e instanceof SocketTimeoutException) {
1082                     fireJavascriptEvent(Event.TYPE_TIMEOUT);
1083                 }
1084                 else {
1085                     fireJavascriptEvent(Event.TYPE_ERROR);
1086                 }
1087                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1088             }
1089             else {
1090                 setState(DONE);
1091                 if (browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
1092                     fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1093                     if (e instanceof SocketTimeoutException) {
1094                         fireJavascriptEvent(Event.TYPE_TIMEOUT);
1095                     }
1096                     else {
1097                         fireJavascriptEvent(Event.TYPE_ERROR);
1098                     }
1099                     fireJavascriptEvent(Event.TYPE_LOAD_END);
1100                 }
1101 
1102                 throw JavaScriptEngine.asJavaScriptException(getWindow(),
1103                         e.getMessage(), DOMException.NETWORK_ERR);
1104             }
1105         }
1106     }
1107 
1108     private boolean isPreflight() {
1109         final HttpMethod method = webRequest_.getHttpMethod();
1110         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
1111             return true;
1112         }
1113         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1114             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
1115                 return true;
1116             }
1117         }
1118         return false;
1119     }
1120 
1121     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
1122         final String originHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
1123         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
1124                 && !webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(originHeader)) {
1125             return false;
1126         }
1127 
1128         // there is no test case for this because the servlet API has no support
1129         // for adding the same header twice
1130         final HashSet<String> accessControlValues = new HashSet<>();
1131         for (final NameValuePair pair : preflightResponse.getResponseHeaders()) {
1132             if (HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS.equalsIgnoreCase(pair.getName())) {
1133                 String value = pair.getValue();
1134                 if (value != null) {
1135                     if (ALLOW_ORIGIN_ALL.equals(value)) {
1136                         // all headers are allowed
1137                         return true;
1138                     }
1139                     value = org.htmlunit.util.StringUtils.toRootLowerCase(value);
1140                     final String[] values = org.htmlunit.util.StringUtils.splitAtComma(value);
1141                     for (String part : values) {
1142                         part = part.trim();
1143                         if (!org.htmlunit.util.StringUtils.isEmptyOrNull(part)) {
1144                             accessControlValues.add(part);
1145                         }
1146                     }
1147                 }
1148             }
1149         }
1150 
1151         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1152             final String key = org.htmlunit.util.StringUtils.toRootLowerCase(header.getKey());
1153             if (isPreflightHeader(key, header.getValue())
1154                     && !accessControlValues.contains(key)) {
1155                 return false;
1156             }
1157         }
1158         return true;
1159     }
1160 
1161     /**
1162      * @param name header name (MUST be lower-case for performance reasons)
1163      * @param value header value
1164      */
1165     private static boolean isPreflightHeader(final String name, final String value) {
1166         if (HttpHeader.CONTENT_TYPE_LC.equals(name)) {
1167             final String lcValue = value.toLowerCase(Locale.ROOT);
1168             return !lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
1169                     && !lcValue.startsWith(FormEncodingType.MULTIPART.getName())
1170                     && !lcValue.startsWith(FormEncodingType.TEXT_PLAIN.getName());
1171         }
1172         if (HttpHeader.ACCEPT_LC.equals(name)
1173                 || HttpHeader.ACCEPT_LANGUAGE_LC.equals(name)
1174                 || HttpHeader.CONTENT_LANGUAGE_LC.equals(name)
1175                 || HttpHeader.REFERER_LC.equals(name)
1176                 || "accept-encoding".equals(name)
1177                 || HttpHeader.ORIGIN_LC.equals(name)) {
1178             return false;
1179         }
1180         return true;
1181     }
1182 
1183     /**
1184      * Sets the specified header to the specified value. The <code>open</code> method must be
1185      * called before this method, or an error will occur.
1186      * @param name the name of the header being set
1187      * @param value the value of the header being set
1188      */
1189     @JsxFunction
1190     public void setRequestHeader(final String name, final String value) {
1191         if (!isAuthorizedHeader(name)) {
1192             if (LOG.isWarnEnabled()) {
1193                 LOG.warn("Ignoring XMLHttpRequest.setRequestHeader for " + name
1194                     + ": it is a restricted header");
1195             }
1196             return;
1197         }
1198 
1199         if (webRequest_ != null) {
1200             webRequest_.setAdditionalHeader(name, value);
1201         }
1202         else {
1203             throw JavaScriptEngine.asJavaScriptException(
1204                     getWindow(),
1205                     "The open() method must be called before setRequestHeader().",
1206                     DOMException.INVALID_STATE_ERR);
1207         }
1208     }
1209 
1210     /**
1211      * Not all request headers can be set from JavaScript.
1212      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
1213      * @param name the header name
1214      * @return {@code true} if the header can be set from JavaScript
1215      */
1216     static boolean isAuthorizedHeader(final String name) {
1217         final String nameLowerCase = org.htmlunit.util.StringUtils.toRootLowerCase(name);
1218         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
1219             return false;
1220         }
1221         if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
1222             return false;
1223         }
1224         return true;
1225     }
1226 
1227     /**
1228      * Override the mime type returned by the server (if any). This may be used, for example, to force a stream
1229      * to be treated and parsed as text/xml, even if the server does not report it as such.
1230      * This must be done before the send method is invoked.
1231      * @param mimeType the type used to override that returned by the server (if any)
1232      * @see <a href="http://xulplanet.com/references/objref/XMLHttpRequest.html#method_overrideMimeType">XUL Planet</a>
1233      */
1234     @JsxFunction
1235     public void overrideMimeType(final String mimeType) {
1236         if (state_ != UNSENT && state_ != OPENED) {
1237             throw JavaScriptEngine.asJavaScriptException(
1238                     getWindow(),
1239                     "Property 'overrideMimeType' not writable after sent.",
1240                     DOMException.INVALID_STATE_ERR);
1241         }
1242         overriddenMimeType_ = mimeType;
1243     }
1244 
1245     /**
1246      * Returns the {@code withCredentials} property.
1247      * @return the {@code withCredentials} property
1248      */
1249     @JsxGetter
1250     public boolean isWithCredentials() {
1251         return withCredentials_;
1252     }
1253 
1254     /**
1255      * Sets the {@code withCredentials} property.
1256      * @param withCredentials the {@code withCredentials} property.
1257      */
1258     @JsxSetter
1259     public void setWithCredentials(final boolean withCredentials) {
1260         withCredentials_ = withCredentials;
1261     }
1262 
1263     /**
1264      * Returns the {@code upload} property.
1265      * @return the {@code upload} property
1266      */
1267     @JsxGetter
1268     public XMLHttpRequestUpload getUpload() {
1269         final XMLHttpRequestUpload upload = new XMLHttpRequestUpload();
1270         upload.setParentScope(getParentScope());
1271         upload.setPrototype(getPrototype(upload.getClass()));
1272         return upload;
1273     }
1274 
1275     /**
1276      * {@inheritDoc}
1277      */
1278     @JsxGetter
1279     @Override
1280     public Function getOnreadystatechange() {
1281         return super.getOnreadystatechange();
1282     }
1283 
1284     /**
1285      * {@inheritDoc}
1286      */
1287     @JsxSetter
1288     @Override
1289     public void setOnreadystatechange(final Function readyStateChangeHandler) {
1290         super.setOnreadystatechange(readyStateChangeHandler);
1291     }
1292 
1293     /**
1294      * @return the number of milliseconds a request can take before automatically being terminated.
1295      *         The default value is 0, which means there is no timeout.
1296      */
1297     @JsxGetter
1298     public int getTimeout() {
1299         return timeout_;
1300     }
1301 
1302     /**
1303      * Sets the number of milliseconds a request can take before automatically being terminated.
1304      * @param timeout the timeout in milliseconds
1305      */
1306     @JsxSetter
1307     public void setTimeout(final int timeout) {
1308         timeout_ = timeout;
1309     }
1310 
1311     private static final class NetworkErrorWebResponse extends WebResponse {
1312         private final WebRequest request_;
1313         private final IOException error_;
1314 
1315         NetworkErrorWebResponse(final WebRequest webRequest, final IOException error) {
1316             super(null, null, 0);
1317             request_ = webRequest;
1318             error_ = error;
1319         }
1320 
1321         @Override
1322         public int getStatusCode() {
1323             return 0;
1324         }
1325 
1326         @Override
1327         public String getStatusMessage() {
1328             return "";
1329         }
1330 
1331         @Override
1332         public String getContentType() {
1333             return "";
1334         }
1335 
1336         @Override
1337         public String getContentAsString() {
1338             return "";
1339         }
1340 
1341         @Override
1342         public InputStream getContentAsStream() {
1343             return null;
1344         }
1345 
1346         @Override
1347         public List<NameValuePair> getResponseHeaders() {
1348             return Collections.emptyList();
1349         }
1350 
1351         @Override
1352         public String getResponseHeaderValue(final String headerName) {
1353             return "";
1354         }
1355 
1356         @Override
1357         public long getLoadTime() {
1358             return 0;
1359         }
1360 
1361         @Override
1362         public Charset getContentCharset() {
1363             return null;
1364         }
1365 
1366         @Override
1367         public WebRequest getWebRequest() {
1368             return request_;
1369         }
1370 
1371         /**
1372          * @return the error
1373          */
1374         public IOException getError() {
1375             return error_;
1376         }
1377     }
1378 
1379     private static final class NoPermittedHeaderException extends IOException {
1380         NoPermittedHeaderException(final String msg) {
1381             super(msg);
1382         }
1383     }
1384 }