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