001/*
002 * Copyright 2022-2026 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.microhttp.Header;
026import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
027import org.jspecify.annotations.NonNull;
028import org.jspecify.annotations.Nullable;
029
030import javax.annotation.concurrent.NotThreadSafe;
031import javax.annotation.concurrent.ThreadSafe;
032import java.net.InetSocketAddress;
033import java.nio.charset.Charset;
034import java.nio.charset.StandardCharsets;
035import java.util.ArrayList;
036import java.util.Arrays;
037import java.util.Collections;
038import java.util.LinkedHashMap;
039import java.util.LinkedHashSet;
040import java.util.List;
041import java.util.Locale;
042import java.util.Locale.LanguageRange;
043import java.util.Map;
044import java.util.Objects;
045import java.util.Optional;
046import java.util.Set;
047import java.util.concurrent.locks.ReentrantLock;
048import java.util.function.Consumer;
049import java.util.stream.Collectors;
050
051import static com.soklet.Utilities.trimAggressivelyToEmpty;
052import static com.soklet.Utilities.trimAggressivelyToNull;
053import static java.lang.String.format;
054import static java.util.Collections.unmodifiableList;
055import static java.util.Objects.requireNonNull;
056
057/**
058 * Encapsulates information specified in an HTTP request.
059 * <p>
060 * 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.
061 * Convenience instance factories are also available via {@link #fromRawUrl(HttpMethod, String)} and {@link #fromPath(HttpMethod, String)}.
062 * <p>
063 * 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.
064 * <p>
065 * For performance, collection values (headers, query parameters, form parameters, cookies, multipart fields) are shallow-copied and not defensively deep-copied. Treat returned collections as immutable.
066 * <p>
067 * Detailed documentation available at <a href="https://www.soklet.com/docs/request-handling">https://www.soklet.com/docs/request-handling</a>.
068 *
069 * @author <a href="https://www.revetkn.com">Mark Allen</a>
070 */
071@ThreadSafe
072public final class Request {
073        @NonNull
074        private static final Charset DEFAULT_CHARSET;
075        @NonNull
076        private static final IdGenerator DEFAULT_ID_GENERATOR;
077
078        static {
079                DEFAULT_CHARSET = StandardCharsets.UTF_8;
080                DEFAULT_ID_GENERATOR = DefaultIdGenerator.defaultInstance();
081        }
082
083        @NonNull
084        private final Object id;
085        @NonNull
086        private final HttpMethod httpMethod;
087        @NonNull
088        private final String rawPath;
089        @Nullable
090        private final String rawQuery;
091        @NonNull
092        private final String path;
093        @NonNull
094        private final ResourcePath resourcePath;
095        @NonNull
096        private final Boolean lazyQueryParameters;
097        @Nullable
098        private final String rawQueryForLazyParameters;
099        @Nullable
100        private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters;
101        @Nullable
102        private final String contentType;
103        @Nullable
104        private final Charset charset;
105        @NonNull
106        private final RequestHeaders headers;
107        @Nullable
108        private final TraceContext traceContext;
109        @Nullable
110        private final InetSocketAddress remoteAddress;
111        @Nullable
112        private final Cors cors;
113        @Nullable
114        private final CorsPreflight corsPreflight;
115        private final byte @Nullable [] body;
116        @NonNull
117        private final Integer encodedBodySizeInBytes;
118        @NonNull
119        private final Boolean multipart;
120        @NonNull
121        private final Boolean contentTooLarge;
122        @NonNull
123        private final MultipartParser multipartParser;
124        @NonNull
125        private final IdGenerator<?> idGenerator;
126        @NonNull
127        private final ReentrantLock lock;
128        @Nullable
129        private volatile String bodyAsString = null;
130        @Nullable
131        private volatile List<@NonNull Locale> locales = null;
132        @Nullable
133        private volatile List<@NonNull LanguageRange> languageRanges = null;
134        @Nullable
135        private volatile List<@NonNull MediaRange> mediaRanges = null;
136        @Nullable
137        private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> cookies = null;
138        @Nullable
139        private volatile Map<@NonNull String, @NonNull Set<@NonNull MultipartField>> multipartFields = null;
140        @Nullable
141        private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> formParameters = null;
142
143        /**
144         * Acquires a builder for {@link Request} instances from the URL provided by clients on a "raw" HTTP/1.1 request line.
145         * <p>
146         * 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://}).
147         * 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.
148         * <p>
149         * Note: request targets are normalized to origin-form. For example, if a client sends an absolute-form URL like {@code http://example.com/path?query}, only the path and query components are retained.
150         * <p>
151         * Paths will be percent-decoded. Percent-encoded slashes (e.g. {@code %2F}) are rejected.
152         * Malformed percent-encoding is rejected.
153         * <p>
154         * Query parameters are parsed and decoded using RFC 3986 semantics - see {@link QueryFormat#RFC_3986_STRICT}.
155         * Query decoding always uses UTF-8, regardless of any {@code Content-Type} charset.
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 rawUrl     the raw (un-decoded) URL for this request
161         * @return the builder
162         */
163        @NonNull
164        public static RawBuilder withRawUrl(@NonNull HttpMethod httpMethod,
165                                                                                                                                                        @NonNull String rawUrl) {
166                requireNonNull(httpMethod);
167                requireNonNull(rawUrl);
168
169                return new RawBuilder(httpMethod, rawUrl);
170        }
171
172        /**
173         * Creates a {@link Request} from a raw request target without additional customization.
174         *
175         * @param httpMethod the HTTP method
176         * @param rawUrl     a raw HTTP/1.1 request target (not URL-decoded)
177         * @return a {@link Request} instance
178         */
179        @NonNull
180        public static Request fromRawUrl(@NonNull HttpMethod httpMethod,
181                                                                                                                                         @NonNull String rawUrl) {
182                return withRawUrl(httpMethod, rawUrl).build();
183        }
184
185        /**
186         * Acquires a builder for {@link Request} instances from already-decoded path and query components - useful for manual construction, e.g. integration tests.
187         * <p>
188         * 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.
189         * <p>
190         * Query parameters must be specified via {@link PathBuilder#queryParameters(Map)} and are assumed to be already-decoded.
191         * <p>
192         * 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}.
193         *
194         * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.})
195         * @param path       the decoded URL path for this request
196         * @return the builder
197         */
198        @NonNull
199        public static PathBuilder withPath(@NonNull HttpMethod httpMethod,
200                                                                                                                                                 @NonNull String path) {
201                requireNonNull(httpMethod);
202                requireNonNull(path);
203
204                return new PathBuilder(httpMethod, path);
205        }
206
207        /**
208         * Creates a {@link Request} from a path without additional customization.
209         *
210         * @param httpMethod the HTTP method
211         * @param path       a decoded request path (e.g. {@code /widgets/123})
212         * @return a {@link Request} instance
213         */
214        @NonNull
215        public static Request fromPath(@NonNull HttpMethod httpMethod,
216                                                                                                                                 @NonNull String path) {
217                return withPath(httpMethod, path).build();
218        }
219
220        /**
221         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
222         *
223         * @return a copier for this instance
224         */
225        @NonNull
226        public Copier copy() {
227                return new Copier(this);
228        }
229
230        private Request(@Nullable RawBuilder rawBuilder,
231                                                                        @Nullable PathBuilder pathBuilder) {
232                // Should never occur
233                if (rawBuilder == null && pathBuilder == null)
234                        throw new IllegalStateException(format("Neither %s nor %s were specified", RawBuilder.class.getSimpleName(), PathBuilder.class.getSimpleName()));
235
236                IdGenerator builderIdGenerator;
237                Object builderId;
238                HttpMethod builderHttpMethod;
239                byte[] builderBody;
240                Integer builderEncodedBodySizeInBytes;
241                MultipartParser builderMultipartParser;
242                Boolean builderContentTooLarge;
243                RequestHeaders builderHeaders;
244                InetSocketAddress builderRemoteAddress;
245                Boolean builderTraceContextSpecified;
246                TraceContext builderTraceContext;
247
248                if (rawBuilder == null) {
249                        PathBuilder activePathBuilder = requireNonNull(pathBuilder);
250                        builderIdGenerator = activePathBuilder.idGenerator;
251                        builderId = activePathBuilder.id;
252                        builderHttpMethod = activePathBuilder.httpMethod;
253                        builderBody = activePathBuilder.body;
254                        builderEncodedBodySizeInBytes = activePathBuilder.encodedBodySizeInBytes;
255                        builderMultipartParser = activePathBuilder.multipartParser;
256                        builderContentTooLarge = activePathBuilder.contentTooLarge;
257                        builderHeaders = new MapRequestHeaders(activePathBuilder.headers);
258                        builderRemoteAddress = activePathBuilder.remoteAddress;
259                        builderTraceContextSpecified = activePathBuilder.traceContextSpecified;
260                        builderTraceContext = activePathBuilder.traceContext;
261                } else {
262                        RawBuilder activeRawBuilder = rawBuilder;
263                        builderIdGenerator = activeRawBuilder.idGenerator;
264                        builderId = activeRawBuilder.id;
265                        builderHttpMethod = activeRawBuilder.httpMethod;
266                        builderBody = activeRawBuilder.body;
267                        builderEncodedBodySizeInBytes = activeRawBuilder.encodedBodySizeInBytes;
268                        builderMultipartParser = activeRawBuilder.multipartParser;
269                        builderContentTooLarge = activeRawBuilder.contentTooLarge;
270                        builderHeaders = activeRawBuilder.requestHeaders();
271                        builderRemoteAddress = activeRawBuilder.remoteAddress;
272                        builderTraceContextSpecified = activeRawBuilder.traceContextSpecified;
273                        builderTraceContext = activeRawBuilder.traceContext;
274                }
275
276                this.idGenerator = builderIdGenerator == null ? DEFAULT_ID_GENERATOR : builderIdGenerator;
277                this.multipartParser = builderMultipartParser == null ? DefaultMultipartParser.defaultInstance() : builderMultipartParser;
278
279                this.headers = builderHeaders;
280                this.traceContext = builderTraceContextSpecified ? builderTraceContext : extractTraceContext(builderHeaders).orElse(null);
281                String contentTypeHeaderValue = firstHeaderValue(this.headers, "Content-Type").orElse(null);
282                this.contentType = Utilities.extractContentTypeFromHeaderValue(contentTypeHeaderValue).orElse(null);
283                this.charset = Utilities.extractCharsetFromHeaderValue(contentTypeHeaderValue).orElse(null);
284                this.remoteAddress = builderRemoteAddress;
285
286                String path;
287                String rawBuilderRawQuery = null;
288                String rawQueryForLazyParameters = null;
289                Boolean lazyQueryParameters = false;
290                @Nullable
291                Map<String, Set<String>> initialQueryParameters;
292
293                // If we use PathBuilder, use its path directly.
294                // If we use RawBuilder, parse and decode its path.
295                if (pathBuilder != null) {
296                        path = trimAggressivelyToEmpty(pathBuilder.path);
297
298                        // Validate path
299                        if (!path.startsWith("/") && !path.equals("*"))
300                                throw new IllegalRequestException("Path must start with '/' or be '*'");
301
302                        if (path.contains("?"))
303                                throw new IllegalRequestException(format("Path should not contain a query string. Use %s.withPath(...).queryParameters(...) to specify query parameters as a %s.",
304                                                Request.class.getSimpleName(), Map.class.getSimpleName()));
305
306                        // Use already-decoded query parameters as provided by the path builder
307                        initialQueryParameters = pathBuilder.queryParameters == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(pathBuilder.queryParameters));
308                } else {
309                        // RawBuilder scenario
310                        String rawUrl = trimAggressivelyToEmpty(requireNonNull(rawBuilder).rawUrl);
311
312                        // Special handling for OPTIONS *
313                        if ("*".equals(rawUrl)) {
314                                path = "*";
315                                initialQueryParameters = Map.of();
316                        } else {
317                                // First, parse and decode the path...
318                                path = Utilities.extractPathFromUrl(rawUrl, true);
319
320                                // ...then, retain raw query parameters for lazy decoding.
321                                rawBuilderRawQuery = rawUrl.contains("?") ? Utilities.extractRawQueryFromUrlStrict(rawUrl).orElse(null) : null;
322                                if (rawBuilderRawQuery != null) {
323                                        // We always assume RFC_3986_STRICT for query parameters because Soklet is for modern systems - HTML Form "GET" submissions are rare/legacy.
324                                        // This means we leave "+" as "+" (not decode to " ") and then apply any percent-decoding rules.
325                                        // Query parameters are decoded as UTF-8 regardless of Content-Type.
326                                        // In the future, we might expose a way to let applications prefer QueryFormat.X_WWW_FORM_URLENCODED instead, which treats "+" as a space
327                                        Utilities.validatePercentEncodingInUrlComponent(rawBuilderRawQuery);
328                                        initialQueryParameters = null;
329                                        lazyQueryParameters = true;
330                                        rawQueryForLazyParameters = rawBuilderRawQuery;
331                                } else {
332                                        initialQueryParameters = Map.of();
333                                }
334                        }
335                }
336
337                if (path.equals("*") && builderHttpMethod != HttpMethod.OPTIONS)
338                        throw new IllegalRequestException(format("Path '*' is only legal for HTTP %s", HttpMethod.OPTIONS.name()));
339
340                if (path.contains("\u0000") || path.contains("%00"))
341                        throw new IllegalRequestException(format("Illegal null byte in path '%s'", path));
342
343                this.path = path;
344
345                String rawPath;
346                String rawQuery;
347
348                if (pathBuilder != null) {
349                        // PathBuilder scenario: check if explicit raw values were provided
350                        if (pathBuilder.rawPath != null) {
351                                // Explicit raw values provided (e.g. from Copier preserving originals)
352                                rawPath = pathBuilder.rawPath;
353                                rawQuery = pathBuilder.rawQuery;
354                        } else {
355                                // No explicit raw values; encode from decoded values
356                                if (path.equals("*")) {
357                                        rawPath = "*";
358                                } else {
359                                        rawPath = Utilities.encodePath(path);
360                                }
361
362                                Map<String, Set<String>> queryParameters = requireNonNull(initialQueryParameters);
363                                if (queryParameters.isEmpty()) {
364                                        rawQuery = null;
365                                } else {
366                                        rawQuery = Utilities.encodeQueryParameters(queryParameters, QueryFormat.RFC_3986_STRICT);
367                                }
368                        }
369                } else {
370                        // RawBuilder scenario: extract raw components from rawUrl
371                        String rawUrl = trimAggressivelyToEmpty(requireNonNull(rawBuilder).rawUrl);
372
373                        if ("*".equals(rawUrl)) {
374                                rawPath = "*";
375                                rawQuery = null;
376                        } else {
377                                rawPath = Utilities.extractPathFromUrl(rawUrl, false);
378                                if (containsEncodedSlash(rawPath))
379                                        throw new IllegalRequestException("Encoded slashes are not allowed in request paths");
380                                rawQuery = rawBuilderRawQuery;
381                        }
382                }
383
384                this.rawPath = rawPath;
385                this.rawQuery = rawQuery;
386                this.queryParameters = initialQueryParameters;
387                this.lazyQueryParameters = lazyQueryParameters;
388                this.rawQueryForLazyParameters = rawQueryForLazyParameters;
389
390                this.lock = new ReentrantLock();
391                this.httpMethod = builderHttpMethod;
392                this.corsPreflight = this.httpMethod == HttpMethod.OPTIONS ? extractCorsPreflight(this.headers).orElse(null) : null;
393                this.cors = this.corsPreflight == null ? extractCors(this.httpMethod, this.headers).orElse(null) : null;
394                this.resourcePath = this.path.equals("*") ? ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH : ResourcePath.fromPath(this.path);
395                this.multipart = this.contentType != null && this.contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/");
396                this.contentTooLarge = builderContentTooLarge == null ? false : builderContentTooLarge;
397
398                // It's illegal to specify a body if the request is marked "content too large"
399                this.body = this.contentTooLarge ? null : builderBody;
400                this.encodedBodySizeInBytes = builderEncodedBodySizeInBytes == null
401                                ? (builderBody == null ? 0 : builderBody.length)
402                                : builderEncodedBodySizeInBytes;
403
404                if (this.encodedBodySizeInBytes < 0)
405                        throw new IllegalArgumentException("Encoded body size must be >= 0");
406
407                // Last step of ctor: generate an ID (if necessary) using this fully-constructed Request
408                this.id = builderId == null ? this.idGenerator.generateId(this) : builderId;
409
410                // Note that cookies, form parameters, and multipart data are lazily parsed/instantiated when callers try to access them
411        }
412
413        @Override
414        public String toString() {
415                return format("%s{id=%s, httpMethod=%s, path=%s, cookies=%s, queryParameters=%s, headers=%s, body=%s}",
416                                getClass().getSimpleName(), getId(), getHttpMethod(), getPath(), getCookies(), getQueryParameters(),
417                                getHeaders(), format("%d bytes", getBody().isPresent() ? getBody().get().length : 0));
418        }
419
420        @Override
421        public boolean equals(@Nullable Object object) {
422                if (this == object)
423                        return true;
424
425                if (!(object instanceof Request request))
426                        return false;
427
428                return Objects.equals(getId(), request.getId())
429                                && Objects.equals(getHttpMethod(), request.getHttpMethod())
430                                && Objects.equals(getPath(), request.getPath())
431                                && Objects.equals(getQueryParameters(), request.getQueryParameters())
432                                && Objects.equals(getHeaders(), request.getHeaders())
433                                && Objects.equals(getTraceContext(), request.getTraceContext())
434                                && Arrays.equals(this.body, request.body)
435                                && Objects.equals(isContentTooLarge(), request.isContentTooLarge());
436        }
437
438        @Override
439        public int hashCode() {
440                return Objects.hash(getId(), getHttpMethod(), getPath(), getQueryParameters(), getHeaders(), getTraceContext(), Arrays.hashCode(this.body), isContentTooLarge());
441        }
442
443        private static boolean containsEncodedSlash(@NonNull String rawPath) {
444                requireNonNull(rawPath);
445                return rawPath.toLowerCase(Locale.ROOT).contains("%2f");
446        }
447
448        /**
449         * An application-specific identifier for this request.
450         * <p>
451         * The identifier is not necessarily unique (for example, numbers that "wrap around" if they get too large).
452         *
453         * @return the request's identifier
454         */
455        @NonNull
456        public Object getId() {
457                return this.id;
458        }
459
460        /**
461         * The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP method</a> for this request.
462         *
463         * @return the request's HTTP method
464         */
465        @NonNull
466        public HttpMethod getHttpMethod() {
467                return this.httpMethod;
468        }
469
470        /**
471         * The percent-decoded path component of this request (no query string).
472         *
473         * @return the path for this request
474         */
475        @NonNull
476        public String getPath() {
477                return this.path;
478        }
479
480        /**
481         * Convenience method to acquire a {@link ResourcePath} representation of {@link #getPath()}.
482         *
483         * @return the resource path for this request
484         */
485        @NonNull
486        public ResourcePath getResourcePath() {
487                return this.resourcePath;
488        }
489
490        /**
491         * The cookies provided by the client for this request.
492         * <p>
493         * The keys are the {@code Cookie} header names and the values are {@code Cookie} header values
494         * (it is possible for a client to send multiple {@code Cookie} headers with the same name).
495         * <p>
496         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
497         * <p>
498         * Use {@link #getCookie(String)} for a convenience method to access cookie values when only one is expected.
499         *
500         * @return the request's cookies
501         */
502        @NonNull
503        public Map<@NonNull String, @NonNull Set<@NonNull String>> getCookies() {
504                Map<String, Set<String>> result = this.cookies;
505
506                if (result == null) {
507                        getLock().lock();
508
509                        try {
510                                result = this.cookies;
511
512                                if (result == null) {
513                                        Set<String> cookieHeaderValues = getHeaderValues("Cookie").orElse(Set.of());
514                                        result = cookieHeaderValues.isEmpty()
515                                                        ? Map.of()
516                                                        : Collections.unmodifiableMap(Utilities.extractCookiesFromHeaders(Map.of("Cookie", cookieHeaderValues)));
517                                        this.cookies = result;
518                                }
519                        } finally {
520                                getLock().unlock();
521                        }
522                }
523
524                return result;
525        }
526
527        /**
528         * The decoded query parameters provided by the client for this request.
529         * <p>
530         * The keys are the query parameter names and the values are query parameter values
531         * (it is possible for a client to send multiple query parameters with the same name, e.g. {@code ?test=1&test=2}).
532         * <p>
533         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
534         * <p>
535         * Use {@link #getQueryParameter(String)} for a convenience method to access query parameter values when only one is expected.
536         *
537         * @return the request's query parameters
538         */
539        @NonNull
540        public Map<@NonNull String, @NonNull Set<@NonNull String>> getQueryParameters() {
541                Map<String, Set<String>> result = this.queryParameters;
542
543                if (result == null && this.lazyQueryParameters) {
544                        getLock().lock();
545
546                        try {
547                                result = this.queryParameters;
548
549                                if (result == null) {
550                                        result = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(requireNonNull(this.rawQueryForLazyParameters), QueryFormat.RFC_3986_STRICT, DEFAULT_CHARSET));
551                                        this.queryParameters = result;
552                                }
553                        } finally {
554                                getLock().unlock();
555                        }
556                }
557
558                return result == null ? Map.of() : result;
559        }
560
561        /**
562         * The decoded HTML {@code application/x-www-form-urlencoded} form parameters provided by the client for this request.
563         * <p>
564         * The keys are the form parameter names and the values are form parameter values
565         * (it is possible for a client to send multiple form parameters with the same name, e.g. {@code ?test=1&test=2}).
566         * <p>
567         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
568         * <p>
569         * Use {@link #getFormParameter(String)} for a convenience method to access form parameter values when only one is expected.
570         *
571         * @return the request's form parameters
572         */
573        @NonNull
574        public Map<@NonNull String, @NonNull Set<@NonNull String>> getFormParameters() {
575                Map<String, Set<String>> result = this.formParameters;
576
577                if (result == null) {
578                        getLock().lock();
579                        try {
580                                result = this.formParameters;
581
582                                if (result == null) {
583                                        if (this.body != null && this.contentType != null && this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) {
584                                                String bodyAsString = getBodyAsString().orElse("");
585                                                result = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, getCharset().orElse(DEFAULT_CHARSET)));
586                                        } else {
587                                                result = Map.of();
588                                        }
589
590                                        this.formParameters = result;
591                                }
592                        } finally {
593                                getLock().unlock();
594                        }
595                }
596
597                return result;
598        }
599
600        /**
601         * The raw (un-decoded) path component of this request exactly as the client specified.
602         * <p>
603         * For example, {@code "/a%20b"} (never decoded).
604         * <p>
605         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
606         * generated by encoding the decoded path, which may not exactly match the original wire format.</em>
607         *
608         * @return the raw path for this request
609         */
610        @NonNull
611        public String getRawPath() {
612                return this.rawPath;
613        }
614
615        /**
616         * The raw (un-decoded) query component of this request exactly as the client specified.
617         * <p>
618         * For example, {@code "a=b&c=d+e"} (never decoded).
619         * <p>
620         * This is useful for special cases like HMAC signature verification, which relies on the exact client format.
621         * <p>
622         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
623         * generated by encoding the decoded query parameters, which may not exactly match the original wire format.</em>
624         *
625         * @return the raw query for this request, or {@link Optional#empty()} if none was specified
626         */
627        @NonNull
628        public Optional<String> getRawQuery() {
629                return Optional.ofNullable(this.rawQuery);
630        }
631
632        /**
633         * The raw (un-decoded) path and query components of this request exactly as the client specified.
634         * <p>
635         * For example, {@code "/my%20path?a=b&c=d%20e"} (never decoded).
636         * <p>
637         * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is
638         * generated by encoding the decoded path and query parameters, which may not exactly match the original wire format.</em>
639         *
640         * @return the raw path and query for this request
641         */
642        @NonNull
643        public String getRawPathAndQuery() {
644                if (this.rawQuery == null)
645                        return this.rawPath;
646
647                return this.rawPath + "?" + this.rawQuery;
648        }
649
650        /**
651         * The remote network address for the client connection, if available.
652         *
653         * @return the remote address for this request, or {@link Optional#empty()} if unavailable
654         */
655        @NonNull
656        public Optional<InetSocketAddress> getRemoteAddress() {
657                return Optional.ofNullable(this.remoteAddress);
658        }
659
660        /**
661         * The headers provided by the client for this request.
662         * <p>
663         * The keys are the header names and the values are header values
664         * (it is possible for a client to send multiple headers with the same name).
665         * <p>
666         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
667         * <p>
668         * Use {@link #getHeader(String)} for a convenience method to access header values when only one is expected.
669         *
670         * @return the request's headers
671         */
672        @NonNull
673        public Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() {
674                return this.headers.asMap();
675        }
676
677        /**
678         * The W3C trace context for this request, if one was supplied by the client or explicitly specified.
679         *
680         * @return the trace context, or {@link Optional#empty()} if unavailable
681         */
682        @NonNull
683        public Optional<TraceContext> getTraceContext() {
684                return Optional.ofNullable(this.traceContext);
685        }
686
687        /**
688         * The {@code Content-Type} header value, as specified by the client.
689         *
690         * @return the request's {@code Content-Type} header value, or {@link Optional#empty()} if not specified
691         */
692        @NonNull
693        public Optional<String> getContentType() {
694                return Optional.ofNullable(this.contentType);
695        }
696
697        /**
698         * The request's character encoding, as specified by the client in the {@code Content-Type} header value.
699         *
700         * @return the request's character encoding, or {@link Optional#empty()} if not specified
701         */
702        @NonNull
703        public Optional<Charset> getCharset() {
704                return Optional.ofNullable(this.charset);
705        }
706
707        /**
708         * Is this a request with {@code Content-Type} of {@code multipart/form-data}?
709         *
710         * @return {@code true} if this is a {@code multipart/form-data} request, {@code false} otherwise
711         */
712        @NonNull
713        public Boolean isMultipart() {
714                return this.multipart;
715        }
716
717        /**
718         * The decoded HTML {@code multipart/form-data} fields provided by the client for this request.
719         * <p>
720         * The keys are the multipart field names and the values are multipart field values
721         * (it is possible for a client to send multiple multipart fields with the same name).
722         * <p>
723         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
724         * <p>
725         * Use {@link #getMultipartField(String)} for a convenience method to access a multipart parameter field value when only one is expected.
726         * <p>
727         * When using Soklet's default {@link HttpServer}, multipart fields are parsed using the {@link MultipartParser} as configured by {@link HttpServer.Builder#multipartParser(MultipartParser)}.
728         *
729         * @return the request's multipart fields, or the empty map if none are present
730         */
731        @NonNull
732        public Map<@NonNull String, @NonNull Set<@NonNull MultipartField>> getMultipartFields() {
733                if (!isMultipart())
734                        return Map.of();
735
736                Map<String, Set<MultipartField>> result = this.multipartFields;
737
738                if (result == null) {
739                        getLock().lock();
740                        try {
741                                result = this.multipartFields;
742
743                                if (result == null) {
744                                        result = Collections.unmodifiableMap(getMultipartParser().extractMultipartFields(this));
745                                        this.multipartFields = result;
746                                }
747                        } finally {
748                                getLock().unlock();
749                        }
750                }
751
752                return result;
753        }
754
755        /**
756         * The size of the request payload body before any transparent {@code Content-Encoding} decompression.
757         * <p>
758         * This excludes the request line, headers, and transfer framing. For example, when the standard HTTP
759         * server transparently decompresses a gzip request body, this is the compressed payload size while
760         * {@link #getBody()} exposes the decompressed bytes. For complete requests that are not transparently
761         * decompressed, including manually constructed requests, this is the size of {@link #getBody()}.
762         * <p>
763         * If the server rejects a request before retaining its complete encoded payload, this value is {@code 0}
764         * and {@link #isContentTooLarge()} is {@code true}.
765         * <p>
766         * Copies created through {@link #copy()} preserve this value even if the copy replaces the handler-visible
767         * body, because the value describes the original inbound payload.
768         *
769         * @return the encoded request payload body size in bytes
770         */
771        @NonNull
772        public Integer getEncodedBodySizeInBytes() {
773                return this.encodedBodySizeInBytes;
774        }
775
776        /**
777         * The request body bytes exposed to handlers - <strong>callers should not modify this array; it is not defensively copied for performance reasons</strong>.
778         * <p>
779         * When the standard HTTP server transparently decompresses a request body, these are the decompressed
780         * bytes. The encoded payload size remains available through {@link #getEncodedBodySizeInBytes()}.
781         * <p>
782         * For convenience, {@link #getBodyAsString()} is available if you expect your request body to be of type {@link String}.
783         *
784         * @return the request body bytes, or {@link Optional#empty()} if none was supplied
785         */
786        @NonNull
787        public Optional<byte[]> getBody() {
788                return Optional.ofNullable(this.body);
789
790                // Note: it would be nice to defensively copy, but it's inefficient
791                // return Optional.ofNullable(this.body == null ? null : Arrays.copyOf(this.body, this.body.length));
792        }
793
794        /**
795         * Was this request too large for the server to handle?
796         * <p>
797         * <em>If so, this request might have incomplete sets of headers/cookies. It will always have a zero-length body.</em>
798         * <p>
799         * 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.
800         * <p>
801         * When using Soklet's default {@link HttpServer}, maximum request size is configured by {@link HttpServer.Builder#maximumRequestSizeInBytes(Integer)}.
802         * That limit applies to the whole received HTTP request, including request line, headers, transfer framing, and body bytes.
803         * Applications that think in terms of payload size should leave room for request metadata and protocol framing.
804         *
805         * @return {@code true} if this request is larger than the server is able to handle, {@code false} otherwise
806         */
807        @NonNull
808        public Boolean isContentTooLarge() {
809                return this.contentTooLarge;
810        }
811
812        /**
813         * Convenience method that provides the {@link #getBody()} bytes as a {@link String} encoded using the client-specified character set per {@link #getCharset()}.
814         * <p>
815         * If no character set is specified, {@link StandardCharsets#UTF_8} is used to perform the encoding.
816         * <p>
817         * 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.
818         * <p>
819         * This method is threadsafe.
820         *
821         * @return a {@link String} representation of this request's body, or {@link Optional#empty()} if no request body was specified by the client
822         */
823        @NonNull
824        public Optional<String> getBodyAsString() {
825                // Lazily instantiate a string instance using double-checked locking
826                String result = this.bodyAsString;
827
828                if (this.body != null && result == null) {
829                        getLock().lock();
830
831                        try {
832                                result = this.bodyAsString;
833
834                                if (result == null) {
835                                        result = new String(this.body, getCharset().orElse(DEFAULT_CHARSET));
836                                        this.bodyAsString = result;
837                                }
838                        } finally {
839                                getLock().unlock();
840                        }
841                }
842
843                return Optional.ofNullable(result);
844        }
845
846        /**
847         * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Non-preflight CORS</a> request data.
848         * <p>
849         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
850         *
851         * @return non-preflight CORS request data, or {@link Optional#empty()} if none was specified
852         */
853        @NonNull
854        public Optional<Cors> getCors() {
855                return Optional.ofNullable(this.cors);
856        }
857
858        /**
859         * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related request data.
860         * <p>
861         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
862         *
863         * @return preflight CORS request data, or {@link Optional#empty()} if none was specified
864         */
865        @NonNull
866        public Optional<CorsPreflight> getCorsPreflight() {
867                return Optional.ofNullable(this.corsPreflight);
868        }
869
870        /**
871         * 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>.
872         * <p>
873         * 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.
874         * <p>
875         * This method is threadsafe.
876         * <p>
877         * See {@link #getLanguageRanges()} for a variant that pulls {@link LanguageRange} values.
878         *
879         * @return locale information for this request, or the empty list if none was specified
880         */
881        @NonNull
882        public List<@NonNull Locale> getLocales() {
883                // Lazily instantiate our parsed locales using double-checked locking
884                List<Locale> result = this.locales;
885
886                if (result == null) {
887                        getLock().lock();
888
889                        try {
890                                result = this.locales;
891
892                                if (result == null) {
893                                        Set<String> acceptLanguageHeaderValues = getHeaderValues("Accept-Language").orElse(null);
894
895                                        if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) {
896                                                // Support data spread across multiple header lines, which spec allows
897                                                String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream()
898                                                                .filter(value -> trimAggressivelyToEmpty(value).length() > 0)
899                                                                .collect(Collectors.joining(","));
900
901                                                try {
902                                                        result = unmodifiableList(Utilities.extractLocalesFromAcceptLanguageHeaderValue(acceptLanguageHeaderValue));
903                                                } catch (Exception ignored) {
904                                                        // Malformed Accept-Language header; ignore it
905                                                        result = List.of();
906                                                }
907                                        } else {
908                                                result = List.of();
909                                        }
910
911                                        this.locales = result;
912                                }
913                        } finally {
914                                getLock().unlock();
915                        }
916                }
917
918                return result;
919        }
920
921        /**
922         * {@link LanguageRange} information for this request as specified by {@code Accept-Language} header value[s].
923         * <p>
924         * 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.
925         * <p>
926         * This method is threadsafe.
927         * <p>
928         * See {@link #getLocales()} for a variant that pulls {@link Locale} values.
929         *
930         * @return language range information for this request, or the empty list if none was specified
931         */
932        @NonNull
933        public List<@NonNull LanguageRange> getLanguageRanges() {
934                // Lazily instantiate our parsed language ranges using double-checked locking
935                List<LanguageRange> result = this.languageRanges;
936
937                if (result == null) {
938                        getLock().lock();
939                        try {
940                                result = this.languageRanges;
941
942                                if (result == null) {
943                                        Set<String> acceptLanguageHeaderValues = getHeaderValues("Accept-Language").orElse(null);
944
945                                        if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) {
946                                                // Support data spread across multiple header lines, which spec allows
947                                                String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream()
948                                                                .filter(value -> trimAggressivelyToEmpty(value).length() > 0)
949                                                                .collect(Collectors.joining(","));
950
951                                                try {
952                                                        result = Collections.unmodifiableList(LanguageRange.parse(acceptLanguageHeaderValue));
953                                                } catch (Exception ignored) {
954                                                        // Malformed Accept-Language header; ignore it
955                                                        result = List.of();
956                                                }
957                                        } else {
958                                                result = List.of();
959                                        }
960
961                                        this.languageRanges = result;
962                                }
963                        } finally {
964                                getLock().unlock();
965                        }
966                }
967
968                return result;
969        }
970
971        /**
972         * {@link MediaRange} information for this request as specified by {@code Accept} header value[s], ordered by
973         * {@code q} weight and specificity (including media-type parameters) as defined by
974         * <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1">RFC 9110, Section 12.5.1</a>.
975         * <p>
976         * This method will lazily parse {@code Accept} header values into an ordered {@link List} of {@link MediaRange}
977         * when first invoked.  This representation is then cached and re-used for subsequent invocations.
978         * Multiple {@code Accept} header lines are joined in their original request order, and malformed media
979         * ranges are skipped.
980         * <p>
981         * This method is threadsafe.
982         * <p>
983         * Note: an empty list means the client expressed no {@code Accept} preference — per
984         * <a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1">RFC 9110</a> that implies any
985         * media type is acceptable, not that none are.
986         *
987         * @return media range information for this request in descending preference order, or the empty list if none was specified
988         */
989        @NonNull
990        public List<@NonNull MediaRange> getMediaRanges() {
991                // Lazily instantiate our parsed media ranges using double-checked locking
992                List<MediaRange> result = this.mediaRanges;
993
994                if (result == null) {
995                        getLock().lock();
996                        try {
997                                result = this.mediaRanges;
998
999                                if (result == null) {
1000                                        Set<String> acceptHeaderValues = getHeaderValues("Accept").orElse(null);
1001
1002                                        if (acceptHeaderValues != null && !acceptHeaderValues.isEmpty()) {
1003                                                // Support data spread across multiple header lines, which spec allows
1004                                                String acceptHeaderValue = acceptHeaderValues.stream()
1005                                                                .filter(value -> trimAggressivelyToEmpty(value).length() > 0)
1006                                                                .collect(Collectors.joining(","));
1007
1008                                                try {
1009                                                        result = Utilities.extractMediaRangesFromAcceptHeaderValue(acceptHeaderValue);
1010                                                } catch (Exception ignored) {
1011                                                        // Malformed Accept header; ignore it
1012                                                        result = List.of();
1013                                                }
1014                                        } else {
1015                                                result = List.of();
1016                                        }
1017
1018                                        this.mediaRanges = result;
1019                                }
1020                        } finally {
1021                                getLock().unlock();
1022                        }
1023                }
1024
1025                return result;
1026        }
1027
1028        /**
1029         * Convenience method to access a decoded query parameter's value when at most one is expected for the given {@code name}.
1030         * <p>
1031         * If a query parameter {@code name} can support multiple values, {@link #getQueryParameters()} should be used instead of this method.
1032         * <p>
1033         * If this method is invoked for a query parameter {@code name} with multiple values, Soklet will throw {@link IllegalQueryParameterException}.
1034         * <p>
1035         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
1036         *
1037         * @param name the name of the query parameter
1038         * @return the value for the query parameter, or {@link Optional#empty()} if none is present
1039         * @throws IllegalQueryParameterException if the query parameter with the given {@code name} has multiple values
1040         */
1041        @NonNull
1042        public Optional<String> getQueryParameter(@NonNull String name) {
1043                requireNonNull(name);
1044
1045                try {
1046                        Map<String, Set<String>> queryParameters = this.queryParameters;
1047
1048                        if (queryParameters == null && this.lazyQueryParameters)
1049                                return singleValueForName(name, Utilities.extractQueryParameterValuesFromQuery(requireNonNull(this.rawQueryForLazyParameters), name, QueryFormat.RFC_3986_STRICT, DEFAULT_CHARSET).orElse(null));
1050
1051                        return singleValueForName(name, getQueryParameters());
1052                } catch (MultipleValuesException e) {
1053                        @SuppressWarnings("unchecked")
1054                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
1055                        throw new IllegalQueryParameterException(format("Multiple values specified for query parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
1056                }
1057        }
1058
1059        /**
1060         * Convenience method to access a decoded form parameter's value when at most one is expected for the given {@code name}.
1061         * <p>
1062         * If a form parameter {@code name} can support multiple values, {@link #getFormParameters()} should be used instead of this method.
1063         * <p>
1064         * If this method is invoked for a form parameter {@code name} with multiple values, Soklet will throw {@link IllegalFormParameterException}.
1065         * <p>
1066         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
1067         *
1068         * @param name the name of the form parameter
1069         * @return the value for the form parameter, or {@link Optional#empty()} if none is present
1070         * @throws IllegalFormParameterException if the form parameter with the given {@code name} has multiple values
1071         */
1072        @NonNull
1073        public Optional<String> getFormParameter(@NonNull String name) {
1074                requireNonNull(name);
1075
1076                try {
1077                        return singleValueForName(name, getFormParameters());
1078                } catch (MultipleValuesException e) {
1079                        @SuppressWarnings("unchecked")
1080                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
1081                        throw new IllegalFormParameterException(format("Multiple values specified for form parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
1082                }
1083        }
1084
1085        /**
1086         * Convenience method to access a header's value when at most one is expected for the given {@code name}.
1087         * <p>
1088         * If a header {@code name} can support multiple values, {@link #getHeaders()} should be used instead of this method.
1089         * <p>
1090         * If this method is invoked for a header {@code name} with multiple values, Soklet will throw {@link IllegalRequestHeaderException}.
1091         * <p>
1092         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
1093         *
1094         * @param name the name of the header
1095         * @return the value for the header, or {@link Optional#empty()} if none is present
1096         * @throws IllegalRequestHeaderException if the header with the given {@code name} has multiple values
1097         */
1098        @NonNull
1099        public Optional<String> getHeader(@NonNull String name) {
1100                requireNonNull(name);
1101
1102                try {
1103                        return singleValueForName(name, getHeaderValues(name).orElse(null));
1104                } catch (MultipleValuesException e) {
1105                        @SuppressWarnings("unchecked")
1106                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
1107                        throw new IllegalRequestHeaderException(format("Multiple values specified for request header '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
1108                }
1109        }
1110
1111        @NonNull
1112        Optional<Set<@NonNull String>> getHeaderValues(@NonNull String name) {
1113                requireNonNull(name);
1114                return this.headers.get(name);
1115        }
1116
1117        /**
1118         * Convenience method to access a cookie's value when at most one is expected for the given {@code name}.
1119         * <p>
1120         * If a cookie {@code name} can support multiple values, {@link #getCookies()} should be used instead of this method.
1121         * <p>
1122         * If this method is invoked for a cookie {@code name} with multiple values, Soklet will throw {@link IllegalRequestCookieException}.
1123         * <p>
1124         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
1125         *
1126         * @param name the name of the cookie
1127         * @return the value for the cookie, or {@link Optional#empty()} if none is present
1128         * @throws IllegalRequestCookieException if the cookie with the given {@code name} has multiple values
1129         */
1130        @NonNull
1131        public Optional<String> getCookie(@NonNull String name) {
1132                requireNonNull(name);
1133
1134                try {
1135                        return singleValueForName(name, getCookies());
1136                } catch (MultipleValuesException e) {
1137                        @SuppressWarnings("unchecked")
1138                        String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", ")));
1139                        throw new IllegalRequestCookieException(format("Multiple values specified for request cookie '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString);
1140                }
1141        }
1142
1143        /**
1144         * Convenience method to access a decoded multipart field when at most one is expected for the given {@code name}.
1145         * <p>
1146         * If a {@code name} can support multiple multipart fields, {@link #getMultipartFields()} should be used instead of this method.
1147         * <p>
1148         * If this method is invoked for a {@code name} with multiple multipart field values, Soklet will throw {@link IllegalMultipartFieldException}.
1149         * <p>
1150         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
1151         *
1152         * @param name the name of the multipart field
1153         * @return the multipart field value, or {@link Optional#empty()} if none is present
1154         * @throws IllegalMultipartFieldException if the multipart field with the given {@code name} has multiple values
1155         */
1156        @NonNull
1157        public Optional<MultipartField> getMultipartField(@NonNull String name) {
1158                requireNonNull(name);
1159
1160                try {
1161                        return singleValueForName(name, getMultipartFields());
1162                } catch (MultipleValuesException e) {
1163                        @SuppressWarnings("unchecked")
1164                        MultipartField firstMultipartField = requireNonNull(getMultipartFields().get(name)).stream().findFirst().get();
1165                        String valuesAsString = format("[%s]", e.getValues().stream()
1166                                        .map(multipartField -> multipartField.toString())
1167                                        .collect(Collectors.joining(", ")));
1168
1169                        throw new IllegalMultipartFieldException(format("Multiple values specified for multipart field '%s' (but expected single value): %s", name, valuesAsString), firstMultipartField);
1170                }
1171        }
1172
1173        @NonNull
1174        private MultipartParser getMultipartParser() {
1175                return this.multipartParser;
1176        }
1177
1178        @NonNull
1179        private IdGenerator<?> getIdGenerator() {
1180                return this.idGenerator;
1181        }
1182
1183        @NonNull
1184        private ReentrantLock getLock() {
1185                return this.lock;
1186        }
1187
1188        @NonNull
1189        private <T> Optional<T> singleValueForName(@NonNull String name,
1190                                                                                                                                                                                 @Nullable Map<String, Set<T>> valuesByName) throws MultipleValuesException {
1191                if (valuesByName == null)
1192                        return Optional.empty();
1193
1194                Set<T> values = valuesByName.get(name);
1195
1196                if (values == null)
1197                        return Optional.empty();
1198
1199                if (values.size() > 1)
1200                        throw new MultipleValuesException(name, values);
1201
1202                return values.stream().findFirst();
1203        }
1204
1205        @NonNull
1206        private <T> Optional<T> singleValueForName(@NonNull String name,
1207                                                                                                                                                                                 @Nullable Set<T> values) throws MultipleValuesException {
1208                requireNonNull(name);
1209
1210                if (values == null)
1211                        return Optional.empty();
1212
1213                if (values.size() > 1)
1214                        throw new MultipleValuesException(name, values);
1215
1216                return values.stream().findFirst();
1217        }
1218
1219        @NonNull
1220        private static Optional<Cors> extractCors(@NonNull HttpMethod httpMethod,
1221                                                                                                                                                                                @NonNull RequestHeaders headers) {
1222                requireNonNull(httpMethod);
1223                requireNonNull(headers);
1224
1225                return firstHeaderValue(headers, "Origin").map(origin -> Cors.fromOrigin(httpMethod, origin));
1226        }
1227
1228        @NonNull
1229        private static Optional<CorsPreflight> extractCorsPreflight(@NonNull RequestHeaders headers) {
1230                requireNonNull(headers);
1231
1232                String origin = firstHeaderValue(headers, "Origin").orElse(null);
1233
1234                if (origin == null)
1235                        return Optional.empty();
1236
1237                Set<String> accessControlRequestMethodHeaderValues = headers.get("Access-Control-Request-Method").orElse(Set.of());
1238                HttpMethod accessControlRequestMethod = null;
1239
1240                for (String headerValue : accessControlRequestMethodHeaderValues) {
1241                        headerValue = trimAggressivelyToEmpty(headerValue);
1242
1243                        try {
1244                                accessControlRequestMethod = HttpMethod.valueOf(headerValue);
1245                                break;
1246                        } catch (Exception ignored) {
1247                                // Ignore invalid method values.
1248                        }
1249                }
1250
1251                if (accessControlRequestMethod == null)
1252                        return Optional.empty();
1253
1254                Set<String> accessControlRequestHeaders = headers.get("Access-Control-Request-Headers").orElse(Set.of())
1255                                .stream()
1256                                .flatMap(value -> Arrays.stream(value.split(",")))
1257                                .map(Utilities::trimAggressivelyToEmpty)
1258                                .filter(value -> !value.isEmpty())
1259                                .collect(Collectors.toCollection(LinkedHashSet::new));
1260
1261                return Optional.of(CorsPreflight.with(origin, accessControlRequestMethod, accessControlRequestHeaders));
1262        }
1263
1264        @NonNull
1265        private static Optional<String> firstHeaderValue(@NonNull RequestHeaders headers,
1266                                                                                                                                                                                                        @NonNull String name) {
1267                requireNonNull(headers);
1268                requireNonNull(name);
1269
1270                Set<String> values = headers.get(name).orElse(null);
1271
1272                if (values == null || values.isEmpty())
1273                        return Optional.empty();
1274
1275                return Optional.ofNullable(trimAggressivelyToNull(values.stream().findFirst().orElse(null)));
1276        }
1277
1278        @NonNull
1279        private static Optional<TraceContext> extractTraceContext(@NonNull RequestHeaders headers) {
1280                requireNonNull(headers);
1281
1282                // Physical request headers preserve duplicate traceparent values. Map-backed request construction
1283                // uses Set values, so identical duplicates are already collapsed by the time parsing runs.
1284                return TraceContext.fromHeaderValues(headers.values("traceparent"), headers.values("tracestate"));
1285        }
1286
1287        private interface RequestHeaders {
1288                @NonNull
1289                Optional<Set<@NonNull String>> get(@NonNull String name);
1290
1291                @NonNull
1292                List<@NonNull String> values(@NonNull String name);
1293
1294                @NonNull
1295                Map<@NonNull String, @NonNull Set<@NonNull String>> asMap();
1296        }
1297
1298        @ThreadSafe
1299        private static final class MapRequestHeaders implements RequestHeaders {
1300                @NonNull
1301                private final Map<@NonNull String, @NonNull Set<@NonNull String>> headers;
1302
1303                private MapRequestHeaders(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) {
1304                        if (headers == null || headers.isEmpty()) {
1305                                this.headers = Map.of();
1306                        } else {
1307                                this.headers = Collections.unmodifiableMap(new LinkedCaseInsensitiveMap<>(headers));
1308                        }
1309                }
1310
1311                @Override
1312                @NonNull
1313                public Optional<Set<@NonNull String>> get(@NonNull String name) {
1314                        requireNonNull(name);
1315                        return Optional.ofNullable(this.headers.get(name));
1316                }
1317
1318                @Override
1319                @NonNull
1320                public List<@NonNull String> values(@NonNull String name) {
1321                        requireNonNull(name);
1322
1323                        Set<String> values = this.headers.get(name);
1324
1325                        if (values == null || values.isEmpty())
1326                                return List.of();
1327
1328                        return List.copyOf(values);
1329                }
1330
1331                @Override
1332                @NonNull
1333                public Map<@NonNull String, @NonNull Set<@NonNull String>> asMap() {
1334                        return this.headers;
1335                }
1336        }
1337
1338        @ThreadSafe
1339        private static final class MicrohttpRequestHeaders implements RequestHeaders {
1340                @NonNull
1341                private final List<@NonNull Header> headers;
1342                @Nullable
1343                private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> materializedHeaders;
1344
1345                private MicrohttpRequestHeaders(@Nullable List<@NonNull Header> headers) {
1346                        this.headers = headers == null ? List.of() : headers;
1347                }
1348
1349                @Override
1350                @NonNull
1351                public Optional<Set<@NonNull String>> get(@NonNull String name) {
1352                        requireNonNull(name);
1353
1354                        Set<String> matchingValues = null;
1355
1356                        for (Header header : this.headers) {
1357                                if (header == null || !name.equalsIgnoreCase(trimAggressivelyToEmpty(header.name())))
1358                                        continue;
1359
1360                                if (matchingValues == null)
1361                                        matchingValues = new LinkedHashSet<>();
1362
1363                                Utilities.addParsedHeaderValues(matchingValues, header.name(), header.value());
1364                        }
1365
1366                        if (matchingValues == null || matchingValues.isEmpty())
1367                                return Optional.empty();
1368
1369                        return Optional.of(Collections.unmodifiableSet(matchingValues));
1370                }
1371
1372                @Override
1373                @NonNull
1374                public List<@NonNull String> values(@NonNull String name) {
1375                        requireNonNull(name);
1376
1377                        List<String> matchingValues = null;
1378
1379                        for (Header header : this.headers) {
1380                                if (header == null || !name.equalsIgnoreCase(trimAggressivelyToEmpty(header.name())))
1381                                        continue;
1382
1383                                if (matchingValues == null)
1384                                        matchingValues = new ArrayList<>();
1385
1386                                matchingValues.add(trimAggressivelyToEmpty(header.value()));
1387                        }
1388
1389                        return matchingValues == null || matchingValues.isEmpty()
1390                                        ? List.of()
1391                                        : Collections.unmodifiableList(matchingValues);
1392                }
1393
1394                @Override
1395                @NonNull
1396                public Map<@NonNull String, @NonNull Set<@NonNull String>> asMap() {
1397                        Map<String, Set<String>> result = this.materializedHeaders;
1398
1399                        if (result == null) {
1400                                Map<String, Set<String>> headers = new LinkedCaseInsensitiveMap<>();
1401
1402                                for (Header header : this.headers) {
1403                                        if (header == null)
1404                                                continue;
1405
1406                                        Utilities.addParsedHeader(headers, header.name(), header.value());
1407                                }
1408
1409                                Utilities.freezeStringValueSets(headers);
1410                                result = Collections.unmodifiableMap(headers);
1411                                this.materializedHeaders = result;
1412                        }
1413
1414                        return result;
1415                }
1416        }
1417
1418        @NotThreadSafe
1419        private static class MultipleValuesException extends Exception {
1420                @NonNull
1421                private final Set<?> values;
1422
1423                private MultipleValuesException(@NonNull String name,
1424                                                                                                                                                @NonNull Set<?> values) {
1425                        super(format("Expected single value but found %d values for '%s': %s", values.size(), name, values));
1426
1427                        requireNonNull(name);
1428                        requireNonNull(values);
1429
1430                        this.values = Collections.unmodifiableSet(new LinkedHashSet<>(values));
1431                }
1432
1433                @NonNull
1434                public Set<?> getValues() {
1435                        return this.values;
1436                }
1437        }
1438
1439        /**
1440         * Builder used to construct instances of {@link Request} via {@link Request#withRawUrl(HttpMethod, String)}.
1441         * <p>
1442         * This class is intended for use by a single thread.
1443         *
1444         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1445         */
1446        @NotThreadSafe
1447        public static final class RawBuilder {
1448                @NonNull
1449                private HttpMethod httpMethod;
1450                @NonNull
1451                private String rawUrl;
1452                @Nullable
1453                private Object id;
1454                @Nullable
1455                private IdGenerator idGenerator;
1456                @Nullable
1457                private MultipartParser multipartParser;
1458                @Nullable
1459                private Map<@NonNull String, @NonNull Set<@NonNull String>> headers;
1460                @Nullable
1461                private List<@NonNull Header> microhttpHeaders;
1462                @Nullable
1463                private TraceContext traceContext;
1464                @NonNull
1465                private Boolean traceContextSpecified = false;
1466                @Nullable
1467                private InetSocketAddress remoteAddress;
1468                private byte @Nullable [] body;
1469                @Nullable
1470                private Integer encodedBodySizeInBytes;
1471                @Nullable
1472                private Boolean contentTooLarge;
1473
1474                RawBuilder(@NonNull HttpMethod httpMethod,
1475                                                                                                 @NonNull String rawUrl) {
1476                        requireNonNull(httpMethod);
1477                        requireNonNull(rawUrl);
1478
1479                        this.httpMethod = httpMethod;
1480                        this.rawUrl = rawUrl;
1481                }
1482
1483                @NonNull
1484                public RawBuilder httpMethod(@NonNull HttpMethod httpMethod) {
1485                        requireNonNull(httpMethod);
1486                        this.httpMethod = httpMethod;
1487                        return this;
1488                }
1489
1490                @NonNull
1491                public RawBuilder rawUrl(@NonNull String rawUrl) {
1492                        requireNonNull(rawUrl);
1493                        this.rawUrl = rawUrl;
1494                        return this;
1495                }
1496
1497                @NonNull
1498                public RawBuilder id(@Nullable Object id) {
1499                        this.id = id;
1500                        return this;
1501                }
1502
1503                @NonNull
1504                public RawBuilder idGenerator(@Nullable IdGenerator idGenerator) {
1505                        this.idGenerator = idGenerator;
1506                        return this;
1507                }
1508
1509                @NonNull
1510                public RawBuilder multipartParser(@Nullable MultipartParser multipartParser) {
1511                        this.multipartParser = multipartParser;
1512                        return this;
1513                }
1514
1515                @NonNull
1516                public RawBuilder headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) {
1517                        this.headers = headers;
1518                        this.microhttpHeaders = null;
1519                        return this;
1520                }
1521
1522                @NonNull
1523                public RawBuilder traceContext(@Nullable TraceContext traceContext) {
1524                        this.traceContext = traceContext;
1525                        this.traceContextSpecified = true;
1526                        return this;
1527                }
1528
1529                @NonNull
1530                RawBuilder microhttpHeaders(@Nullable List<@NonNull Header> headers) {
1531                        this.headers = null;
1532                        this.microhttpHeaders = headers;
1533                        return this;
1534                }
1535
1536                @NonNull
1537                public RawBuilder remoteAddress(@Nullable InetSocketAddress remoteAddress) {
1538                        this.remoteAddress = remoteAddress;
1539                        return this;
1540                }
1541
1542                @NonNull
1543                public RawBuilder body(byte @Nullable [] body) {
1544                        this.body = body;
1545                        return this;
1546                }
1547
1548                @NonNull
1549                RawBuilder encodedBodySizeInBytes(@Nullable Integer encodedBodySizeInBytes) {
1550                        this.encodedBodySizeInBytes = encodedBodySizeInBytes;
1551                        return this;
1552                }
1553
1554                @NonNull
1555                public RawBuilder contentTooLarge(@Nullable Boolean contentTooLarge) {
1556                        this.contentTooLarge = contentTooLarge;
1557                        return this;
1558                }
1559
1560                @NonNull
1561                public Request build() {
1562                        return new Request(this, null);
1563                }
1564
1565                @NonNull
1566                private RequestHeaders requestHeaders() {
1567                        if (this.microhttpHeaders != null)
1568                                return new MicrohttpRequestHeaders(this.microhttpHeaders);
1569
1570                        return new MapRequestHeaders(this.headers);
1571                }
1572        }
1573
1574        /**
1575         * Builder used to construct instances of {@link Request} via {@link Request#withPath(HttpMethod, String)}.
1576         * <p>
1577         * This class is intended for use by a single thread.
1578         *
1579         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1580         */
1581        @NotThreadSafe
1582        public static final class PathBuilder {
1583                @NonNull
1584                private HttpMethod httpMethod;
1585                @NonNull
1586                private String path;
1587                @Nullable
1588                private String rawPath;
1589                @Nullable
1590                private String rawQuery;
1591                @Nullable
1592                private Object id;
1593                @Nullable
1594                private IdGenerator idGenerator;
1595                @Nullable
1596                private MultipartParser multipartParser;
1597                @Nullable
1598                private Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters;
1599                @Nullable
1600                private Map<@NonNull String, @NonNull Set<@NonNull String>> headers;
1601                @Nullable
1602                private TraceContext traceContext;
1603                @NonNull
1604                private Boolean traceContextSpecified = false;
1605                @Nullable
1606                private InetSocketAddress remoteAddress;
1607                private byte @Nullable [] body;
1608                @Nullable
1609                private Integer encodedBodySizeInBytes;
1610                @Nullable
1611                private Boolean contentTooLarge;
1612
1613                PathBuilder(@NonNull HttpMethod httpMethod,
1614                                                                                                        @NonNull String path) {
1615                        requireNonNull(httpMethod);
1616                        requireNonNull(path);
1617
1618                        this.httpMethod = httpMethod;
1619                        this.path = path;
1620                }
1621
1622                @NonNull
1623                public PathBuilder httpMethod(@NonNull HttpMethod httpMethod) {
1624                        requireNonNull(httpMethod);
1625                        this.httpMethod = httpMethod;
1626                        return this;
1627                }
1628
1629                @NonNull
1630                public PathBuilder path(@NonNull String path) {
1631                        requireNonNull(path);
1632                        this.path = path;
1633                        return this;
1634                }
1635
1636                // Package-private setter for raw value (used by Copier)
1637                @NonNull
1638                PathBuilder rawPath(@Nullable String rawPath) {
1639                        this.rawPath = rawPath;
1640                        return this;
1641                }
1642
1643                // Package-private setter for raw value (used by Copier)
1644                @NonNull
1645                PathBuilder rawQuery(@Nullable String rawQuery) {
1646                        this.rawQuery = rawQuery;
1647                        return this;
1648                }
1649
1650                @NonNull
1651                public PathBuilder id(@Nullable Object id) {
1652                        this.id = id;
1653                        return this;
1654                }
1655
1656                @NonNull
1657                public PathBuilder idGenerator(@Nullable IdGenerator idGenerator) {
1658                        this.idGenerator = idGenerator;
1659                        return this;
1660                }
1661
1662                @NonNull
1663                public PathBuilder multipartParser(@Nullable MultipartParser multipartParser) {
1664                        this.multipartParser = multipartParser;
1665                        return this;
1666                }
1667
1668                @NonNull
1669                public PathBuilder queryParameters(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters) {
1670                        this.queryParameters = queryParameters;
1671                        return this;
1672                }
1673
1674                @NonNull
1675                public PathBuilder headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) {
1676                        this.headers = headers;
1677                        return this;
1678                }
1679
1680                @NonNull
1681                public PathBuilder traceContext(@Nullable TraceContext traceContext) {
1682                        this.traceContext = traceContext;
1683                        this.traceContextSpecified = true;
1684                        return this;
1685                }
1686
1687                @NonNull
1688                public PathBuilder remoteAddress(@Nullable InetSocketAddress remoteAddress) {
1689                        this.remoteAddress = remoteAddress;
1690                        return this;
1691                }
1692
1693                @NonNull
1694                public PathBuilder body(byte @Nullable [] body) {
1695                        this.body = body;
1696                        return this;
1697                }
1698
1699                @NonNull
1700                PathBuilder encodedBodySizeInBytes(@Nullable Integer encodedBodySizeInBytes) {
1701                        this.encodedBodySizeInBytes = encodedBodySizeInBytes;
1702                        return this;
1703                }
1704
1705                @NonNull
1706                public PathBuilder contentTooLarge(@Nullable Boolean contentTooLarge) {
1707                        this.contentTooLarge = contentTooLarge;
1708                        return this;
1709                }
1710
1711                @NonNull
1712                public Request build() {
1713                        return new Request(null, this);
1714                }
1715        }
1716
1717        /**
1718         * Builder used to copy instances of {@link Request} via {@link Request#copy()}.
1719         * <p>
1720         * This class is intended for use by a single thread.
1721         *
1722         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1723         */
1724        @NotThreadSafe
1725        public static final class Copier {
1726                @NonNull
1727                private final PathBuilder builder;
1728
1729                // Track original raw values and modification state
1730                @Nullable
1731                private String originalRawPath;
1732                @Nullable
1733                private String originalRawQuery;
1734                @Nullable
1735                private InetSocketAddress originalRemoteAddress;
1736                @Nullable
1737                private TraceContext originalTraceContext;
1738                @NonNull
1739                private Boolean queryParametersModified = false;
1740                @NonNull
1741                private Boolean headersModified = false;
1742                @NonNull
1743                private Boolean traceContextModified = false;
1744
1745                Copier(@NonNull Request request) {
1746                        requireNonNull(request);
1747
1748                        this.originalRawPath = request.getRawPath();
1749                        this.originalRawQuery = request.rawQuery; // Direct field access
1750                        this.originalRemoteAddress = request.getRemoteAddress().orElse(null);
1751                        this.originalTraceContext = request.getTraceContext().orElse(null);
1752
1753                        this.builder = new PathBuilder(request.getHttpMethod(), request.getPath())
1754                                        .id(request.getId())
1755                                        .queryParameters(mutableLinkedCopy(request.getQueryParameters()))
1756                                        .headers(mutableCaseInsensitiveCopy(request.getHeaders()))
1757                                        .body(request.body) // Direct field access to avoid array copy
1758                                        .encodedBodySizeInBytes(request.getEncodedBodySizeInBytes())
1759                                        .multipartParser(request.getMultipartParser())
1760                                        .idGenerator(request.getIdGenerator())
1761                                        .contentTooLarge(request.isContentTooLarge())
1762                                        .remoteAddress(this.originalRemoteAddress)
1763                                        // Preserve original raw values initially
1764                                        .rawPath(this.originalRawPath)
1765                                        .rawQuery(this.originalRawQuery);
1766                }
1767
1768                @NonNull
1769                public Copier httpMethod(@NonNull HttpMethod httpMethod) {
1770                        requireNonNull(httpMethod);
1771                        this.builder.httpMethod(httpMethod);
1772                        return this;
1773                }
1774
1775                @NonNull
1776                public Copier path(@NonNull String path) {
1777                        requireNonNull(path);
1778                        this.builder.path(path);
1779                        // Clear preserved raw path since decoded path changed
1780                        this.builder.rawPath(null);
1781                        return this;
1782                }
1783
1784                @NonNull
1785                public Copier id(@Nullable Object id) {
1786                        this.builder.id(id);
1787                        return this;
1788                }
1789
1790                @NonNull
1791                public Copier queryParameters(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters) {
1792                        this.builder.queryParameters(queryParameters);
1793                        this.queryParametersModified = true;
1794                        // Clear preserved raw query since decoded query parameters changed
1795                        this.builder.rawQuery(null);
1796                        return this;
1797                }
1798
1799                // Convenience method for mutation
1800                @NonNull
1801                public Copier queryParameters(@NonNull Consumer<Map<@NonNull String, @NonNull Set<@NonNull String>>> queryParametersConsumer) {
1802                        requireNonNull(queryParametersConsumer);
1803
1804                        if (this.builder.queryParameters == null)
1805                                this.builder.queryParameters(new LinkedHashMap<>());
1806
1807                        queryParametersConsumer.accept(this.builder.queryParameters);
1808                        this.queryParametersModified = true;
1809                        // Clear preserved raw query since decoded query parameters changed
1810                        this.builder.rawQuery(null);
1811                        return this;
1812                }
1813
1814                @NonNull
1815                public Copier headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) {
1816                        this.builder.headers(headers);
1817                        this.headersModified = true;
1818                        return this;
1819                }
1820
1821                @NonNull
1822                public Copier traceContext(@Nullable TraceContext traceContext) {
1823                        this.builder.traceContext(traceContext);
1824                        this.traceContextModified = true;
1825                        return this;
1826                }
1827
1828                @NonNull
1829                public Copier remoteAddress(@Nullable InetSocketAddress remoteAddress) {
1830                        this.builder.remoteAddress(remoteAddress);
1831                        return this;
1832                }
1833
1834                // Convenience method for mutation
1835                @NonNull
1836                public Copier headers(@NonNull Consumer<Map<@NonNull String, @NonNull Set<@NonNull String>>> headersConsumer) {
1837                        requireNonNull(headersConsumer);
1838
1839                        if (this.builder.headers == null)
1840                                this.builder.headers(new LinkedCaseInsensitiveMap<>());
1841
1842                        headersConsumer.accept(this.builder.headers);
1843                        this.headersModified = true;
1844                        return this;
1845                }
1846
1847                @NonNull
1848                public Copier body(byte @Nullable [] body) {
1849                        this.builder.body(body);
1850                        return this;
1851                }
1852
1853                @NonNull
1854                public Copier contentTooLarge(@Nullable Boolean contentTooLarge) {
1855                        this.builder.contentTooLarge(contentTooLarge);
1856                        return this;
1857                }
1858
1859                @NonNull
1860                public Request finish() {
1861                        if (this.queryParametersModified) {
1862                                Map<String, Set<String>> queryParameters = this.builder.queryParameters;
1863
1864                                if (queryParameters == null || queryParameters.isEmpty()) {
1865                                        this.builder.rawQuery(null);
1866                                } else {
1867                                        this.builder.rawQuery(Utilities.encodeQueryParameters(queryParameters, QueryFormat.RFC_3986_STRICT));
1868                                }
1869                        }
1870
1871                        if (!this.headersModified && !this.traceContextModified)
1872                                this.builder.traceContext(this.originalTraceContext);
1873
1874                        return this.builder.build();
1875                }
1876
1877                @NonNull
1878                private static Map<@NonNull String, @NonNull Set<@NonNull String>> mutableLinkedCopy(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> valuesByName) {
1879                        requireNonNull(valuesByName);
1880
1881                        Map<String, Set<String>> copy = new LinkedHashMap<>();
1882                        for (Map.Entry<String, Set<String>> entry : valuesByName.entrySet())
1883                                copy.put(entry.getKey(), entry.getValue() == null ? new LinkedHashSet<>() : new LinkedHashSet<>(entry.getValue()));
1884
1885                        return copy;
1886                }
1887
1888                @NonNull
1889                private static Map<@NonNull String, @NonNull Set<@NonNull String>> mutableCaseInsensitiveCopy(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> valuesByName) {
1890                        requireNonNull(valuesByName);
1891
1892                        Map<String, Set<String>> copy = new LinkedCaseInsensitiveMap<>();
1893                        for (Map.Entry<String, Set<String>> entry : valuesByName.entrySet())
1894                                copy.put(entry.getKey(), entry.getValue() == null ? new LinkedHashSet<>() : new LinkedHashSet<>(entry.getValue()));
1895
1896                        return copy;
1897                }
1898        }
1899}