001/*
002 * Copyright 2022-2025 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import com.soklet.exception.IllegalFormParameterException;
020import com.soklet.exception.IllegalMultipartFieldException;
021import com.soklet.exception.IllegalQueryParameterException;
022import com.soklet.exception.IllegalRequestCookieException;
023import com.soklet.exception.IllegalRequestException;
024import com.soklet.exception.IllegalRequestHeaderException;
025import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
026
027import javax.annotation.Nonnull;
028import javax.annotation.Nullable;
029import javax.annotation.concurrent.NotThreadSafe;
030import javax.annotation.concurrent.ThreadSafe;
031import java.nio.charset.Charset;
032import java.nio.charset.StandardCharsets;
033import java.util.Arrays;
034import java.util.Collections;
035import java.util.LinkedHashMap;
036import java.util.LinkedHashSet;
037import java.util.List;
038import java.util.Locale;
039import java.util.Locale.LanguageRange;
040import java.util.Map;
041import java.util.Objects;
042import java.util.Optional;
043import java.util.Set;
044import java.util.concurrent.locks.ReentrantLock;
045import java.util.function.Consumer;
046import java.util.stream.Collectors;
047
048import static com.soklet.Utilities.trimAggressivelyToEmpty;
049import static java.lang.String.format;
050import static java.util.Collections.unmodifiableList;
051import static java.util.Objects.requireNonNull;
052
053/**
054 * Encapsulates information specified in an HTTP request.
055 * <p>
056 * Instances can be acquired via the {@link #withRawUrl(HttpMethod, String)} (e.g. provided by clients on a "raw" HTTP/1.1 request line, un-decoded) and {@link #withPath(HttpMethod, String)} (e.g. manually-constructed during integration testing, understood to be already-decoded) builder factory methods.
057 * <p>
058 * Any necessary decoding (path, URL parameter, {@code Content-Type: application/x-www-form-urlencoded}, etc.) will be automatically performed.  Unless otherwise indicated, all accessor methods will return decoded data.
059 * <p>
060 * Detailed documentation available at <a href="https://www.soklet.com/docs/request-handling">https://www.soklet.com/docs/request-handling</a>.
061 *
062 * @author <a href="https://www.revetkn.com">Mark Allen</a>
063 */
064@ThreadSafe
065public final class Request {
066        @Nonnull
067        private static final Charset DEFAULT_CHARSET;
068        @Nonnull
069        private static final IdGenerator DEFAULT_ID_GENERATOR;
070
071        static {
072                DEFAULT_CHARSET = StandardCharsets.UTF_8;
073                DEFAULT_ID_GENERATOR = DefaultIdGenerator.withDefaults();
074        }
075
076        @Nonnull
077        private final Object id;
078        @Nonnull
079        private final HttpMethod httpMethod;
080        @Nonnull
081        private final String rawPath;
082        @Nullable
083        private final String rawQuery;
084        @Nonnull
085        private final String path;
086        @Nonnull
087        private final ResourcePath resourcePath;
088        @Nonnull
089        private final Map<String, Set<String>> queryParameters;
090        @Nullable
091        private final String contentType;
092        @Nullable
093        private final Charset charset;
094        @Nonnull
095        private final Map<String, Set<String>> headers;
096        @Nullable
097        private final Cors cors;
098        @Nullable
099        private final CorsPreflight corsPreflight;
100        @Nullable
101        private final byte[] body;
102        @Nonnull
103        private final Boolean multipart;
104        @Nonnull
105        private final Boolean contentTooLarge;
106        @Nonnull
107        private final MultipartParser multipartParser;
108        @Nonnull
109        private final IdGenerator<?> idGenerator;
110        @Nonnull
111        private final ReentrantLock lock;
112        @Nullable
113        private volatile String bodyAsString = null;
114        @Nullable
115        private volatile List<Locale> locales = null;
116        @Nullable
117        private volatile List<LanguageRange> languageRanges = null;
118        @Nullable
119        private volatile Map<String, Set<String>> cookies = null;
120        @Nullable
121        private volatile Map<String, Set<MultipartField>> multipartFields = null;
122        @Nullable
123        private volatile Map<String, Set<String>> formParameters = null;
124
125        /**
126         * Acquires a builder for {@link Request} instances from the URL provided by clients on a "raw" HTTP/1.1 request line.
127         * <p>
128         * The provided {@code rawUrl} must be un-decoded and in either "path-and-query" form (i.e. starts with a {@code /} character) or an absolute URL (i.e. starts with {@code http://} or {@code https://}).
129         * It might include un-decoded query parameters, e.g. {@code https://www.example.com/one?two=thr%20ee} or {@code /one?two=thr%20ee}.  An exception to this rule is {@code OPTIONS *} requests, where the URL is the {@code *} "splat" symbol.
130         * <p>
131         * Paths will be percent-decoded.
132         * <p>
133         * Query parameters are parsed and decoded using RFC 3986 semantics - see {@link QueryFormat#RFC_3986_STRICT}.
134         * <p>
135         * Request body form parameters with {@code Content-Type: application/x-www-form-urlencoded} are parsed and decoded by using {@link QueryFormat#X_WWW_FORM_URLENCODED}.
136         *
137         * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.})
138         * @param rawUrl     the raw (un-decoded) URL for this request
139         * @return the builder
140         */
141        @Nonnull
142        public static RawBuilder withRawUrl(@Nonnull HttpMethod httpMethod,
143                                                                                                                                                        @Nonnull String rawUrl) {
144                requireNonNull(httpMethod);
145                requireNonNull(rawUrl);
146
147                return new RawBuilder(httpMethod, rawUrl);
148        }
149
150        /**
151         * Acquires a builder for {@link Request} instances from already-decoded path and query components - useful for manual construction, e.g. integration tests.
152         * <p>
153         * The provided {@code path} must start with the {@code /} character and already be decoded (e.g. {@code "/my path"}, not {@code "/my%20path"}).  It must not include query parameters. For {@code OPTIONS *} requests, the {@code path} must be {@code *} - the "splat" symbol.
154         * <p>
155         * Query parameters must be specified via {@link PathBuilder#queryParameters(Map)} and are assumed to be already-decoded.
156         * <p>
157         * Request body form parameters with {@code Content-Type: application/x-www-form-urlencoded} are parsed and decoded by using {@link QueryFormat#X_WWW_FORM_URLENCODED}.
158         *
159         * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.})
160         * @param path       the decoded URL path for this request
161         * @return the builder
162         */
163        @Nonnull
164        public static PathBuilder withPath(@Nonnull HttpMethod httpMethod,
165                                                                                                                                                 @Nonnull String path) {
166                requireNonNull(httpMethod);
167                requireNonNull(path);
168
169                return new PathBuilder(httpMethod, path);
170        }
171
172        /**
173         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
174         *
175         * @return a copier for this instance
176         */
177        @Nonnull
178        public Copier copy() {
179                return new Copier(this);
180        }
181
182        private Request(@Nullable RawBuilder rawBuilder,
183                                                                        @Nullable PathBuilder pathBuilder) {
184                // Should never occur
185                if (rawBuilder == null && pathBuilder == null)
186                        throw new IllegalStateException(format("Neither %s nor %s were specified", RawBuilder.class.getSimpleName(), PathBuilder.class.getSimpleName()));
187
188                IdGenerator builderIdGenerator = rawBuilder == null ? pathBuilder.idGenerator : rawBuilder.idGenerator;
189                Object builderId = rawBuilder == null ? pathBuilder.id : rawBuilder.id;
190                HttpMethod builderHttpMethod = rawBuilder == null ? pathBuilder.httpMethod : rawBuilder.httpMethod;
191                byte[] builderBody = rawBuilder == null ? pathBuilder.body : rawBuilder.body;
192                MultipartParser builderMultipartParser = rawBuilder == null ? pathBuilder.multipartParser : rawBuilder.multipartParser;
193                Boolean builderContentTooLarge = rawBuilder == null ? pathBuilder.contentTooLarge : rawBuilder.contentTooLarge;
194                Map<String, Set<String>> builderHeaders = rawBuilder == null ? pathBuilder.headers : rawBuilder.headers;
195
196                if (builderHeaders == null)
197                        builderHeaders = Map.of();
198
199                this.idGenerator = builderIdGenerator == null ? DEFAULT_ID_GENERATOR : builderIdGenerator;
200                this.multipartParser = builderMultipartParser == null ? DefaultMultipartParser.defaultInstance() : builderMultipartParser;
201
202                // Header names are case-insensitive.  Enforce that here with a special map
203                Map<String, Set<String>> caseInsensitiveHeaders = new LinkedCaseInsensitiveMap<>(builderHeaders);
204                this.headers = Collections.unmodifiableMap(caseInsensitiveHeaders);
205                this.contentType = Utilities.extractContentTypeFromHeaders(this.headers).orElse(null);
206                this.charset = Utilities.extractCharsetFromHeaders(this.headers).orElse(null);
207
208                String path;
209
210                // If we use PathBuilder, use its path directly.
211                // If we use RawBuilder, parse and decode its path.
212                if (pathBuilder != null) {
213                        path = trimAggressivelyToEmpty(pathBuilder.path);
214
215                        // Validate path
216                        if (!path.startsWith("/") && !path.equals("*"))
217                                throw new IllegalRequestException("Path must start with '/' or be '*'");
218
219                        if (path.contains("?"))
220                                throw new IllegalRequestException(format("Path should not contain a query string. Use %s.withPath(...).queryParameters(...) to specify query parameters as a %s.",
221                                                Request.class.getSimpleName(), Map.class.getSimpleName()));
222
223                        // Use already-decoded query parameters as provided by the path builder
224                        this.queryParameters = pathBuilder.queryParameters == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(pathBuilder.queryParameters));
225                } else {
226                        // RawBuilder scenario
227                        String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl);
228
229                        // Special handling for OPTIONS *
230                        if ("*".equals(rawUrl)) {
231                                path = "*";
232                                this.queryParameters = Map.of();
233                        } else {
234                                // First, parse and decode the path...
235                                path = Utilities.extractPathFromUrl(rawUrl, true);
236
237                                // ...then, parse out any query parameters.
238                                if (rawUrl.contains("?")) {
239                                        // We always assume RFC_3986_STRICT for query parameters because Soklet is for modern systems - HTML Form "GET" submissions are rare/legacy.
240                                        // This means we leave "+" as "+" (not decode to " ") and then apply any percent-decoding rules.
241                                        // In the future, we might expose a way to let applications prefer QueryFormat.X_WWW_FORM_URLENCODED instead, which treats "+" as a space
242                                        this.queryParameters = Collections.unmodifiableMap(Utilities.extractQueryParametersFromUrl(rawUrl, QueryFormat.RFC_3986_STRICT, getCharset().orElse(DEFAULT_CHARSET)));
243                                } else {
244                                        this.queryParameters = Map.of();
245                                }
246                        }
247                }
248
249                if (path.equals("*") && builderHttpMethod != HttpMethod.OPTIONS)
250                        throw new IllegalRequestException(format("Path '*' is only legal for HTTP %s", HttpMethod.OPTIONS.name()));
251
252                if (path.contains("\u0000") || path.contains("%00"))
253                        throw new IllegalRequestException(format("Illegal null byte in path '%s'", path));
254
255                this.path = path;
256
257                String rawPath;
258                String rawQuery;
259
260                if (pathBuilder != null) {
261                        // PathBuilder scenario: check if explicit raw values were provided
262                        if (pathBuilder.rawPath != null) {
263                                // Explicit raw values provided (e.g. from Copier preserving originals)
264                                rawPath = pathBuilder.rawPath;
265                                rawQuery = pathBuilder.rawQuery;
266                        } else {
267                                // No explicit raw values; encode from decoded values
268                                if (path.equals("*")) {
269                                        rawPath = "*";
270                                } else {
271                                        rawPath = Utilities.encodePath(path);
272                                }
273
274                                if (this.queryParameters.isEmpty()) {
275                                        rawQuery = null;
276                                } else {
277                                        rawQuery = Utilities.encodeQueryParameters(this.queryParameters, QueryFormat.RFC_3986_STRICT);
278                                }
279                        }
280                } else {
281                        // RawBuilder scenario: extract raw components from rawUrl
282                        String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl);
283
284                        if ("*".equals(rawUrl)) {
285                                rawPath = "*";
286                                rawQuery = null;
287                        } else {
288                                rawPath = Utilities.extractPathFromUrl(rawUrl, false);
289                                rawQuery = Utilities.extractRawQueryFromUrl(rawUrl).orElse(null);
290                        }
291                }
292
293                this.rawPath = rawPath;
294                this.rawQuery = rawQuery;
295
296                this.lock = new ReentrantLock();
297                this.id = builderId == null ? this.idGenerator.generateId() : builderId;
298                this.httpMethod = builderHttpMethod;
299                this.corsPreflight = this.httpMethod == HttpMethod.OPTIONS ? CorsPreflight.fromHeaders(this.headers).orElse(null) : null;
300                this.cors = this.corsPreflight == null ? Cors.fromHeaders(this.httpMethod, this.headers).orElse(null) : null;
301                this.resourcePath = this.path.equals("*") ? ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH : ResourcePath.withPath(this.path);
302                this.multipart = this.contentType != null && this.contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/");
303                this.contentTooLarge = builderContentTooLarge == null ? false : builderContentTooLarge;
304
305                // It's illegal to specify a body if the request is marked "content too large"
306                this.body = this.contentTooLarge ? null : builderBody;
307
308                // Cookies, form parameters, and multipart data are lazily parsed/instantiated when callers try to access them
309        }
310
311        @Override
312        public String toString() {
313                return format("%s{id=%s, httpMethod=%s, path=%s, cookies=%s, queryParameters=%s, headers=%s, body=%s}",
314                                getClass().getSimpleName(), getId(), getHttpMethod(), getPath(), getCookies(), getQueryParameters(),
315                                getHeaders(), format("%d bytes", getBody().isPresent() ? getBody().get().length : 0));
316        }
317
318        @Override
319        public boolean equals(@Nullable Object object) {
320                if (this == object)
321                        return true;
322
323                if (!(object instanceof Request request))
324                        return false;
325
326                return Objects.equals(getId(), request.getId())
327                                && Objects.equals(getHttpMethod(), request.getHttpMethod())
328                                && Objects.equals(getPath(), request.getPath())
329                                && Objects.equals(getQueryParameters(), request.getQueryParameters())
330                                && Objects.equals(getHeaders(), request.getHeaders())
331                                && Arrays.equals(this.body, request.body)
332                                && Objects.equals(isContentTooLarge(), request.isContentTooLarge());
333        }
334
335        @Override
336        public int hashCode() {
337                return Objects.hash(getId(), getHttpMethod(), getPath(), getQueryParameters(), getHeaders(), Arrays.hashCode(this.body), isContentTooLarge());
338        }
339
340        /**
341         * An application-specific identifier for this request.
342         * <p>
343         * The identifier is not necessarily unique (for example, numbers that "wrap around" if they get too large).
344         *
345         * @return the request's identifier
346         */
347        @Nonnull
348        public Object getId() {
349                return this.id;
350        }
351
352        /**
353         * The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP method</a> for this request.
354         *
355         * @return the request's HTTP method
356         */
357        @Nonnull
358        public HttpMethod getHttpMethod() {
359                return this.httpMethod;
360        }
361
362        /**
363         * The percent-decoded path component of this request (no query string).
364         *
365         * @return the path for this request
366         */
367        @Nonnull
368        public String getPath() {
369                return this.path;
370        }
371
372        /**
373         * Convenience method to acquire a {@link ResourcePath} representation of {@link #getPath()}.
374         *
375         * @return the resource path for this request
376         */
377        @Nonnull
378        public ResourcePath getResourcePath() {
379                return this.resourcePath;
380        }
381
382        /**
383         * The cookies provided by the client for this request.
384         * <p>
385         * The keys are the {@code Cookie} header names and the values are {@code Cookie} header values
386         * (it is possible for a client to send multiple {@code Cookie} headers with the same name).
387         * <p>
388         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
389         * <p>
390         * Use {@link #getCookie(String)} for a convenience method to access cookie values when only one is expected.
391         *
392         * @return the request's cookies
393         */
394        @Nonnull
395        public Map<String, Set<String>> getCookies() {
396                Map<String, Set<String>> result = this.cookies;
397
398                if (result == null) {
399                        getLock().lock();
400
401                        try {
402                                result = this.cookies;
403
404                                if (result == null) {
405                                        result = Collections.unmodifiableMap(Utilities.extractCookiesFromHeaders(getHeaders()));
406                                        this.cookies = result;
407                                }
408                        } finally {
409                                getLock().unlock();
410                        }
411                }
412
413                return result;
414        }
415
416        /**
417         * The decoded query parameters provided by the client for this request.
418         * <p>
419         * The keys are the query parameter names and the values are query parameter values
420         * (it is possible for a client to send multiple query parameters with the same name, e.g. {@code ?test=1&test=2}).
421         * <p>
422         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
423         * <p>
424         * Use {@link #getQueryParameter(String)} for a convenience method to access query parameter values when only one is expected.
425         *
426         * @return the request's query parameters
427         */
428        @Nonnull
429        public Map<String, Set<String>> getQueryParameters() {
430                return this.queryParameters;
431        }
432
433        /**
434         * The decoded HTML {@code application/x-www-form-urlencoded} form parameters provided by the client for this request.
435         * <p>
436         * The keys are the form parameter names and the values are form parameter values
437         * (it is possible for a client to send multiple form parameters with the same name, e.g. {@code ?test=1&test=2}).
438         * <p>
439         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
440         * <p>
441         * Use {@link #getFormParameter(String)} for a convenience method to access form parameter values when only one is expected.
442         *
443         * @return the request's form parameters
444         */
445        @Nonnull
446        public Map<String, Set<String>> getFormParameters() {
447                Map<String, Set<String>> result = this.formParameters;
448
449                if (result == null) {
450                        getLock().lock();
451                        try {
452                                result = this.formParameters;
453
454                                if (result == null) {
455                                        if (this.body != null && this.contentType != null && this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) {
456                                                String bodyAsString = getBodyAsString().orElse(null);
457                                                result = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, getCharset().orElse(DEFAULT_CHARSET)));
458                                        } else {
459                                                result = Map.of();
460                                        }
461
462                                        this.formParameters = result;
463                                }
464                        } finally {
465                                getLock().unlock();
466                        }
467                }
468
469                return result;
470        }
471
472        /**
473         * The raw (un-decoded) path component of this request exactly as the client specified.
474         * <p>
475         * For example, {@code "/a%20b"} (never decoded).
476         * <p>
477         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
478         * generated by encoding the decoded path, which may not exactly match the original wire format.</em>
479         *
480         * @return the raw path for this request
481         */
482        @Nonnull
483        public String getRawPath() {
484                return this.rawPath;
485        }
486
487        /**
488         * The raw (un-decoded) query component of this request exactly as the client specified.
489         * <p>
490         * For example, {@code "a=b&c=d+e"} (never decoded).
491         * <p>
492         * This is useful for special cases like HMAC signature verification, which relies on the exact client format.
493         * <p>
494         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
495         * generated by encoding the decoded query parameters, which may not exactly match the original wire format.</em>
496         *
497         * @return the raw query for this request, or {@link Optional#empty()} if none was specified
498         */
499        @Nonnull
500        public Optional<String> getRawQuery() {
501                return Optional.ofNullable(this.rawQuery);
502        }
503
504        /**
505         * The raw (un-decoded) path and query components of this request exactly as the client specified.
506         * <p>
507         * For example, {@code "/my%20path?a=b&c=d%20e"} (never decoded).
508         * <p>
509         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
510         * generated by encoding the decoded path and query parameters, which may not exactly match the original wire format.</em>
511         *
512         * @return the raw path and query for this request
513         */
514        @Nonnull
515        public String getRawPathAndQuery() {
516                if (this.rawQuery == null)
517                        return this.rawPath;
518
519                return this.rawPath + "?" + this.rawQuery;
520        }
521
522        /**
523         * The headers provided by the client for this request.
524         * <p>
525         * The keys are the header names and the values are header values
526         * (it is possible for a client to send multiple headers with the same name).
527         * <p>
528         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
529         * <p>
530         * Use {@link #getHeader(String)} for a convenience method to access header values when only one is expected.
531         *
532         * @return the request's headers
533         */
534        @Nonnull
535        public Map<String, Set<String>> getHeaders() {
536                return this.headers;
537        }
538
539        /**
540         * The {@code Content-Type} header value, as specified by the client.
541         *
542         * @return the request's {@code Content-Type} header value, or {@link Optional#empty()} if not specified
543         */
544        @Nonnull
545        public Optional<String> getContentType() {
546                return Optional.ofNullable(this.contentType);
547        }
548
549        /**
550         * The request's character encoding, as specified by the client in the {@code Content-Type} header value.
551         *
552         * @return the request's character encoding, or {@link Optional#empty()} if not specified
553         */
554        @Nonnull
555        public Optional<Charset> getCharset() {
556                return Optional.ofNullable(this.charset);
557        }
558
559        /**
560         * Is this a request with {@code Content-Type} of {@code multipart/form-data}?
561         *
562         * @return {@code true} if this is a {@code multipart/form-data} request, {@code false} otherwise
563         */
564        @Nonnull
565        public Boolean isMultipart() {
566                return this.multipart;
567        }
568
569        /**
570         * The decoded HTML {@code multipart/form-data} fields provided by the client for this request.
571         * <p>
572         * The keys are the multipart field names and the values are multipart field values
573         * (it is possible for a client to send multiple multipart fields with the same name).
574         * <p>
575         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
576         * <p>
577         * Use {@link #getMultipartField(String)} for a convenience method to access a multipart parameter field value when only one is expected.
578         * <p>
579         * When using Soklet's default {@link Server}, multipart fields are parsed using the {@link MultipartParser} as configured by {@link Server.Builder#multipartParser(MultipartParser)}.
580         *
581         * @return the request's multipart fields, or the empty map if none are present
582         */
583        @Nonnull
584        public Map<String, Set<MultipartField>> getMultipartFields() {
585                if (!isMultipart())
586                        return Map.of();
587
588                Map<String, Set<MultipartField>> result = this.multipartFields;
589
590                if (result == null) {
591                        getLock().lock();
592                        try {
593                                result = this.multipartFields;
594
595                                if (result == null) {
596                                        result = Collections.unmodifiableMap(getMultipartParser().extractMultipartFields(this));
597                                        this.multipartFields = result;
598                                }
599                        } finally {
600                                getLock().unlock();
601                        }
602                }
603
604                return result;
605        }
606
607        /**
608         * The raw bytes of the request body - <strong>callers should not modify this array; it is not defensively copied for performance reasons</strong>.
609         * <p>
610         * For convenience, {@link #getBodyAsString()} is available if you expect your request body to be of type {@link String}.
611         *
612         * @return the request body bytes, or {@link Optional#empty()} if none was supplied
613         */
614        @Nonnull
615        public Optional<byte[]> getBody() {
616                return Optional.ofNullable(this.body);
617
618                // Note: it would be nice to defensively copy, but it's inefficient
619                // return Optional.ofNullable(this.body == null ? null : Arrays.copyOf(this.body, this.body.length));
620        }
621
622        /**
623         * Was this request too large for the server to handle?
624         * <p>
625         * <em>If so, this request might have incomplete sets of headers/cookies. It will always have a zero-length body.</em>
626         * <p>
627         * Soklet is designed to power systems that exchange small "transactional" payloads that live entirely in memory. It is not appropriate for handling multipart files at scale, buffering uploads to disk, streaming, etc.
628         * <p>
629         * When using Soklet's default {@link Server}, maximum request size is configured by {@link Server.Builder#maximumRequestSizeInBytes(Integer)}.
630         *
631         * @return {@code true} if this request is larger than the server is able to handle, {@code false} otherwise
632         */
633        @Nonnull
634        public Boolean isContentTooLarge() {
635                return this.contentTooLarge;
636        }
637
638        /**
639         * Convenience method that provides the {@link #getBody()} bytes as a {@link String} encoded using the client-specified character set per {@link #getCharset()}.
640         * <p>
641         * If no character set is specified, {@link StandardCharsets#UTF_8} is used to perform the encoding.
642         * <p>
643         * This method will lazily convert the raw bytes as specified by {@link #getBody()} to an instance of {@link String} when first invoked.  The {@link String} representation is then cached and re-used for subsequent invocations.
644         * <p>
645         * This method is threadsafe.
646         *
647         * @return a {@link String} representation of this request's body, or {@link Optional#empty()} if no request body was specified by the client
648         */
649        @Nonnull
650        public Optional<String> getBodyAsString() {
651                // Lazily instantiate a string instance using double-checked locking
652                String result = this.bodyAsString;
653
654                if (this.body != null && result == null) {
655                        getLock().lock();
656
657                        try {
658                                result = this.bodyAsString;
659
660                                if (this.body != null && result == null) {
661                                        result = new String(this.body, getCharset().orElse(DEFAULT_CHARSET));
662                                        this.bodyAsString = result;
663                                }
664                        } finally {
665                                getLock().unlock();
666                        }
667                }
668
669                return Optional.ofNullable(result);
670        }
671
672        /**
673         * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Non-preflight CORS</a> request data.
674         * <p>
675         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
676         *
677         * @return non-preflight CORS request data, or {@link Optional#empty()} if none was specified
678         */
679        @Nonnull
680        public Optional<Cors> getCors() {
681                return Optional.ofNullable(this.cors);
682        }
683
684        /**
685         * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related request data.
686         * <p>
687         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
688         *
689         * @return preflight CORS request data, or {@link Optional#empty()} if none was specified
690         */
691        @Nonnull
692        public Optional<CorsPreflight> getCorsPreflight() {
693                return Optional.ofNullable(this.corsPreflight);
694        }
695
696        /**
697         * Locale information for this request as specified by {@code Accept-Language} header value[s] and ordered by weight as defined by <a href="https://www.rfc-editor.org/rfc/rfc7231#section-5.3.5">RFC 7231, Section 5.3.5</a>.
698         * <p>
699         * This method will lazily parse {@code Accept-Language} header values into to an ordered {@link List} of {@link Locale} when first invoked.  This representation is then cached and re-used for subsequent invocations.
700         * <p>
701         * This method is threadsafe.
702         * <p>
703         * See {@link #getLanguageRanges()} for a variant that pulls {@link LanguageRange} values.
704         *
705         * @return locale information for this request, or the empty list if none was specified
706         */
707        @Nonnull
708        public List<Locale> getLocales() {
709                // Lazily instantiate our parsed locales using double-checked locking
710                List<Locale> result = this.locales;
711
712                if (result == null) {
713                        getLock().lock();
714
715                        try {
716                                result = this.locales;
717
718                                if (result == null) {
719                                        Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language");
720
721                                        if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) {
722                                                // Support data spread across multiple header lines, which spec allows
723                                                String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream()
724                                                                .filter(value -> trimAggressivelyToEmpty(value).length() > 0)
725                                                                .collect(Collectors.joining(","));
726
727                                                try {
728                                                        result = unmodifiableList(Utilities.extractLocalesFromAcceptLanguageHeaderValue(acceptLanguageHeaderValue));
729                                                } catch (Exception ignored) {
730                                                        // Malformed Accept-Language header; ignore it
731                                                        result = List.of();
732                                                }
733                                        } else {
734                                                result = List.of();
735                                        }
736
737                                        this.locales = result;
738                                }
739                        } finally {
740                                getLock().unlock();
741                        }
742                }
743
744                return result;
745        }
746
747        /**
748         * {@link LanguageRange} information for this request as specified by {@code Accept-Language} header value[s].
749         * <p>
750         * This method will lazily parse {@code Accept-Language} header values into to an ordered {@link List} of {@link LanguageRange} when first invoked.  This representation is then cached and re-used for subsequent invocations.
751         * <p>
752         * This method is threadsafe.
753         * <p>
754         * See {@link #getLocales()} for a variant that pulls {@link Locale} values.
755         *
756         * @return language range information for this request, or the empty list if none was specified
757         */
758        @Nonnull
759        public List<LanguageRange> getLanguageRanges() {
760                // Lazily instantiate our parsed language ranges using double-checked locking
761                List<LanguageRange> result = this.languageRanges;
762
763                if (result == null) {
764                        getLock().lock();
765                        try {
766                                result = this.languageRanges;
767
768                                if (result == null) {
769                                        Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language");
770
771                                        if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) {
772                                                // Support data spread across multiple header lines, which spec allows
773                                                String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream()
774                                                                .filter(value -> trimAggressivelyToEmpty(value).length() > 0)
775                                                                .collect(Collectors.joining(","));
776
777                                                try {
778                                                        result = Collections.unmodifiableList(LanguageRange.parse(acceptLanguageHeaderValue));
779                                                } catch (Exception ignored) {
780                                                        // Malformed Accept-Language header; ignore it
781                                                        result = List.of();
782                                                }
783                                        } else {
784                                                result = List.of();
785                                        }
786
787                                        this.languageRanges = result;
788                                }
789                        } finally {
790                                getLock().unlock();
791                        }
792                }
793
794                return result;
795        }
796
797        /**
798         * Convenience method to access a decoded query parameter's value when at most one is expected for the given {@code name}.
799         * <p>
800         * If a query parameter {@code name} can support multiple values, {@link #getQueryParameters()} should be used instead of this method.
801         * <p>
802         * If this method is invoked for a query parameter {@code name} with multiple values, Soklet will throw {@link IllegalQueryParameterException}.
803         * <p>
804         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
805         *
806         * @param name the name of the query parameter
807         * @return the value for the query parameter, or {@link Optional#empty()} if none is present
808         * @throws IllegalQueryParameterException if the query parameter with the given {@code name} has multiple values
809         */
810        @Nonnull
811        public Optional<String> getQueryParameter(@Nonnull String name) {
812                requireNonNull(name);
813
814                try {
815                        return singleValueForName(name, getQueryParameters());
816                } catch (MultipleValuesException e) {
817                        @SuppressWarnings("unchecked")
818                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
819                        throw new IllegalQueryParameterException(format("Multiple values specified for query parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
820                }
821        }
822
823        /**
824         * Convenience method to access a decoded form parameter's value when at most one is expected for the given {@code name}.
825         * <p>
826         * If a form parameter {@code name} can support multiple values, {@link #getFormParameters()} should be used instead of this method.
827         * <p>
828         * If this method is invoked for a form parameter {@code name} with multiple values, Soklet will throw {@link IllegalFormParameterException}.
829         * <p>
830         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
831         *
832         * @param name the name of the form parameter
833         * @return the value for the form parameter, or {@link Optional#empty()} if none is present
834         * @throws IllegalFormParameterException if the form parameter with the given {@code name} has multiple values
835         */
836        @Nonnull
837        public Optional<String> getFormParameter(@Nonnull String name) {
838                requireNonNull(name);
839
840                try {
841                        return singleValueForName(name, getFormParameters());
842                } catch (MultipleValuesException e) {
843                        @SuppressWarnings("unchecked")
844                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
845                        throw new IllegalFormParameterException(format("Multiple values specified for form parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
846                }
847        }
848
849        /**
850         * Convenience method to access a header's value when at most one is expected for the given {@code name}.
851         * <p>
852         * If a header {@code name} can support multiple values, {@link #getHeaders()} should be used instead of this method.
853         * <p>
854         * If this method is invoked for a header {@code name} with multiple values, Soklet will throw {@link IllegalRequestHeaderException}.
855         * <p>
856         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
857         *
858         * @param name the name of the header
859         * @return the value for the header, or {@link Optional#empty()} if none is present
860         * @throws IllegalRequestHeaderException if the header with the given {@code name} has multiple values
861         */
862        @Nonnull
863        public Optional<String> getHeader(@Nonnull String name) {
864                requireNonNull(name);
865
866                try {
867                        return singleValueForName(name, getHeaders());
868                } catch (MultipleValuesException e) {
869                        @SuppressWarnings("unchecked")
870                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
871                        throw new IllegalRequestHeaderException(format("Multiple values specified for request header '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
872                }
873        }
874
875        /**
876         * Convenience method to access a cookie's value when at most one is expected for the given {@code name}.
877         * <p>
878         * If a cookie {@code name} can support multiple values, {@link #getCookies()} should be used instead of this method.
879         * <p>
880         * If this method is invoked for a cookie {@code name} with multiple values, Soklet will throw {@link IllegalRequestCookieException}.
881         * <p>
882         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
883         *
884         * @param name the name of the cookie
885         * @return the value for the cookie, or {@link Optional#empty()} if none is present
886         * @throws IllegalRequestCookieException if the cookie with the given {@code name} has multiple values
887         */
888        @Nonnull
889        public Optional<String> getCookie(@Nonnull String name) {
890                requireNonNull(name);
891
892                try {
893                        return singleValueForName(name, getCookies());
894                } catch (MultipleValuesException e) {
895                        @SuppressWarnings("unchecked")
896                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
897                        throw new IllegalRequestCookieException(format("Multiple values specified for request cookie '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
898                }
899        }
900
901        /**
902         * Convenience method to access a decoded multipart field when at most one is expected for the given {@code name}.
903         * <p>
904         * If a {@code name} can support multiple multipart fields, {@link #getMultipartFields()} should be used instead of this method.
905         * <p>
906         * If this method is invoked for a {@code name} with multiple multipart field values, Soklet will throw {@link IllegalMultipartFieldException}.
907         * <p>
908         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
909         *
910         * @param name the name of the multipart field
911         * @return the multipart field value, or {@link Optional#empty()} if none is present
912         * @throws IllegalMultipartFieldException if the multipart field with the given {@code name} has multiple values
913         */
914        @Nonnull
915        public Optional<MultipartField> getMultipartField(@Nonnull String name) {
916                requireNonNull(name);
917
918                try {
919                        return singleValueForName(name, getMultipartFields());
920                } catch (MultipleValuesException e) {
921                        @SuppressWarnings("unchecked")
922                        MultipartField firstMultipartField = getMultipartFields().get(name).stream().findFirst().get();
923                        String valuesAsString = format("[%s]", e.getValues().stream()
924                                        .map(multipartField -> multipartField.toString())
925                                        .collect(Collectors.joining(", ")));
926
927                        throw new IllegalMultipartFieldException(format("Multiple values specified for multipart field '%s' (but expected single value): %s", name, valuesAsString), firstMultipartField);
928                }
929        }
930
931        @Nonnull
932        private MultipartParser getMultipartParser() {
933                return this.multipartParser;
934        }
935
936        @Nonnull
937        private IdGenerator<?> getIdGenerator() {
938                return this.idGenerator;
939        }
940
941        @Nonnull
942        private ReentrantLock getLock() {
943                return this.lock;
944        }
945
946        @Nonnull
947        private <T> Optional<T> singleValueForName(@Nonnull String name,
948                                                                                                                                                                                 @Nullable Map<String, Set<T>> valuesByName) throws MultipleValuesException {
949                if (valuesByName == null)
950                        return Optional.empty();
951
952                Set<T> values = valuesByName.get(name);
953
954                if (values == null)
955                        return Optional.empty();
956
957                if (values.size() > 1)
958                        throw new MultipleValuesException(name, values);
959
960                return values.stream().findFirst();
961        }
962
963        @NotThreadSafe
964        private static class MultipleValuesException extends Exception {
965                @Nonnull
966                private final String name;
967                @Nonnull
968                private final Set<?> values;
969
970                private MultipleValuesException(@Nonnull String name,
971                                                                                                                                                @Nonnull Set<?> values) {
972                        super(format("Expected single value but found %d values for '%s': %s", values.size(), name, values));
973
974                        requireNonNull(name);
975                        requireNonNull(values);
976
977                        this.name = name;
978                        this.values = Collections.unmodifiableSet(new LinkedHashSet<>(values));
979                }
980
981                @Nonnull
982                public String getName() {
983                        return this.name;
984                }
985
986                @Nonnull
987                public Set<?> getValues() {
988                        return this.values;
989                }
990        }
991
992        /**
993         * Builder used to construct instances of {@link Request} via {@link Request#withRawUrl(HttpMethod, String)}.
994         * <p>
995         * This class is intended for use by a single thread.
996         *
997         * @author <a href="https://www.revetkn.com">Mark Allen</a>
998         */
999        @NotThreadSafe
1000        public static final class RawBuilder {
1001                @Nonnull
1002                private HttpMethod httpMethod;
1003                @Nonnull
1004                private String rawUrl;
1005                @Nullable
1006                private Object id;
1007                @Nullable
1008                private IdGenerator idGenerator;
1009                @Nullable
1010                private MultipartParser multipartParser;
1011                @Nullable
1012                private Map<String, Set<String>> headers;
1013                @Nullable
1014                private byte[] body;
1015                @Nullable
1016                private Boolean contentTooLarge;
1017
1018                protected RawBuilder(@Nonnull HttpMethod httpMethod,
1019                                                                                                 @Nonnull String rawUrl) {
1020                        requireNonNull(httpMethod);
1021                        requireNonNull(rawUrl);
1022
1023                        this.httpMethod = httpMethod;
1024                        this.rawUrl = rawUrl;
1025                }
1026
1027                @Nonnull
1028                public RawBuilder httpMethod(@Nonnull HttpMethod httpMethod) {
1029                        requireNonNull(httpMethod);
1030                        this.httpMethod = httpMethod;
1031                        return this;
1032                }
1033
1034                @Nonnull
1035                public RawBuilder rawUrl(@Nonnull String rawUrl) {
1036                        requireNonNull(rawUrl);
1037                        this.rawUrl = rawUrl;
1038                        return this;
1039                }
1040
1041                @Nonnull
1042                public RawBuilder id(@Nullable Object id) {
1043                        this.id = id;
1044                        return this;
1045                }
1046
1047                @Nonnull
1048                public RawBuilder idGenerator(@Nullable IdGenerator idGenerator) {
1049                        this.idGenerator = idGenerator;
1050                        return this;
1051                }
1052
1053                @Nonnull
1054                public RawBuilder multipartParser(@Nullable MultipartParser multipartParser) {
1055                        this.multipartParser = multipartParser;
1056                        return this;
1057                }
1058
1059                @Nonnull
1060                public RawBuilder headers(@Nullable Map<String, Set<String>> headers) {
1061                        this.headers = headers;
1062                        return this;
1063                }
1064
1065                @Nonnull
1066                public RawBuilder body(@Nullable byte[] body) {
1067                        this.body = body;
1068                        return this;
1069                }
1070
1071                @Nonnull
1072                public RawBuilder contentTooLarge(@Nullable Boolean contentTooLarge) {
1073                        this.contentTooLarge = contentTooLarge;
1074                        return this;
1075                }
1076
1077                @Nonnull
1078                public Request build() {
1079                        return new Request(this, null);
1080                }
1081        }
1082
1083        /**
1084         * Builder used to construct instances of {@link Request} via {@link Request#withPath(HttpMethod, String)}.
1085         * <p>
1086         * This class is intended for use by a single thread.
1087         *
1088         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1089         */
1090        @NotThreadSafe
1091        public static final class PathBuilder {
1092                @Nonnull
1093                private HttpMethod httpMethod;
1094                @Nonnull
1095                private String path;
1096                @Nullable
1097                private String rawPath;
1098                @Nullable
1099                private String rawQuery;
1100                @Nullable
1101                private Object id;
1102                @Nullable
1103                private IdGenerator idGenerator;
1104                @Nullable
1105                private MultipartParser multipartParser;
1106                @Nullable
1107                private Map<String, Set<String>> queryParameters;
1108                @Nullable
1109                private Map<String, Set<String>> headers;
1110                @Nullable
1111                private byte[] body;
1112                @Nullable
1113                private Boolean contentTooLarge;
1114
1115                protected PathBuilder(@Nonnull HttpMethod httpMethod,
1116                                                                                                        @Nonnull String path) {
1117                        requireNonNull(httpMethod);
1118                        requireNonNull(path);
1119
1120                        this.httpMethod = httpMethod;
1121                        this.path = path;
1122                }
1123
1124                @Nonnull
1125                public PathBuilder httpMethod(@Nonnull HttpMethod httpMethod) {
1126                        requireNonNull(httpMethod);
1127                        this.httpMethod = httpMethod;
1128                        return this;
1129                }
1130
1131                @Nonnull
1132                public PathBuilder path(@Nonnull String path) {
1133                        requireNonNull(path);
1134                        this.path = path;
1135                        return this;
1136                }
1137
1138                // Package-private setter for raw value (used by Copier)
1139                @Nonnull
1140                PathBuilder rawPath(@Nullable String rawPath) {
1141                        this.rawPath = rawPath;
1142                        return this;
1143                }
1144
1145                // Package-private setter for raw value (used by Copier)
1146                @Nonnull
1147                PathBuilder rawQuery(@Nullable String rawQuery) {
1148                        this.rawQuery = rawQuery;
1149                        return this;
1150                }
1151
1152                @Nonnull
1153                public PathBuilder id(@Nullable Object id) {
1154                        this.id = id;
1155                        return this;
1156                }
1157
1158                @Nonnull
1159                public PathBuilder idGenerator(@Nullable IdGenerator idGenerator) {
1160                        this.idGenerator = idGenerator;
1161                        return this;
1162                }
1163
1164                @Nonnull
1165                public PathBuilder multipartParser(@Nullable MultipartParser multipartParser) {
1166                        this.multipartParser = multipartParser;
1167                        return this;
1168                }
1169
1170                @Nonnull
1171                public PathBuilder queryParameters(@Nullable Map<String, Set<String>> queryParameters) {
1172                        this.queryParameters = queryParameters;
1173                        return this;
1174                }
1175
1176                @Nonnull
1177                public PathBuilder headers(@Nullable Map<String, Set<String>> headers) {
1178                        this.headers = headers;
1179                        return this;
1180                }
1181
1182                @Nonnull
1183                public PathBuilder body(@Nullable byte[] body) {
1184                        this.body = body;
1185                        return this;
1186                }
1187
1188                @Nonnull
1189                public PathBuilder contentTooLarge(@Nullable Boolean contentTooLarge) {
1190                        this.contentTooLarge = contentTooLarge;
1191                        return this;
1192                }
1193
1194                @Nonnull
1195                public Request build() {
1196                        return new Request(null, this);
1197                }
1198        }
1199
1200        /**
1201         * Builder used to copy instances of {@link Request} via {@link Request#copy()}.
1202         * <p>
1203         * This class is intended for use by a single thread.
1204         *
1205         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1206         */
1207        @NotThreadSafe
1208        public static final class Copier {
1209                @Nonnull
1210                private final PathBuilder builder;
1211
1212                // Track original raw values and modification state
1213                @Nullable
1214                private String originalRawPath;
1215                @Nullable
1216                private String originalRawQuery;
1217                @Nonnull
1218                private Boolean pathModified = false;
1219                @Nonnull
1220                private Boolean queryParametersModified = false;
1221
1222                Copier(@Nonnull Request request) {
1223                        requireNonNull(request);
1224
1225                        this.originalRawPath = request.getRawPath();
1226                        this.originalRawQuery = request.rawQuery; // Direct field access
1227
1228                        this.builder = new PathBuilder(request.getHttpMethod(), request.getPath())
1229                                        .id(request.getId())
1230                                        .queryParameters(new LinkedHashMap<>(request.getQueryParameters()))
1231                                        .headers(new LinkedCaseInsensitiveMap<>(request.getHeaders()))
1232                                        .body(request.body) // Direct field access to avoid array copy
1233                                        .multipartParser(request.getMultipartParser())
1234                                        .idGenerator(request.getIdGenerator())
1235                                        .contentTooLarge(request.isContentTooLarge())
1236                                        // Preserve original raw values initially
1237                                        .rawPath(this.originalRawPath)
1238                                        .rawQuery(this.originalRawQuery);
1239                }
1240
1241                @Nonnull
1242                public Copier httpMethod(@Nonnull HttpMethod httpMethod) {
1243                        requireNonNull(httpMethod);
1244                        this.builder.httpMethod(httpMethod);
1245                        return this;
1246                }
1247
1248                @Nonnull
1249                public Copier path(@Nonnull String path) {
1250                        requireNonNull(path);
1251                        this.pathModified = true;
1252                        // Clear preserved raw path since decoded path changed
1253                        this.builder.rawPath(null);
1254                        return this;
1255                }
1256
1257                @Nonnull
1258                public Copier id(@Nullable Object id) {
1259                        this.builder.id(id);
1260                        return this;
1261                }
1262
1263                @Nonnull
1264                public Copier queryParameters(@Nullable Map<String, Set<String>> queryParameters) {
1265                        this.builder.queryParameters(queryParameters);
1266                        this.queryParametersModified = true;
1267                        // Clear preserved raw query since decoded query parameters changed
1268                        this.builder.rawQuery(null);
1269                        return this;
1270                }
1271
1272                // Convenience method for mutation
1273                @Nonnull
1274                public Copier queryParameters(@Nonnull Consumer<Map<String, Set<String>>> queryParametersConsumer) {
1275                        requireNonNull(queryParametersConsumer);
1276
1277                        if (this.builder.queryParameters == null)
1278                                this.builder.queryParameters(new LinkedHashMap<>());
1279
1280                        queryParametersConsumer.accept(this.builder.queryParameters);
1281                        this.queryParametersModified = true;
1282                        // Clear preserved raw query since decoded query parameters changed
1283                        this.builder.rawQuery(null);
1284                        return this;
1285                }
1286
1287                @Nonnull
1288                public Copier headers(@Nullable Map<String, Set<String>> headers) {
1289                        this.builder.headers(headers);
1290                        return this;
1291                }
1292
1293                // Convenience method for mutation
1294                @Nonnull
1295                public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) {
1296                        requireNonNull(headersConsumer);
1297
1298                        if (this.builder.headers == null)
1299                                this.builder.headers(new LinkedCaseInsensitiveMap<>());
1300
1301                        headersConsumer.accept(this.builder.headers);
1302                        return this;
1303                }
1304
1305                @Nonnull
1306                public Copier body(@Nullable byte[] body) {
1307                        this.builder.body(body);
1308                        return this;
1309                }
1310
1311                @Nonnull
1312                public Copier contentTooLarge(@Nullable Boolean contentTooLarge) {
1313                        this.builder.contentTooLarge(contentTooLarge);
1314                        return this;
1315                }
1316
1317                @Nonnull
1318                public Request finish() {
1319                        return this.builder.build();
1320                }
1321        }
1322}