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 static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.XHR_ALL_RESPONSE_HEADERS_SEPARATE_BY_LF;
19  import static org.htmlunit.BrowserVersionFeatures.XHR_HANDLE_SYNC_NETWORK_ERRORS;
20  import static org.htmlunit.BrowserVersionFeatures.XHR_LOAD_ALWAYS_AFTER_DONE;
21  import static org.htmlunit.BrowserVersionFeatures.XHR_RESPONSE_TEXT_EMPTY_UNSENT;
22  import static org.htmlunit.BrowserVersionFeatures.XHR_SEND_NETWORK_ERROR_IF_ABORTED;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.StringWriter;
27  import java.net.MalformedURLException;
28  import java.net.SocketTimeoutException;
29  import java.net.URL;
30  import java.nio.charset.Charset;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map.Entry;
37  import java.util.TreeMap;
38  
39  import javax.xml.transform.OutputKeys;
40  import javax.xml.transform.Transformer;
41  import javax.xml.transform.TransformerFactory;
42  import javax.xml.transform.dom.DOMSource;
43  import javax.xml.transform.stream.StreamResult;
44  
45  import org.apache.commons.io.IOUtils;
46  import org.apache.commons.lang3.StringUtils;
47  import org.apache.commons.logging.Log;
48  import org.apache.commons.logging.LogFactory;
49  import org.htmlunit.AjaxController;
50  import org.htmlunit.BrowserVersion;
51  import org.htmlunit.FormEncodingType;
52  import org.htmlunit.HttpHeader;
53  import org.htmlunit.HttpMethod;
54  import org.htmlunit.SgmlPage;
55  import org.htmlunit.WebClient;
56  import org.htmlunit.WebRequest;
57  import org.htmlunit.WebRequest.HttpHint;
58  import org.htmlunit.WebResponse;
59  import org.htmlunit.WebWindow;
60  import org.htmlunit.corejs.javascript.Context;
61  import org.htmlunit.corejs.javascript.ContextAction;
62  import org.htmlunit.corejs.javascript.Function;
63  import org.htmlunit.corejs.javascript.ScriptableObject;
64  import org.htmlunit.corejs.javascript.json.JsonParser;
65  import org.htmlunit.corejs.javascript.json.JsonParser.ParseException;
66  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
67  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
68  import org.htmlunit.html.HtmlPage;
69  import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
70  import org.htmlunit.javascript.HtmlUnitContextFactory;
71  import org.htmlunit.javascript.JavaScriptEngine;
72  import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
73  import org.htmlunit.javascript.background.JavaScriptJob;
74  import org.htmlunit.javascript.configuration.JsxClass;
75  import org.htmlunit.javascript.configuration.JsxConstant;
76  import org.htmlunit.javascript.configuration.JsxConstructor;
77  import org.htmlunit.javascript.configuration.JsxFunction;
78  import org.htmlunit.javascript.configuration.JsxGetter;
79  import org.htmlunit.javascript.configuration.JsxSetter;
80  import org.htmlunit.javascript.host.Element;
81  import org.htmlunit.javascript.host.URLSearchParams;
82  import org.htmlunit.javascript.host.Window;
83  import org.htmlunit.javascript.host.dom.DOMException;
84  import org.htmlunit.javascript.host.dom.DOMParser;
85  import org.htmlunit.javascript.host.dom.Document;
86  import org.htmlunit.javascript.host.event.Event;
87  import org.htmlunit.javascript.host.event.ProgressEvent;
88  import org.htmlunit.javascript.host.file.Blob;
89  import org.htmlunit.javascript.host.html.HTMLDocument;
90  import org.htmlunit.util.EncodingSniffer;
91  import org.htmlunit.util.MimeType;
92  import org.htmlunit.util.NameValuePair;
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) {
317             if (LOG.isDebugEnabled()) {
318                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
319                         + ((NetworkErrorWebResponse) webResponse_).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(getWindow(), 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(getWindow(), 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 (StringUtils.isEmpty(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(), this).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) {
472             if (LOG.isDebugEnabled()) {
473                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
474                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
475             }
476 
477             final NetworkErrorWebResponse resp = (NetworkErrorWebResponse) webResponse_;
478             if (resp.getError() instanceof NoPermittedHeaderException) {
479                 return "";
480             }
481             return null;
482         }
483 
484         if (webResponse_ != null) {
485             final Charset encoding = webResponse_.getContentCharset();
486             final String content = webResponse_.getContentAsString(encoding);
487             if (content == null) {
488                 return "";
489             }
490             return content;
491         }
492 
493         LOG.debug("XMLHttpRequest.responseText was retrieved before the response was available.");
494         return "";
495     }
496 
497     /**
498      * Returns a DOM-compatible document object version of the data retrieved from the server.
499      * @return a DOM-compatible document object version of the data retrieved from the server
500      */
501     @JsxGetter
502     public Object getResponseXML() {
503         if (responseXML_ != null) {
504             return responseXML_;
505         }
506 
507         if (webResponse_ == null) {
508             if (LOG.isDebugEnabled()) {
509                 LOG.debug("XMLHttpRequest.responseXML returns null because there "
510                         + "in no web resonse so far (has send() been called?)");
511             }
512             return null;
513         }
514 
515         if (webResponse_ instanceof NetworkErrorWebResponse) {
516             if (LOG.isDebugEnabled()) {
517                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
518                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
519             }
520             return null;
521         }
522 
523         String contentType = webResponse_.getContentType();
524         if (StringUtils.isEmpty(contentType)) {
525             contentType = MimeType.TEXT_XML;
526         }
527 
528         if (MimeType.TEXT_HTML.equalsIgnoreCase(contentType)) {
529             if (!async_ || !RESPONSE_TYPE_DOCUMENT.equals(responseType_)) {
530                 return null;
531             }
532         }
533 
534         return buildResponseXML(contentType);
535     }
536 
537     /**
538      * Returns the numeric status returned by the server, such as 404 for "Not Found"
539      * or 200 for "OK".
540      * @return the numeric status returned by the server
541      */
542     @JsxGetter
543     public int getStatus() {
544         if (state_ == UNSENT || state_ == OPENED) {
545             return 0;
546         }
547         if (webResponse_ != null) {
548             return webResponse_.getStatusCode();
549         }
550 
551         if (LOG.isErrorEnabled()) {
552             LOG.error("XMLHttpRequest.status was retrieved without a response available (readyState: "
553                 + state_ + ").");
554         }
555         return 0;
556     }
557 
558     /**
559      * Returns the string message accompanying the status code, such as "Not Found" or "OK".
560      * @return the string message accompanying the status code
561      */
562     @JsxGetter
563     public String getStatusText() {
564         if (state_ == UNSENT || state_ == OPENED) {
565             return "";
566         }
567         if (webResponse_ != null) {
568             return webResponse_.getStatusMessage();
569         }
570 
571         if (LOG.isErrorEnabled()) {
572             LOG.error("XMLHttpRequest.statusText was retrieved without a response available (readyState: "
573                 + state_ + ").");
574         }
575         return null;
576     }
577 
578     /**
579      * Cancels the current HTTP request.
580      */
581     @JsxFunction
582     public void abort() {
583         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
584 
585         if (state_ == OPENED
586                 || state_ == HEADERS_RECEIVED
587                 || state_ == LOADING) {
588             setState(DONE);
589             webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
590             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
591             fireJavascriptEvent(Event.TYPE_ABORT);
592             fireJavascriptEvent(Event.TYPE_LOAD_END);
593         }
594 
595         // JavaScriptEngine.constructError("NetworkError",
596         //         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
597 
598         setState(UNSENT);
599         webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
600         aborted_ = true;
601     }
602 
603     /**
604      * Returns the labels and values of all the HTTP headers.
605      * @return the labels and values of all the HTTP headers
606      */
607     @JsxFunction
608     public String getAllResponseHeaders() {
609         if (state_ == UNSENT || state_ == OPENED) {
610             return "";
611         }
612         if (webResponse_ != null) {
613             final StringBuilder builder = new StringBuilder();
614             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
615                 builder.append(header.getName()).append(": ").append(header.getValue());
616 
617                 if (!getBrowserVersion().hasFeature(XHR_ALL_RESPONSE_HEADERS_SEPARATE_BY_LF)) {
618                     builder.append('\r');
619                 }
620                 builder.append('\n');
621             }
622             return builder.toString();
623         }
624 
625         if (LOG.isErrorEnabled()) {
626             LOG.error("XMLHttpRequest.getAllResponseHeaders() was called without a response available (readyState: "
627                 + state_ + ").");
628         }
629         return null;
630     }
631 
632     /**
633      * Retrieves the value of an HTTP header from the response body.
634      * @param headerName the (case-insensitive) name of the header to retrieve
635      * @return the value of the specified HTTP header
636      */
637     @JsxFunction
638     public String getResponseHeader(final String headerName) {
639         if (state_ == UNSENT || state_ == OPENED) {
640             return null;
641         }
642         if (webResponse_ != null) {
643             return webResponse_.getResponseHeaderValue(headerName);
644         }
645 
646         if (LOG.isErrorEnabled()) {
647             LOG.error("XMLHttpRequest.getAllResponseHeaders(..) was called without a response available (readyState: "
648                 + state_ + ").");
649         }
650         return null;
651     }
652 
653     /**
654      * Assigns the destination URL, method and other optional attributes of a pending request.
655      * @param method the method to use to send the request to the server (GET, POST, etc)
656      * @param urlParam the URL to send the request to
657      * @param asyncParam Whether or not to send the request to the server asynchronously, defaults to {@code true}
658      * @param user If authentication is needed for the specified URL, the username to use to authenticate
659      * @param password If authentication is needed for the specified URL, the password to use to authenticate
660      */
661     @JsxFunction
662     public void open(final String method, final Object urlParam, final Object asyncParam,
663         final Object user, final Object password) {
664 
665         // async defaults to true if not specified
666         boolean async = true;
667         if (!JavaScriptEngine.isUndefined(asyncParam)) {
668             async = JavaScriptEngine.toBoolean(asyncParam);
669         }
670 
671         final String url = JavaScriptEngine.toString(urlParam);
672 
673         // (URL + Method + User + Password) become a WebRequest instance.
674         final HtmlPage containingPage = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
675 
676         try {
677             final URL pageUrl = containingPage.getUrl();
678             final URL fullUrl = containingPage.getFullyQualifiedUrl(url);
679             final WebRequest request = new WebRequest(fullUrl, getBrowserVersion().getXmlHttpRequestAcceptHeader(),
680                                                                 getBrowserVersion().getAcceptEncodingHeader());
681             request.setCharset(UTF_8);
682             // https://xhr.spec.whatwg.org/#response-body
683             request.setDefaultResponseContentCharset(UTF_8);
684             request.setRefererHeader(pageUrl);
685 
686             try {
687                 request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
688             }
689             catch (final IllegalArgumentException e) {
690                 if (LOG.isInfoEnabled()) {
691                     LOG.info("Incorrect HTTP Method '" + method + "'");
692                 }
693                 return;
694             }
695 
696             isSameOrigin_ = isSameOrigin(pageUrl, fullUrl);
697             final boolean alwaysAddOrigin = HttpMethod.GET != request.getHttpMethod()
698                                             && HttpMethod.PATCH != request.getHttpMethod()
699                                             && HttpMethod.HEAD != request.getHttpMethod();
700             if (alwaysAddOrigin || !isSameOrigin_) {
701                 final StringBuilder origin = new StringBuilder().append(pageUrl.getProtocol()).append("://")
702                         .append(pageUrl.getHost());
703                 if (pageUrl.getPort() != -1) {
704                     origin.append(':').append(pageUrl.getPort());
705                 }
706                 request.setAdditionalHeader(HttpHeader.ORIGIN, origin.toString());
707             }
708 
709             // password is ignored if no user defined
710             if (user != null && !JavaScriptEngine.isUndefined(user)) {
711                 final String userCred = user.toString();
712 
713                 String passwordCred = "";
714                 if (password != null && !JavaScriptEngine.isUndefined(password)) {
715                     passwordCred = password.toString();
716                 }
717 
718                 request.setCredentials(new HtmlUnitUsernamePasswordCredentials(userCred, passwordCred.toCharArray()));
719             }
720             webRequest_ = request;
721         }
722         catch (final MalformedURLException e) {
723             if (LOG.isErrorEnabled()) {
724                 LOG.error("Unable to initialize XMLHttpRequest using malformed URL '" + url + "'.");
725             }
726             return;
727         }
728 
729         // Async stays a boolean.
730         async_ = async;
731 
732         // Change the state!
733         setState(OPENED);
734         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
735     }
736 
737     private static boolean isSameOrigin(final URL originUrl, final URL newUrl) {
738         if (!originUrl.getHost().equals(newUrl.getHost())) {
739             return false;
740         }
741 
742         int originPort = originUrl.getPort();
743         if (originPort == -1) {
744             originPort = originUrl.getDefaultPort();
745         }
746         int newPort = newUrl.getPort();
747         if (newPort == -1) {
748             newPort = newUrl.getDefaultPort();
749         }
750         return originPort == newPort;
751     }
752 
753     /**
754      * Sends the specified content to the server in an HTTP request and receives the response.
755      * @param content the body of the message being sent with the request
756      */
757     @JsxFunction
758     public void send(final Object content) {
759         responseXML_ = null;
760 
761         if (webRequest_ == null) {
762             return;
763         }
764         if (!async_ && timeout_ > 0) {
765             throw JavaScriptEngine.throwAsScriptRuntimeEx(
766                     new RuntimeException("Synchronous requests must not set a timeout."));
767         }
768 
769         prepareRequestContent(content);
770         if (timeout_ > 0) {
771             webRequest_.setTimeout(timeout_);
772         }
773 
774         final Window w = getWindow();
775         final WebWindow ww = w.getWebWindow();
776         final WebClient client = ww.getWebClient();
777         final AjaxController ajaxController = client.getAjaxController();
778         final HtmlPage page = (HtmlPage) ww.getEnclosedPage();
779         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
780         if (synchron) {
781             doSend();
782         }
783         else {
784             // Create and start a thread in which to execute the request.
785             final HtmlUnitContextFactory cf = client.getJavaScriptEngine().getContextFactory();
786             final ContextAction<Object> action = new ContextAction<Object>() {
787                 @Override
788                 public Object run(final Context cx) {
789                     doSend();
790                     return null;
791                 }
792 
793                 @Override
794                 public String toString() {
795                     return "XMLHttpRequest " + webRequest_.getHttpMethod() + " '" + webRequest_.getUrl() + "'";
796                 }
797             };
798             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
799                     createJavascriptXMLHttpRequestJob(cf, action);
800             LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
801             jobID_ = ww.getJobManager().addJob(job, page);
802 
803             fireJavascriptEvent(Event.TYPE_LOAD_START);
804         }
805     }
806 
807     /**
808      * Prepares the WebRequest that will be sent.
809      * @param content the content to send
810      */
811     private void prepareRequestContent(final Object content) {
812         if (content != null
813             && (HttpMethod.POST == webRequest_.getHttpMethod()
814                     || HttpMethod.PUT == webRequest_.getHttpMethod()
815                     || HttpMethod.PATCH == webRequest_.getHttpMethod()
816                     || HttpMethod.DELETE == webRequest_.getHttpMethod()
817                     || HttpMethod.OPTIONS == webRequest_.getHttpMethod())
818             && !JavaScriptEngine.isUndefined(content)) {
819 
820             final boolean setEncodingType = webRequest_.getAdditionalHeader(HttpHeader.CONTENT_TYPE) == null;
821 
822             if (content instanceof HTMLDocument) {
823                 // final String body = ((HTMLDocument) content).getDomNodeOrDie().asXml();
824                 String body = new XMLSerializer().serializeToString((HTMLDocument) content);
825                 if (LOG.isDebugEnabled()) {
826                     LOG.debug("Setting request body to: " + body);
827                 }
828 
829                 final Element docElement = ((Document) content).getDocumentElement();
830                 final SgmlPage page = docElement.getDomNodeOrDie().getPage();
831                 final DocumentType doctype = page.getDoctype();
832                 if (doctype != null && StringUtils.isNotEmpty(doctype.getName())) {
833                     body = "<!DOCTYPE " + doctype.getName() + ">" + body;
834                 }
835 
836                 webRequest_.setRequestBody(body);
837                 if (setEncodingType) {
838                     webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=UTF-8");
839                 }
840             }
841             else if (content instanceof XMLDocument) {
842                 // this output differs from real browsers but it seems to be a good starting point
843                 try (StringWriter writer = new StringWriter()) {
844                     final XMLDocument xmlDocument = (XMLDocument) content;
845 
846                     final Transformer transformer = TransformerFactory.newInstance().newTransformer();
847                     transformer.setOutputProperty(OutputKeys.METHOD, "xml");
848                     transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
849                     transformer.setOutputProperty(OutputKeys.INDENT, "no");
850                     transformer.transform(
851                             new DOMSource(xmlDocument.getDomNodeOrDie().getFirstChild()), new StreamResult(writer));
852 
853                     final String body = writer.toString();
854                     if (LOG.isDebugEnabled()) {
855                         LOG.debug("Setting request body to: " + body);
856                     }
857                     webRequest_.setRequestBody(body);
858                     if (setEncodingType) {
859                         webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE,
860                                         MimeType.APPLICATION_XML + ";charset=UTF-8");
861                     }
862                 }
863                 catch (final Exception e) {
864                     throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
865                 }
866             }
867             else if (content instanceof FormData) {
868                 ((FormData) content).fillRequest(webRequest_);
869             }
870             else if (content instanceof NativeArrayBufferView) {
871                 final NativeArrayBufferView view = (NativeArrayBufferView) content;
872                 webRequest_.setRequestBody(new String(view.getBuffer().getBuffer(), UTF_8));
873                 if (setEncodingType) {
874                     webRequest_.setEncodingType(null);
875                 }
876             }
877             else if (content instanceof URLSearchParams) {
878                 ((URLSearchParams) content).fillRequest(webRequest_);
879                 webRequest_.addHint(HttpHint.IncludeCharsetInContentTypeHeader);
880             }
881             else if (content instanceof Blob) {
882                 ((Blob) content).fillRequest(webRequest_);
883             }
884             else {
885                 final String body = JavaScriptEngine.toString(content);
886                 if (!body.isEmpty()) {
887                     if (LOG.isDebugEnabled()) {
888                         LOG.debug("Setting request body to: " + body);
889                     }
890                     webRequest_.setRequestBody(body);
891                     webRequest_.setCharset(UTF_8);
892                     if (setEncodingType) {
893                         webRequest_.setEncodingType(FormEncodingType.TEXT_PLAIN);
894                     }
895                 }
896             }
897         }
898     }
899 
900     /**
901      * The real send job.
902      */
903     void doSend() {
904         final WebClient wc = getWindow().getWebWindow().getWebClient();
905 
906         // accessing to local resource is forbidden for security reason
907         if (!wc.getOptions().isFileProtocolForXMLHttpRequestsAllowed()
908                 && "file".equals(webRequest_.getUrl().getProtocol())) {
909 
910             if (async_) {
911                 setState(DONE);
912                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
913                 fireJavascriptEvent(Event.TYPE_ERROR);
914                 fireJavascriptEvent(Event.TYPE_LOAD_END);
915             }
916 
917             if (LOG.isDebugEnabled()) {
918                 LOG.debug("Not allowed to load local resource: " + webRequest_.getUrl());
919             }
920             throw JavaScriptEngine.asJavaScriptException(
921                     getWindow(),
922                     "Not allowed to load local resource: " + webRequest_.getUrl(),
923                     DOMException.NETWORK_ERR);
924         }
925 
926         final BrowserVersion browserVersion = getBrowserVersion();
927         try {
928             if (!isSameOrigin_ && isPreflight()) {
929                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
930 
931                 // preflight request shouldn't have cookies
932                 preflightRequest.addHint(HttpHint.BlockCookies);
933 
934                 // header origin
935                 final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
936                 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
937 
938                 // header request-method
939                 preflightRequest.setAdditionalHeader(
940                         HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
941                         webRequest_.getHttpMethod().name());
942 
943                 // header request-headers
944                 final StringBuilder builder = new StringBuilder();
945                 for (final Entry<String, String> header
946                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
947                     final String name = org.htmlunit.util.StringUtils
948                                             .toRootLowerCase(header.getKey());
949                     if (isPreflightHeader(name, header.getValue())) {
950                         if (builder.length() != 0) {
951                             builder.append(',');
952                         }
953                         builder.append(name);
954                     }
955                 }
956                 preflightRequest.setAdditionalHeader(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
957                 if (timeout_ > 0) {
958                     preflightRequest.setTimeout(timeout_);
959                 }
960 
961                 // do the preflight request
962                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
963                 if (!preflightResponse.isSuccessOrUseProxyOrNotModified()
964                         || !isPreflightAuthorized(preflightResponse)) {
965                     setState(DONE);
966                     if (async_ || browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
967                         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
968                         fireJavascriptEvent(Event.TYPE_ERROR);
969                         fireJavascriptEvent(Event.TYPE_LOAD_END);
970                     }
971 
972                     if (LOG.isDebugEnabled()) {
973                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
974                     }
975                     throw JavaScriptEngine.asJavaScriptException(
976                             getWindow(),
977                             "No permitted \"Access-Control-Allow-Origin\" header.",
978                             DOMException.NETWORK_ERR);
979                 }
980             }
981 
982             if (!isSameOrigin_) {
983                 // Cookies should not be sent for cross-origin requests when withCredentials is false
984                 if (!isWithCredentials()) {
985                     webRequest_.addHint(HttpHint.BlockCookies);
986                 }
987             }
988 
989             webResponse_ = wc.loadWebResponse(webRequest_);
990             LOG.debug("Web response loaded successfully.");
991 
992             boolean allowOriginResponse = true;
993             if (!isSameOrigin_) {
994                 String value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
995                 allowOriginResponse = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(value);
996                 if (isWithCredentials()) {
997                     // second step: check the allow-credentials header for true
998                     value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS);
999                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
1000                 }
1001                 else {
1002                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
1003                 }
1004             }
1005             if (allowOriginResponse) {
1006                 if (overriddenMimeType_ != null) {
1007                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
1008                     String charsetName = "";
1009                     if (index != -1) {
1010                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
1011                     }
1012 
1013                     final String charsetNameFinal = charsetName;
1014                     final Charset charset;
1015                     if (XUserDefinedCharset.NAME.equalsIgnoreCase(charsetName)) {
1016                         charset = XUserDefinedCharset.INSTANCE;
1017                     }
1018                     else {
1019                         charset = EncodingSniffer.toCharset(charsetName);
1020                     }
1021                     webResponse_ = new WebResponseWrapper(webResponse_) {
1022                         @Override
1023                         public String getContentType() {
1024                             return overriddenMimeType_;
1025                         }
1026 
1027                         @Override
1028                         public Charset getContentCharset() {
1029                             if (charsetNameFinal.isEmpty() || charset == null) {
1030                                 return super.getContentCharset();
1031                             }
1032                             return charset;
1033                         }
1034                     };
1035                 }
1036             }
1037             if (!allowOriginResponse) {
1038                 if (LOG.isDebugEnabled()) {
1039                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
1040                 }
1041                 throw new NoPermittedHeaderException("No permitted \"Access-Control-Allow-Origin\" header.");
1042             }
1043 
1044             setState(HEADERS_RECEIVED);
1045             if (async_) {
1046                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1047 
1048                 setState(LOADING);
1049                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1050                 fireJavascriptEvent(Event.TYPE_PROGRESS);
1051             }
1052 
1053             setState(DONE);
1054             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1055 
1056             if (!async_ && aborted_
1057                     && browserVersion.hasFeature(XHR_SEND_NETWORK_ERROR_IF_ABORTED)) {
1058                 throw JavaScriptEngine.constructError("Error",
1059                         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
1060             }
1061 
1062             if (browserVersion.hasFeature(XHR_LOAD_ALWAYS_AFTER_DONE)) {
1063                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD);
1064                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD_END);
1065             }
1066             else {
1067                 fireJavascriptEvent(Event.TYPE_LOAD);
1068                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1069             }
1070         }
1071         catch (final IOException e) {
1072             LOG.debug("IOException: returning a network error response.", e);
1073 
1074             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
1075             if (async_) {
1076                 setState(DONE);
1077                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1078                 if (e instanceof SocketTimeoutException) {
1079                     fireJavascriptEvent(Event.TYPE_TIMEOUT);
1080                 }
1081                 else {
1082                     fireJavascriptEvent(Event.TYPE_ERROR);
1083                 }
1084                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1085             }
1086             else {
1087                 setState(DONE);
1088                 if (browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
1089                     fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1090                     if (e instanceof SocketTimeoutException) {
1091                         fireJavascriptEvent(Event.TYPE_TIMEOUT);
1092                     }
1093                     else {
1094                         fireJavascriptEvent(Event.TYPE_ERROR);
1095                     }
1096                     fireJavascriptEvent(Event.TYPE_LOAD_END);
1097                 }
1098 
1099                 throw JavaScriptEngine.asJavaScriptException(getWindow(),
1100                         e.getMessage(), DOMException.NETWORK_ERR);
1101             }
1102         }
1103     }
1104 
1105     private boolean isPreflight() {
1106         final HttpMethod method = webRequest_.getHttpMethod();
1107         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
1108             return true;
1109         }
1110         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1111             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
1112                 return true;
1113             }
1114         }
1115         return false;
1116     }
1117 
1118     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
1119         final String originHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
1120         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
1121                 && !webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(originHeader)) {
1122             return false;
1123         }
1124 
1125         // there is no test case for this because the servlet API has no support
1126         // for adding the same header twice
1127         final HashSet<String> accessControlValues = new HashSet<>();
1128         for (final NameValuePair pair : preflightResponse.getResponseHeaders()) {
1129             if (HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS.equalsIgnoreCase(pair.getName())) {
1130                 String value = pair.getValue();
1131                 if (value != null) {
1132                     value = org.htmlunit.util.StringUtils.toRootLowerCase(value);
1133                     final String[] values = org.htmlunit.util.StringUtils.splitAtComma(value);
1134                     for (String part : values) {
1135                         part = part.trim();
1136                         if (StringUtils.isNotEmpty(part)) {
1137                             accessControlValues.add(part);
1138                         }
1139                     }
1140                 }
1141             }
1142         }
1143 
1144         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1145             final String key = org.htmlunit.util.StringUtils.toRootLowerCase(header.getKey());
1146             if (isPreflightHeader(key, header.getValue())
1147                     && !accessControlValues.contains(key)) {
1148                 return false;
1149             }
1150         }
1151         return true;
1152     }
1153 
1154     /**
1155      * @param name header name (MUST be lower-case for performance reasons)
1156      * @param value header value
1157      */
1158     private static boolean isPreflightHeader(final String name, final String value) {
1159         if (HttpHeader.CONTENT_TYPE_LC.equals(name)) {
1160             final String lcValue = value.toLowerCase(Locale.ROOT);
1161             return !lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
1162                     && !lcValue.startsWith(FormEncodingType.MULTIPART.getName())
1163                     && !lcValue.startsWith(FormEncodingType.TEXT_PLAIN.getName());
1164         }
1165         if (HttpHeader.ACCEPT_LC.equals(name)
1166                 || HttpHeader.ACCEPT_LANGUAGE_LC.equals(name)
1167                 || HttpHeader.CONTENT_LANGUAGE_LC.equals(name)
1168                 || HttpHeader.REFERER_LC.equals(name)
1169                 || "accept-encoding".equals(name)
1170                 || HttpHeader.ORIGIN_LC.equals(name)) {
1171             return false;
1172         }
1173         return true;
1174     }
1175 
1176     /**
1177      * Sets the specified header to the specified value. The <code>open</code> method must be
1178      * called before this method, or an error will occur.
1179      * @param name the name of the header being set
1180      * @param value the value of the header being set
1181      */
1182     @JsxFunction
1183     public void setRequestHeader(final String name, final String value) {
1184         if (!isAuthorizedHeader(name)) {
1185             if (LOG.isWarnEnabled()) {
1186                 LOG.warn("Ignoring XMLHttpRequest.setRequestHeader for " + name
1187                     + ": it is a restricted header");
1188             }
1189             return;
1190         }
1191 
1192         if (webRequest_ != null) {
1193             webRequest_.setAdditionalHeader(name, value);
1194         }
1195         else {
1196             throw JavaScriptEngine.asJavaScriptException(
1197                     getWindow(),
1198                     "The open() method must be called before setRequestHeader().",
1199                     DOMException.INVALID_STATE_ERR);
1200         }
1201     }
1202 
1203     /**
1204      * Not all request headers can be set from JavaScript.
1205      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
1206      * @param name the header name
1207      * @return {@code true} if the header can be set from JavaScript
1208      */
1209     static boolean isAuthorizedHeader(final String name) {
1210         final String nameLowerCase = org.htmlunit.util.StringUtils.toRootLowerCase(name);
1211         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
1212             return false;
1213         }
1214         if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
1215             return false;
1216         }
1217         return true;
1218     }
1219 
1220     /**
1221      * Override the mime type returned by the server (if any). This may be used, for example, to force a stream
1222      * to be treated and parsed as text/xml, even if the server does not report it as such.
1223      * This must be done before the send method is invoked.
1224      * @param mimeType the type used to override that returned by the server (if any)
1225      * @see <a href="http://xulplanet.com/references/objref/XMLHttpRequest.html#method_overrideMimeType">XUL Planet</a>
1226      */
1227     @JsxFunction
1228     public void overrideMimeType(final String mimeType) {
1229         if (state_ != UNSENT && state_ != OPENED) {
1230             throw JavaScriptEngine.asJavaScriptException(
1231                     getWindow(),
1232                     "Property 'overrideMimeType' not writable after sent.",
1233                     DOMException.INVALID_STATE_ERR);
1234         }
1235         overriddenMimeType_ = mimeType;
1236     }
1237 
1238     /**
1239      * Returns the {@code withCredentials} property.
1240      * @return the {@code withCredentials} property
1241      */
1242     @JsxGetter
1243     public boolean isWithCredentials() {
1244         return withCredentials_;
1245     }
1246 
1247     /**
1248      * Sets the {@code withCredentials} property.
1249      * @param withCredentials the {@code withCredentials} property.
1250      */
1251     @JsxSetter
1252     public void setWithCredentials(final boolean withCredentials) {
1253         withCredentials_ = withCredentials;
1254     }
1255 
1256     /**
1257      * Returns the {@code upload} property.
1258      * @return the {@code upload} property
1259      */
1260     @JsxGetter
1261     public XMLHttpRequestUpload getUpload() {
1262         final XMLHttpRequestUpload upload = new XMLHttpRequestUpload();
1263         upload.setParentScope(getParentScope());
1264         upload.setPrototype(getPrototype(upload.getClass()));
1265         return upload;
1266     }
1267 
1268     /**
1269      * {@inheritDoc}
1270      */
1271     @JsxGetter
1272     @Override
1273     public Function getOnreadystatechange() {
1274         return super.getOnreadystatechange();
1275     }
1276 
1277     /**
1278      * {@inheritDoc}
1279      */
1280     @JsxSetter
1281     @Override
1282     public void setOnreadystatechange(final Function readyStateChangeHandler) {
1283         super.setOnreadystatechange(readyStateChangeHandler);
1284     }
1285 
1286     /**
1287      * @return the number of milliseconds a request can take before automatically being terminated.
1288      *         The default value is 0, which means there is no timeout.
1289      */
1290     @JsxGetter
1291     public int getTimeout() {
1292         return timeout_;
1293     }
1294 
1295     /**
1296      * Sets the number of milliseconds a request can take before automatically being terminated.
1297      * @param timeout the timeout in milliseconds
1298      */
1299     @JsxSetter
1300     public void setTimeout(final int timeout) {
1301         timeout_ = timeout;
1302     }
1303 
1304     private static final class NetworkErrorWebResponse extends WebResponse {
1305         private final WebRequest request_;
1306         private final IOException error_;
1307 
1308         NetworkErrorWebResponse(final WebRequest webRequest, final IOException error) {
1309             super(null, null, 0);
1310             request_ = webRequest;
1311             error_ = error;
1312         }
1313 
1314         @Override
1315         public int getStatusCode() {
1316             return 0;
1317         }
1318 
1319         @Override
1320         public String getStatusMessage() {
1321             return "";
1322         }
1323 
1324         @Override
1325         public String getContentType() {
1326             return "";
1327         }
1328 
1329         @Override
1330         public String getContentAsString() {
1331             return "";
1332         }
1333 
1334         @Override
1335         public InputStream getContentAsStream() {
1336             return null;
1337         }
1338 
1339         @Override
1340         public List<NameValuePair> getResponseHeaders() {
1341             return Collections.emptyList();
1342         }
1343 
1344         @Override
1345         public String getResponseHeaderValue(final String headerName) {
1346             return "";
1347         }
1348 
1349         @Override
1350         public long getLoadTime() {
1351             return 0;
1352         }
1353 
1354         @Override
1355         public Charset getContentCharset() {
1356             return null;
1357         }
1358 
1359         @Override
1360         public WebRequest getWebRequest() {
1361             return request_;
1362         }
1363 
1364         /**
1365          * @return the error
1366          */
1367         public IOException getError() {
1368             return error_;
1369         }
1370     }
1371 
1372     private static final class NoPermittedHeaderException extends IOException {
1373         NoPermittedHeaderException(final String msg) {
1374             super(msg);
1375         }
1376     }
1377 }