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;
16  
17  import java.io.IOException;
18  import java.io.ObjectInputStream;
19  import java.io.ObjectOutputStream;
20  import java.io.Serializable;
21  import java.net.IDN;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.EnumSet;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  
34  import org.apache.http.auth.Credentials;
35  import org.htmlunit.http.HttpUtils;
36  import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
37  import org.htmlunit.util.NameValuePair;
38  import org.htmlunit.util.StringUtils;
39  import org.htmlunit.util.UrlUtils;
40  
41  /**
42   * Parameter object for making web requests.
43   *
44   * @author Brad Clarke
45   * @author Hans Donner
46   * @author Ahmed Ashour
47   * @author Marc Guillemot
48   * @author Rodney Gitzel
49   * @author Ronald Brill
50   * @author Adam Afeltowicz
51   * @author Joerg Werner
52   * @author Michael Lueck
53   * @author Lai Quang Duong
54   * @author Kristof Neirynck
55   */
56  @SuppressWarnings("PMD.TooManyFields")
57  public class WebRequest implements Serializable {
58  
59      /**
60       * Enum to configure request creation.
61       */
62      public enum HttpHint {
63          /** Force to include the charset. */
64          IncludeCharsetInContentTypeHeader,
65  
66          /** Disable sending of stored cookies and receiving of new cookies. */
67          BlockCookies,
68  
69          /** Do not follow redirects; the response of the redirect is returned as it is. */
70          DoNotFollowRedirects
71      }
72  
73      /**
74       * The destination of a request, as defined by the Fetch spec
75       * (<a href="https://fetch.spec.whatwg.org/#concept-request-destination">
76       * https://fetch.spec.whatwg.org/#concept-request-destination</a>).
77       * <p>
78       * This is used to compute the value of the {@code Sec-Fetch-Dest} request
79       * header (see <a href="https://www.w3.org/TR/fetch-metadata/">
80       * https://www.w3.org/TR/fetch-metadata/</a>) and also determines the default
81       * {@link FetchMode} to be used, unless explicitly overridden via
82       * {@link #setFetchModeOverride(FetchMode)}.
83       * </p>
84       */
85      public enum FetchDestination {
86          /** A top-level document, e.g. loaded by the address bar, a link, or a form submission. */
87          DOCUMENT("document"),
88          /** A nested browsing context of type {@code <iframe>}. */
89          IFRAME("iframe"),
90          /** A nested browsing context of type {@code <frame>}. */
91          FRAME("frame"),
92          /** An {@code <object>} element. */
93          OBJECT("object"),
94          /** An {@code <embed>} element. */
95          EMBED("embed"),
96          /** An image, e.g. {@code <img>}, a CSS background-image, or a favicon. */
97          IMAGE("image"),
98          /** A classic or module script, e.g. {@code <script src>}. */
99          SCRIPT("script"),
100         /** A stylesheet, e.g. {@code <link rel=stylesheet>} or a CSS {@code @import}. */
101         STYLE("style"),
102         /** A font resource, e.g. loaded via {@code @font-face}. */
103         FONT("font"),
104         /** An {@code <audio>} resource. */
105         AUDIO("audio"),
106         /** A {@code <video>} resource. */
107         VIDEO("video"),
108         /** A {@code <track>} resource. */
109         TRACK("track"),
110         /** A dedicated worker script. */
111         WORKER("worker"),
112         /** A shared worker script. */
113         SHARED_WORKER("sharedworker"),
114         /** A service worker script. */
115         SERVICE_WORKER("serviceworker"),
116         /** A web app manifest, e.g. {@code <link rel=manifest>}. */
117         MANIFEST("manifest"),
118         /** A reporting endpoint request. */
119         REPORT("report"),
120         /** A WebSocket handshake request. */
121         WEBSOCKET("websocket"),
122         /** No specific destination, e.g. {@code XMLHttpRequest} or {@code fetch()}. */
123         EMPTY("empty");
124 
125         private final String value_;
126 
127         FetchDestination(final String value) {
128             value_ = value;
129         }
130 
131         /**
132          * Returns the value to be used for the {@code Sec-Fetch-Dest} header.
133          *
134          * @return the value to be used for the {@code Sec-Fetch-Dest} header
135          */
136         public String getValue() {
137             return value_;
138         }
139     }
140 
141     /**
142      * The mode of a request, as defined by the Fetch spec
143      * (<a href="https://fetch.spec.whatwg.org/#concept-request-mode">
144      * https://fetch.spec.whatwg.org/#concept-request-mode</a>).
145      * <p>
146      * This is used to compute the value of the {@code Sec-Fetch-Mode} request
147      * header. Most {@link FetchDestination}s imply a fixed mode; this only needs
148      * to be set explicitly to override that default - e.g. for a {@code fetch()}
149      * call using an explicit {@code mode} option, or a subresource request using
150      * the {@code crossorigin} attribute.
151      * </p>
152      */
153     public enum FetchMode {
154         /** Same-origin requests, e.g. worker scripts, or {@code fetch()} with {@code mode: 'same-origin'}. */
155         SAME_ORIGIN("same-origin"),
156         /** Subresource requests without CORS, e.g. {@code <img>} without {@code crossorigin}. */
157         NO_CORS("no-cors"),
158         /** CORS-enabled requests, e.g. {@code XMLHttpRequest}, {@code fetch()} default, or module scripts. */
159         CORS("cors"),
160         /** Navigations, e.g. top-level document loads, {@code <iframe>}/{@code <frame>} loads. */
161         NAVIGATE("navigate"),
162         /** WebSocket handshake requests. */
163         WEBSOCKET("websocket");
164 
165         private final String value_;
166 
167         FetchMode(final String value) {
168             value_ = value;
169         }
170 
171         /**
172          * Returns the value to be used for the {@code Sec-Fetch-Mode} header.
173          *
174          * @return the value to be used for the {@code Sec-Fetch-Mode} header
175          */
176         public String getValue() {
177             return value_;
178         }
179     }
180 
181     // String instead of java.net.URL because "about:blank" URLs don't serialize correctly
182     private String url_;
183 
184     private String proxyHost_;
185     private int proxyPort_;
186     private String proxyScheme_;
187     private boolean isSocksProxy_;
188     private HttpMethod httpMethod_ = HttpMethod.GET;
189     private FormEncodingType encodingType_ = FormEncodingType.URL_ENCODED;
190     private Map<String, String> additionalHeaders_ = new HashMap<>();
191     private Credentials urlCredentials_;
192     private Credentials credentials_;
193     private int timeout_;
194     private transient Set<HttpHint> httpHints_;
195 
196     private transient Charset charset_ = StandardCharsets.ISO_8859_1;
197     // https://datatracker.ietf.org/doc/html/rfc6838#section-4.2.1
198     // private transient Charset defaultResponseContentCharset_ = StandardCharsets.UTF_8;
199     private transient Charset defaultResponseContentCharset_ = StandardCharsets.ISO_8859_1;
200 
201     /*
202      * These two are mutually exclusive; additionally, requestBody_ should only be
203      * set for POST requests.
204      */
205     private List<NameValuePair> requestParameters_ = Collections.emptyList();
206     private String requestBody_;
207 
208     // Sec-Fetch-* support; see https://www.w3.org/TR/fetch-metadata/
209     private FetchDestination fetchDestination_ = FetchDestination.EMPTY;
210     private FetchMode fetchModeOverride_;
211     private boolean userActivation_;
212 
213     // String instead of java.net.URL for the same serialization reason as url_ above;
214     // null means "no initiator" (e.g. a browser-chrome-initiated navigation), which
215     // maps to Sec-Fetch-Site: none.
216     private String requestingUrl_;
217 
218     /**
219      * Instantiates a {@link WebRequest} for the specified URL,
220      * setting the Accept and Accept-Encoding headers if provided.
221      *
222      * @param url                  the target URL
223      * @param acceptHeader         the accept header to use
224      * @param acceptEncodingHeader the accept encoding header to use
225      */
226     public WebRequest(final URL url, final String acceptHeader, final String acceptEncodingHeader) {
227         setUrl(url);
228         if (acceptHeader != null) {
229             setAdditionalHeader(HttpHeader.ACCEPT, acceptHeader);
230         }
231         if (acceptEncodingHeader != null) {
232             setAdditionalHeader(HttpHeader.ACCEPT_ENCODING, acceptEncodingHeader);
233         }
234         timeout_ = -1;
235 
236         // for backward compatibility
237         setFetchDestination(FetchDestination.DOCUMENT);
238         setFetchModeOverride(FetchMode.NAVIGATE);
239         setUserActivation(true);
240     }
241 
242     /**
243      * Instantiates a {@link WebRequest} for the specified URL,
244      * using the given charset and referer.
245      *
246      * @param url        the target URL
247      * @param charset    the charset to use
248      * @param refererUrl the url be used by the referer header
249      */
250     public WebRequest(final URL url, final Charset charset, final URL refererUrl) {
251         setUrl(url);
252         setCharset(charset);
253         setRefererHeader(refererUrl);
254     }
255 
256     /**
257      * Returns a new request for about:blank.
258      *
259      * @return a new request for about:blank
260      */
261     public static WebRequest newAboutBlankRequest() {
262         return new WebRequest(UrlUtils.URL_ABOUT_BLANK, "*/*", "gzip, deflate");
263     }
264 
265     /**
266      * Instantiates a {@link WebRequest} for the specified URL.
267      *
268      * @param url the target URL
269      */
270     public WebRequest(final URL url) {
271         this(url, "*/*", "gzip, deflate");
272     }
273 
274     /**
275      * Instantiates a {@link WebRequest} for the specified URL
276      * using the given HTTP submit method.
277      *
278      * @param url          the target URL
279      * @param submitMethod the HTTP submit method to use
280      */
281     public WebRequest(final URL url, final HttpMethod submitMethod) {
282         this(url);
283         setHttpMethod(submitMethod);
284     }
285 
286     /**
287      * Returns the target URL.
288      *
289      * @return the target URL
290      */
291     public URL getUrl() {
292         return UrlUtils.toUrlSafe(url_);
293     }
294 
295     /**
296      * Sets the target URL. The URL may be simplified if needed (for instance
297      * eliminating irrelevant path portions like "/./").
298      *
299      * @param url the target URL
300      */
301     public void setUrl(URL url) {
302         if (url == null) {
303             url_ = null;
304             return;
305         }
306 
307         final String path = url.getPath();
308         if (path.isEmpty()) {
309             if (!url.getFile().isEmpty() || url.getProtocol().startsWith("http")) {
310                 url = buildUrlWithNewPath(url, "/");
311             }
312         }
313         else if (path.contains("/.")) {
314             url = buildUrlWithNewPath(url, StringUtils.removeDots(path));
315         }
316 
317         try {
318             final String idn = IDN.toASCII(url.getHost());
319             if (!idn.equals(url.getHost())) {
320                 url = UrlUtils.getUrlWithNewHost(url, idn);
321             }
322         }
323         catch (final Exception e) {
324             throw new IllegalArgumentException(
325                     "Cannot convert the hostname of URL: '" + url.toExternalForm() + "' to ASCII.", e);
326         }
327 
328         try {
329             url_ = UrlUtils.removeRedundantPort(url).toExternalForm();
330         }
331         catch (final MalformedURLException e) {
332             throw new RuntimeException("Cannot strip default port of URL: " + url.toExternalForm(), e);
333         }
334 
335         // http://john.smith:secret@localhost
336         final String userInfo = url.getUserInfo();
337         if (userInfo != null) {
338             final int splitPos = userInfo.indexOf(':');
339             if (splitPos == -1) {
340                 urlCredentials_ = new HtmlUnitUsernamePasswordCredentials(userInfo, new char[0]);
341             }
342             else {
343                 final String username = userInfo.substring(0, splitPos);
344                 final String password = userInfo.substring(splitPos + 1);
345                 urlCredentials_ = new HtmlUnitUsernamePasswordCredentials(username, password.toCharArray());
346             }
347         }
348     }
349 
350     private static URL buildUrlWithNewPath(URL url, final String newPath) {
351         try {
352             url = UrlUtils.getUrlWithNewPath(url, newPath);
353         }
354         catch (final Exception e) {
355             throw new RuntimeException("Cannot change path of URL: " + url.toExternalForm(), e);
356         }
357         return url;
358     }
359 
360     /**
361      * Returns the proxy host to use.
362      *
363      * @return the proxy host to use
364      */
365     public String getProxyHost() {
366         return proxyHost_;
367     }
368 
369     /**
370      * Sets the proxy host to use.
371      *
372      * @param proxyHost the proxy host to use
373      */
374     public void setProxyHost(final String proxyHost) {
375         proxyHost_ = proxyHost;
376     }
377 
378     /**
379      * Returns the proxy port to use.
380      *
381      * @return the proxy port to use
382      */
383     public int getProxyPort() {
384         return proxyPort_;
385     }
386 
387     /**
388      * Sets the proxy port to use.
389      *
390      * @param proxyPort the proxy port to use
391      */
392     public void setProxyPort(final int proxyPort) {
393         proxyPort_ = proxyPort;
394     }
395 
396     /**
397      * Returns the proxy scheme to use.
398      *
399      * @return the proxy scheme to use
400      */
401     public String getProxyScheme() {
402         return proxyScheme_;
403     }
404 
405     /**
406      * Sets the proxy scheme to use.
407      *
408      * @param proxyScheme the proxy scheme to use
409      *
410      */
411     public void setProxyScheme(final String proxyScheme) {
412         proxyScheme_ = proxyScheme;
413     }
414 
415     /**
416      * Returns whether SOCKS proxy or not.
417      *
418      * Returns whether SOCKS proxy or not.
419      *
420      * @return whether SOCKS proxy or not
421      *
422      */
423     public boolean isSocksProxy() {
424         return isSocksProxy_;
425     }
426 
427     /**
428      * Sets whether SOCKS proxy or not.
429      *
430      * @param isSocksProxy whether SOCKS proxy or not
431      *
432      */
433     public void setSocksProxy(final boolean isSocksProxy) {
434         isSocksProxy_ = isSocksProxy;
435     }
436 
437     /**
438      * Returns the timeout to use.
439      *
440      * @return the timeout to use
441      *
442      */
443     public int getTimeout() {
444         return timeout_;
445     }
446 
447     /**
448      * Sets the timeout to use.
449      *
450      * @param timeout the timeout to use
451      *
452      */
453     public void setTimeout(final int timeout) {
454         timeout_ = timeout;
455     }
456 
457     /**
458      * Returns the form encoding type to use.
459      *
460      * Returns the form encoding type to use.
461      *
462      * @return the form encoding type to use
463      *
464      */
465     public FormEncodingType getEncodingType() {
466         return encodingType_;
467     }
468 
469     /**
470      * Sets the form encoding type to use.
471      *
472      * @param encodingType the form encoding type to use
473      *
474      */
475     public void setEncodingType(final FormEncodingType encodingType) {
476         encodingType_ = encodingType;
477     }
478 
479     /**
480      * Retrieves the request parameters used. Similar to the servlet api function
481      * getParameterMap() this works depending on the request type and collects the
482      * url parameters and the body stuff.<br>
483      * The value is also normalized - null is converted to an empty string.
484      * <p>In contrast to the servlet api this creates a separate KeyValuePair for every
485      * parameter. This means that pairs with the same name can be part of the list. The
486      * servlet api will return a string[] as value for the key in this case.<br>
487      * Additionally this method includes also the uploaded files for multipart post
488      * requests.</p>
489      *
490      * @return the request parameters to use
491      */
492     public List<NameValuePair> getParameters() {
493         // developer note:
494         // this has to be in sync with org.htmlunit.HttpWebConnection.makeHttpMethod(WebRequest, HttpClientBuilder)
495 
496         // developer note:
497         // the spring org.springframework.test.web.servlet.htmlunitHtmlUnitRequestBuilder uses
498         // this method and is sensitive to all the details of the current implementation.
499 
500         final List<NameValuePair> allParameters = new ArrayList<>(
501                 HttpUtils.parseUrlQuery(getUrl().getQuery(), getCharset()));
502 
503         // the servlet api ignores these parameters but to make spring happy we include them
504         final HttpMethod httpMethod = getHttpMethod();
505         if (httpMethod == HttpMethod.POST
506             || httpMethod == HttpMethod.PUT
507             || httpMethod == HttpMethod.PATCH
508             || httpMethod == HttpMethod.DELETE
509             || httpMethod == HttpMethod.OPTIONS) {
510             if (FormEncodingType.URL_ENCODED == getEncodingType()
511                 && httpMethod != HttpMethod.OPTIONS) {
512                 // spring ignores URL_ENCODED parameters for OPTIONS requests
513                 // getRequestParameters and getRequestBody are mutually exclusive
514                 if (getRequestBody() == null) {
515                     allParameters.addAll(getRequestParameters());
516                 }
517                 else {
518                     allParameters.addAll(HttpUtils.parseUrlQuery(getRequestBody(), getCharset()));
519                 }
520             }
521             else if (FormEncodingType.MULTIPART == getEncodingType()) {
522                 if (httpMethod == HttpMethod.POST) {
523                     allParameters.addAll(getRequestParameters());
524                 }
525                 else {
526                     // for PUT, PATCH, DELETE and OPTIONS spring moves the parameters up to the query
527                     // it doesn't replace the query
528                     allParameters.addAll(0, getRequestParameters());
529                 }
530             }
531         }
532 
533         return normalize(allParameters);
534     }
535 
536     private static List<NameValuePair> normalize(final List<NameValuePair> pairs) {
537         if (pairs == null || pairs.isEmpty()) {
538             return pairs;
539         }
540 
541         final List<NameValuePair> resultingPairs = new ArrayList<>();
542         for (final NameValuePair pair : pairs) {
543             resultingPairs.add(pair.normalized());
544         }
545 
546         return resultingPairs;
547     }
548 
549     /**
550      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT
551      * YOUR OWN RISK.</span><br>
552      *
553      * Returns the request parameters to use. If set, these request parameters
554      * will overwrite any request parameters which may be present in the
555      * {@link #getUrl() URL}. Should not be used in combination with the
556      * {@link #setRequestBody(String) request body}.
557      *
558      * @return the request parameters to use
559      */
560     public List<NameValuePair> getRequestParameters() {
561         return requestParameters_;
562     }
563 
564     /**
565      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT
566      * YOUR OWN RISK.</span><br>
567      *
568      * Sets the request parameters to use. If set, these request parameters will
569      * overwrite any request parameters which may be present in the {@link #getUrl()
570      * URL}. Should not be used in combination with the
571      * {@link #setRequestBody(String) request body}.
572      *
573      * @param requestParameters the request parameters to use
574      * @throws RuntimeException if the request body has already been set
575      */
576     public void setRequestParameters(final List<NameValuePair> requestParameters) throws RuntimeException {
577         if (requestBody_ != null) {
578             final String msg = "Trying to set the request parameters, but the request body has already been specified;"
579                     + "the two are mutually exclusive!";
580             throw new RuntimeException(msg);
581         }
582         requestParameters_ = requestParameters;
583     }
584 
585     /**
586      * Returns the body content to be submitted if this is a {@code POST}
587      * request. Ignored for all other request types. Should not be used in
588      * combination with {@link #setRequestParameters(List) request parameters}.
589      *
590      * @return the body content, or {@code null} if not set
591      */
592     public String getRequestBody() {
593         return requestBody_;
594     }
595 
596     /**
597      * Sets the body content to be submitted if this is a {@code POST}, {@code PUT}
598      * or {@code PATCH} request. Other request types result in
599      * {@link IllegalStateException}. Should not be used in combination with
600      * {@link #setRequestParameters(List) request parameters}.
601      *
602      * @param requestBody the body content to be submitted if this is a
603      *                    {@code POST}, {@code PUT} or {@code PATCH} request
604      * @throws IllegalStateException if the request parameters have already been set or
605      *                          this is not a {@code POST}, {@code PUT} or
606      *                          {@code PATCH} request.
607      *
608      */
609     public void setRequestBody(final String requestBody) throws IllegalStateException {
610         if (requestParameters_ != null && !requestParameters_.isEmpty()) {
611             final String msg = "Trying to set the request body, but the request parameters have already been specified;"
612                        + "the two are mutually exclusive!";
613             throw new IllegalStateException(msg);
614         }
615         if (httpMethod_ != HttpMethod.POST
616                 && httpMethod_ != HttpMethod.PUT
617                 && httpMethod_ != HttpMethod.PATCH
618                 && httpMethod_ != HttpMethod.DELETE
619                 && httpMethod_ != HttpMethod.OPTIONS) {
620             final String msg = "The request body may only be set for POST, PUT, PATCH, DELETE or OPTIONS requests!";
621             throw new IllegalStateException(msg);
622         }
623         requestBody_ = requestBody;
624     }
625 
626     /**
627      * Returns the HTTP submit method to use.
628      *
629      * @return the HTTP submit method to use
630      */
631     public HttpMethod getHttpMethod() {
632         return httpMethod_;
633     }
634 
635     /**
636      * Sets the HTTP submit method to use.
637      *
638      * @param submitMethod the HTTP submit method to use
639      */
640     public void setHttpMethod(final HttpMethod submitMethod) {
641         httpMethod_ = submitMethod;
642     }
643 
644     /**
645      * Returns the additional HTTP headers to use.
646      *
647      * @return the additional HTTP headers to use
648      */
649     public Map<String, String> getAdditionalHeaders() {
650         return additionalHeaders_;
651     }
652 
653     /**
654      * Sets the additional HTTP headers to use.
655      *
656      * @param additionalHeaders the additional HTTP headers to use
657      */
658     public void setAdditionalHeaders(final Map<String, String> additionalHeaders) {
659         additionalHeaders_ = additionalHeaders;
660     }
661 
662     /**
663      * Returns whether the specified header name is already included in the
664      * additional HTTP headers. The comparison is case-insensitive.
665      *
666      * @param name the header name to look up
667      * @return {@code true} if the header is present; {@code false} otherwise
668      */
669     public boolean isAdditionalHeader(final String name) {
670         for (final String key : additionalHeaders_.keySet()) {
671             if (name.equalsIgnoreCase(key)) {
672                 return true;
673             }
674         }
675         return false;
676     }
677 
678     /**
679      * Returns the value of the specified additional HTTP header,
680      * or {@code null} if the header is not set. The name comparison
681      * is case-insensitive.
682      *
683      * @param name the header name to look up
684      * @return the header value, or {@code null} if not present
685      */
686     public String getAdditionalHeader(final String name) {
687         final String val = additionalHeaders_.get(name);
688         if (val != null) {
689             return val;
690         }
691 
692         // fall back to case-insensitive scan
693         for (final Map.Entry<String, String> entry : additionalHeaders_.entrySet()) {
694             if (name.equalsIgnoreCase(entry.getKey())) {
695                 return entry.getValue();
696             }
697         }
698         return null;
699     }
700 
701     /**
702      * Sets the {@code Referer} HTTP header to the external form of the given
703      * URL. Does nothing if the URL is {@code null} or does not use the
704      * {@code http} or {@code https} scheme.
705      *
706      * @param url the URL to use as the referer, or {@code null}
707      */
708     public void setRefererHeader(final URL url) {
709         if (url == null || !url.getProtocol().startsWith("http")) {
710             return;
711         }
712 
713         try {
714             setAdditionalHeader(HttpHeader.REFERER, UrlUtils.getUrlWithoutRef(url).toExternalForm());
715         }
716         catch (final MalformedURLException ignored) {
717             // bad luck use the whole url from the pager
718         }
719     }
720 
721     /**
722      * Returns the destination of this request, used to compute the
723      * {@code Sec-Fetch-Dest} header (and, unless overridden, the default
724      * {@code Sec-Fetch-Mode}). Defaults to {@link FetchDestination#EMPTY},
725      * which is correct for plain {@code XMLHttpRequest}/{@code fetch()} calls.
726      *
727      * @return the destination of this request
728      */
729     public FetchDestination getFetchDestination() {
730         return fetchDestination_;
731     }
732 
733     /**
734      * Sets the destination of this request.
735      *
736      * @param fetchDestination the destination of this request, or {@code null}
737      *                         to reset to {@link FetchDestination#EMPTY}
738      */
739     public void setFetchDestination(final FetchDestination fetchDestination) {
740         fetchDestination_ = fetchDestination == null ? FetchDestination.EMPTY : fetchDestination;
741     }
742 
743     /**
744      * Returns the explicit mode override for this request, if any. When
745      * {@code null} (the default), the mode is derived from the
746      * {@link #getFetchDestination() destination}.
747      *
748      * @return the mode override, or {@code null} if none was set
749      */
750     public FetchMode getFetchModeOverride() {
751         return fetchModeOverride_;
752     }
753 
754     /**
755      * Sets an explicit mode override for this request, e.g. for a {@code fetch()}
756      * call using an explicit {@code mode} option, or a subresource request using
757      * the {@code crossorigin} attribute (which forces CORS mode).
758      *
759      * @param fetchMode the mode to use, or {@code null} to derive it from the
760      *                  {@link #getFetchDestination() destination}
761      */
762     public void setFetchModeOverride(final FetchMode fetchMode) {
763         fetchModeOverride_ = fetchMode;
764     }
765 
766     /**
767      * Returns whether this request is the result of a navigation backed by
768      * genuine user activation (e.g. a click on a link, a typed URL, or a form
769      * submitted via a click on its submit button) as opposed to one triggered
770      * purely by script (e.g. {@code location.href = ...}, a {@code <meta
771      * http-equiv="refresh">}, or an automatically-loaded {@code <iframe>}).
772      * <p>
773      * Only relevant for requests whose {@code Sec-Fetch-Mode} is {@code
774      * navigate}; used to compute the presence of the {@code Sec-Fetch-User}
775      * header, which real browsers omit entirely (never send as {@code ?0})
776      * whenever this is {@code false}.
777      * </p>
778      *
779      * @return whether this request was triggered by a real user gesture
780      */
781     public boolean isUserActivation() {
782         return userActivation_;
783     }
784 
785     /**
786      * Sets whether this request is the result of a navigation backed by genuine
787      * user activation.
788      *
789      * @param userActivation whether this request was triggered by a real user
790      *                       gesture
791      */
792     public void setUserActivation(final boolean userActivation) {
793         userActivation_ = userActivation;
794     }
795 
796     /**
797      * Returns the URL of the document or script that initiated this request, used
798      * to compute the {@code Sec-Fetch-Site} header. {@code null} means there is
799      * no initiator (e.g. a browser-chrome-initiated navigation such as a typed
800      * URL or bookmark), which maps to {@code Sec-Fetch-Site: none}.
801      * <p>
802      * Note this is tracked separately from the {@code Referer} header: unlike
803      * the referrer, it must not be affected by referrer-policy stripping, since
804      * {@code Sec-Fetch-Site} always reflects the true relationship between the
805      * initiator and the target, even when no {@code Referer} header is sent.
806      * </p>
807      *
808      * @return the URL of the initiator, or {@code null} if there is none
809      */
810     public URL getRequestingUrl() {
811         return requestingUrl_ == null ? null : UrlUtils.toUrlSafe(requestingUrl_);
812     }
813 
814     /**
815      * Sets the URL of the document or script that initiated this request.
816      *
817      * @param requestingUrl the URL of the initiator, or {@code null} if there is
818      *                      none
819      */
820     public void setRequestingUrl(final URL requestingUrl) {
821         requestingUrl_ = requestingUrl == null ? null : requestingUrl.toExternalForm();
822     }
823 
824     /**
825      * Convenience method for the common case of a top-level navigation (an
826      * anchor/area click, a form submission, a script-driven location change, ...):
827      * sets {@link FetchDestination#DOCUMENT}, the initiator URL, and whether the
828      * navigation was backed by genuine user activation, all in one call.
829      * <p>
830      * Every navigation-triggering call site needs all three of these set
831      * together for correct {@code Sec-Fetch-*} headers; bundling them here
832      * makes it harder for a call site to set some of them and forget the rest.
833      * </p>
834      * <p>
835      * For navigations whose destination is not {@link FetchDestination#DOCUMENT}
836      * (e.g. an {@code <iframe>}/{@code <frame>} load), use
837      * {@link #markAsNavigation(FetchDestination, URL, boolean)} instead.
838      * </p>
839      *
840      * @param requestingUrl the URL of the page initiating this navigation, or
841      *                      {@code null} if there is none (e.g. a typed URL)
842      * @param userActivation whether this navigation was triggered by a real
843      *                       user gesture as opposed to script
844      */
845     public void markAsNavigation(final URL requestingUrl, final boolean userActivation) {
846         markAsNavigation(FetchDestination.DOCUMENT, requestingUrl, userActivation);
847     }
848 
849     /**
850      * Same as {@link #markAsNavigation(URL, boolean)}, but for navigations whose
851      * destination isn't a top-level {@link FetchDestination#DOCUMENT} - currently
852      * only {@code <iframe>}/{@code <frame>} loads ({@link FetchDestination#IFRAME}/
853      * {@link FetchDestination#FRAME}).
854      *
855      * @param destination the navigation's destination
856      * @param requestingUrl the URL of the page initiating this navigation, or
857      *                      {@code null} if there is none
858      * @param userActivation whether this navigation was triggered by a real
859      *                       user gesture as opposed to script
860      */
861     public void markAsNavigation(final FetchDestination destination, final URL requestingUrl,
862             final boolean userActivation) {
863         setFetchDestination(destination);
864         setRequestingUrl(requestingUrl);
865         setUserActivation(userActivation);
866     }
867 
868     /**
869      * Sets the specified name/value pair in the additional HTTP headers,
870      * replacing any existing header with the same name (case-insensitive).
871      *
872      * @param name  the header name
873      * @param value the header value
874      */
875     public void setAdditionalHeader(final String name, final String value) {
876         if (additionalHeaders_.containsKey(name)) {
877             additionalHeaders_.put(name, value);
878             return;
879         }
880 
881         // fall back to case-insensitive scan
882         for (final String key : additionalHeaders_.keySet()) {
883             if (name.equalsIgnoreCase(key)) {
884                 additionalHeaders_.put(key, value);
885                 return;
886             }
887         }
888 
889         // no existing header found, insert with the given name
890         additionalHeaders_.put(name, value);
891     }
892 
893     /**
894      * Removes the specified header from the additional HTTP headers.
895      * The name comparison is case-insensitive. Does nothing if the
896      * header is not present.
897      *
898      * @param name the header name to remove
899      */
900     public void removeAdditionalHeader(final String name) {
901         if (additionalHeaders_.remove(name) != null) {
902             return;
903         }
904 
905         // fall back to case-insensitive scan
906         for (final String key : additionalHeaders_.keySet()) {
907             if (name.equalsIgnoreCase(key)) {
908                 additionalHeaders_.remove(key);
909                 return;
910             }
911         }
912     }
913 
914     /**
915      * Returns the credentials extracted from the URL's userinfo component
916      * (e.g. {@code http://user:secret@host/}), or {@code null} if none were present.
917      *
918      * @return the URL-embedded credentials, or {@code null}
919      */
920     public Credentials getUrlCredentials() {
921         return urlCredentials_;
922     }
923 
924     /**
925      * Returns the credentials explicitly set via {@link #setCredentials(Credentials)},
926      * or {@code null} if none were set.
927      *
928      * @return the explicitly configured credentials, or {@code null}
929      */
930     public Credentials getCredentials() {
931         return credentials_;
932     }
933 
934     /**
935      * Sets the credentials to use.
936      *
937      * @param credentials the credentials to use
938      */
939     public void setCredentials(final Credentials credentials) {
940         credentials_ = credentials;
941     }
942 
943     /**
944      * Returns the character set to use to perform the request.
945      *
946      * @return the character set to use to perform the request
947      */
948     public Charset getCharset() {
949         return charset_;
950     }
951 
952     /**
953      * Sets the character set to use to perform the request. The default value is
954      * {@link java.nio.charset.StandardCharsets#ISO_8859_1}.
955      *
956      * @param charset the character set to use to perform the request
957      */
958     public void setCharset(final Charset charset) {
959         charset_ = charset;
960     }
961 
962     /**
963      * Returns the default character set to use for the response when it does not
964      * specify one.
965      *
966      * @return the default character set to use for the response when it does not
967      *         specify one.
968      */
969     public Charset getDefaultResponseContentCharset() {
970         return defaultResponseContentCharset_;
971     }
972 
973     /**
974      * Sets the default character set to use when the response does not declare
975      * one explicitly.
976      * <p>
977      * Unless overridden, the default is
978      * {@link java.nio.charset.StandardCharsets#ISO_8859_1} (per HTTP/1.1).
979      * </p>
980      *
981      * @param defaultResponseContentCharset the fallback charset; must not be {@code null}
982      */
983     public void setDefaultResponseContentCharset(final Charset defaultResponseContentCharset) {
984         WebAssert.notNull("defaultResponseContentCharset", defaultResponseContentCharset);
985         defaultResponseContentCharset_ = defaultResponseContentCharset;
986     }
987 
988     /**
989      * Returns whether the given {@link HttpHint} is currently enabled for this request.
990      *
991      * @param hint the hint to check
992      * @return {@code true} if the hint is enabled; {@code false} otherwise
993      */
994     public boolean hasHint(final HttpHint hint) {
995         if (httpHints_ == null) {
996             return false;
997         }
998         return httpHints_.contains(hint);
999     }
1000 
1001     /**
1002      * Enables the given {@link HttpHint} for this request.
1003      *
1004      * @param hint the hint to enable
1005      */
1006     public void addHint(final HttpHint hint) {
1007         if (httpHints_ == null) {
1008             httpHints_ = EnumSet.noneOf(HttpHint.class);
1009         }
1010         httpHints_.add(hint);
1011     }
1012 
1013     /**
1014      * {@inheritDoc}
1015      */
1016     @Override
1017     public String toString() {
1018         final StringBuilder builder = new StringBuilder(100)
1019                 .append(getClass().getSimpleName())
1020                 .append("[<url=\"")
1021                 .append(url_)
1022                 .append("\", ").append(httpMethod_)
1023                 .append(", ").append(encodingType_)
1024                 .append(", ").append(requestParameters_)
1025                 .append(", ").append(additionalHeaders_)
1026                 .append(", ").append(credentials_)
1027                 .append(">]");
1028         return builder.toString();
1029     }
1030 
1031     private void writeObject(final ObjectOutputStream oos) throws IOException {
1032         oos.defaultWriteObject();
1033         oos.writeObject(charset_ == null ? null : charset_.name());
1034         oos.writeObject(defaultResponseContentCharset_ == null ? null : defaultResponseContentCharset_.name());
1035     }
1036 
1037     private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
1038         ois.defaultReadObject();
1039         final String charsetName = (String) ois.readObject();
1040         if (charsetName != null) {
1041             charset_ = Charset.forName(charsetName);
1042         }
1043         final String defaultResponseContentCharset = (String) ois.readObject();
1044         if (defaultResponseContentCharset != null) {
1045             defaultResponseContentCharset_ = Charset.forName(defaultResponseContentCharset);
1046         }
1047     }
1048 }