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