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