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