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