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