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