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}