001/* 002 * Copyright 2022-2025 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.spring.LinkedCaseInsensitiveMap; 026 027import javax.annotation.Nonnull; 028import javax.annotation.Nullable; 029import javax.annotation.concurrent.NotThreadSafe; 030import javax.annotation.concurrent.ThreadSafe; 031import java.nio.charset.Charset; 032import java.nio.charset.StandardCharsets; 033import java.util.Arrays; 034import java.util.Collections; 035import java.util.LinkedHashMap; 036import java.util.LinkedHashSet; 037import java.util.List; 038import java.util.Locale; 039import java.util.Locale.LanguageRange; 040import java.util.Map; 041import java.util.Objects; 042import java.util.Optional; 043import java.util.Set; 044import java.util.concurrent.locks.ReentrantLock; 045import java.util.function.Consumer; 046import java.util.stream.Collectors; 047 048import static com.soklet.Utilities.trimAggressivelyToEmpty; 049import static java.lang.String.format; 050import static java.util.Collections.unmodifiableList; 051import static java.util.Objects.requireNonNull; 052 053/** 054 * Encapsulates information specified in an HTTP request. 055 * <p> 056 * 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. 057 * <p> 058 * 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. 059 * <p> 060 * Detailed documentation available at <a href="https://www.soklet.com/docs/request-handling">https://www.soklet.com/docs/request-handling</a>. 061 * 062 * @author <a href="https://www.revetkn.com">Mark Allen</a> 063 */ 064@ThreadSafe 065public final class Request { 066 @Nonnull 067 private static final Charset DEFAULT_CHARSET; 068 @Nonnull 069 private static final IdGenerator DEFAULT_ID_GENERATOR; 070 071 static { 072 DEFAULT_CHARSET = StandardCharsets.UTF_8; 073 DEFAULT_ID_GENERATOR = DefaultIdGenerator.withDefaults(); 074 } 075 076 @Nonnull 077 private final Object id; 078 @Nonnull 079 private final HttpMethod httpMethod; 080 @Nonnull 081 private final String rawPath; 082 @Nullable 083 private final String rawQuery; 084 @Nonnull 085 private final String path; 086 @Nonnull 087 private final ResourcePath resourcePath; 088 @Nonnull 089 private final Map<String, Set<String>> queryParameters; 090 @Nullable 091 private final String contentType; 092 @Nullable 093 private final Charset charset; 094 @Nonnull 095 private final Map<String, Set<String>> headers; 096 @Nullable 097 private final Cors cors; 098 @Nullable 099 private final CorsPreflight corsPreflight; 100 @Nullable 101 private final byte[] body; 102 @Nonnull 103 private final Boolean multipart; 104 @Nonnull 105 private final Boolean contentTooLarge; 106 @Nonnull 107 private final MultipartParser multipartParser; 108 @Nonnull 109 private final IdGenerator<?> idGenerator; 110 @Nonnull 111 private final ReentrantLock lock; 112 @Nullable 113 private volatile String bodyAsString = null; 114 @Nullable 115 private volatile List<Locale> locales = null; 116 @Nullable 117 private volatile List<LanguageRange> languageRanges = null; 118 @Nullable 119 private volatile Map<String, Set<String>> cookies = null; 120 @Nullable 121 private volatile Map<String, Set<MultipartField>> multipartFields = null; 122 @Nullable 123 private volatile Map<String, Set<String>> formParameters = null; 124 125 /** 126 * Acquires a builder for {@link Request} instances from the URL provided by clients on a "raw" HTTP/1.1 request line. 127 * <p> 128 * 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://}). 129 * 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. 130 * <p> 131 * Paths will be percent-decoded. 132 * <p> 133 * Query parameters are parsed and decoded using RFC 3986 semantics - see {@link QueryFormat#RFC_3986_STRICT}. 134 * <p> 135 * 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}. 136 * 137 * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.}) 138 * @param rawUrl the raw (un-decoded) URL for this request 139 * @return the builder 140 */ 141 @Nonnull 142 public static RawBuilder withRawUrl(@Nonnull HttpMethod httpMethod, 143 @Nonnull String rawUrl) { 144 requireNonNull(httpMethod); 145 requireNonNull(rawUrl); 146 147 return new RawBuilder(httpMethod, rawUrl); 148 } 149 150 /** 151 * Acquires a builder for {@link Request} instances from already-decoded path and query components - useful for manual construction, e.g. integration tests. 152 * <p> 153 * 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. 154 * <p> 155 * Query parameters must be specified via {@link PathBuilder#queryParameters(Map)} and are assumed to be already-decoded. 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 path the decoded URL path for this request 161 * @return the builder 162 */ 163 @Nonnull 164 public static PathBuilder withPath(@Nonnull HttpMethod httpMethod, 165 @Nonnull String path) { 166 requireNonNull(httpMethod); 167 requireNonNull(path); 168 169 return new PathBuilder(httpMethod, path); 170 } 171 172 /** 173 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 174 * 175 * @return a copier for this instance 176 */ 177 @Nonnull 178 public Copier copy() { 179 return new Copier(this); 180 } 181 182 private Request(@Nullable RawBuilder rawBuilder, 183 @Nullable PathBuilder pathBuilder) { 184 // Should never occur 185 if (rawBuilder == null && pathBuilder == null) 186 throw new IllegalStateException(format("Neither %s nor %s were specified", RawBuilder.class.getSimpleName(), PathBuilder.class.getSimpleName())); 187 188 IdGenerator builderIdGenerator = rawBuilder == null ? pathBuilder.idGenerator : rawBuilder.idGenerator; 189 Object builderId = rawBuilder == null ? pathBuilder.id : rawBuilder.id; 190 HttpMethod builderHttpMethod = rawBuilder == null ? pathBuilder.httpMethod : rawBuilder.httpMethod; 191 byte[] builderBody = rawBuilder == null ? pathBuilder.body : rawBuilder.body; 192 MultipartParser builderMultipartParser = rawBuilder == null ? pathBuilder.multipartParser : rawBuilder.multipartParser; 193 Boolean builderContentTooLarge = rawBuilder == null ? pathBuilder.contentTooLarge : rawBuilder.contentTooLarge; 194 Map<String, Set<String>> builderHeaders = rawBuilder == null ? pathBuilder.headers : rawBuilder.headers; 195 196 if (builderHeaders == null) 197 builderHeaders = Map.of(); 198 199 this.idGenerator = builderIdGenerator == null ? DEFAULT_ID_GENERATOR : builderIdGenerator; 200 this.multipartParser = builderMultipartParser == null ? DefaultMultipartParser.defaultInstance() : builderMultipartParser; 201 202 // Header names are case-insensitive. Enforce that here with a special map 203 Map<String, Set<String>> caseInsensitiveHeaders = new LinkedCaseInsensitiveMap<>(builderHeaders); 204 this.headers = Collections.unmodifiableMap(caseInsensitiveHeaders); 205 this.contentType = Utilities.extractContentTypeFromHeaders(this.headers).orElse(null); 206 this.charset = Utilities.extractCharsetFromHeaders(this.headers).orElse(null); 207 208 String path; 209 210 // If we use PathBuilder, use its path directly. 211 // If we use RawBuilder, parse and decode its path. 212 if (pathBuilder != null) { 213 path = trimAggressivelyToEmpty(pathBuilder.path); 214 215 // Validate path 216 if (!path.startsWith("/") && !path.equals("*")) 217 throw new IllegalRequestException("Path must start with '/' or be '*'"); 218 219 if (path.contains("?")) 220 throw new IllegalRequestException(format("Path should not contain a query string. Use %s.withPath(...).queryParameters(...) to specify query parameters as a %s.", 221 Request.class.getSimpleName(), Map.class.getSimpleName())); 222 223 // Use already-decoded query parameters as provided by the path builder 224 this.queryParameters = pathBuilder.queryParameters == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(pathBuilder.queryParameters)); 225 } else { 226 // RawBuilder scenario 227 String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl); 228 229 // Special handling for OPTIONS * 230 if ("*".equals(rawUrl)) { 231 path = "*"; 232 this.queryParameters = Map.of(); 233 } else { 234 // First, parse and decode the path... 235 path = Utilities.extractPathFromUrl(rawUrl, true); 236 237 // ...then, parse out any query parameters. 238 if (rawUrl.contains("?")) { 239 // We always assume RFC_3986_STRICT for query parameters because Soklet is for modern systems - HTML Form "GET" submissions are rare/legacy. 240 // This means we leave "+" as "+" (not decode to " ") and then apply any percent-decoding rules. 241 // In the future, we might expose a way to let applications prefer QueryFormat.X_WWW_FORM_URLENCODED instead, which treats "+" as a space 242 this.queryParameters = Collections.unmodifiableMap(Utilities.extractQueryParametersFromUrl(rawUrl, QueryFormat.RFC_3986_STRICT, getCharset().orElse(DEFAULT_CHARSET))); 243 } else { 244 this.queryParameters = Map.of(); 245 } 246 } 247 } 248 249 if (path.equals("*") && builderHttpMethod != HttpMethod.OPTIONS) 250 throw new IllegalRequestException(format("Path '*' is only legal for HTTP %s", HttpMethod.OPTIONS.name())); 251 252 if (path.contains("\u0000") || path.contains("%00")) 253 throw new IllegalRequestException(format("Illegal null byte in path '%s'", path)); 254 255 this.path = path; 256 257 String rawPath; 258 String rawQuery; 259 260 if (pathBuilder != null) { 261 // PathBuilder scenario: check if explicit raw values were provided 262 if (pathBuilder.rawPath != null) { 263 // Explicit raw values provided (e.g. from Copier preserving originals) 264 rawPath = pathBuilder.rawPath; 265 rawQuery = pathBuilder.rawQuery; 266 } else { 267 // No explicit raw values; encode from decoded values 268 if (path.equals("*")) { 269 rawPath = "*"; 270 } else { 271 rawPath = Utilities.encodePath(path); 272 } 273 274 if (this.queryParameters.isEmpty()) { 275 rawQuery = null; 276 } else { 277 rawQuery = Utilities.encodeQueryParameters(this.queryParameters, QueryFormat.RFC_3986_STRICT); 278 } 279 } 280 } else { 281 // RawBuilder scenario: extract raw components from rawUrl 282 String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl); 283 284 if ("*".equals(rawUrl)) { 285 rawPath = "*"; 286 rawQuery = null; 287 } else { 288 rawPath = Utilities.extractPathFromUrl(rawUrl, false); 289 rawQuery = Utilities.extractRawQueryFromUrl(rawUrl).orElse(null); 290 } 291 } 292 293 this.rawPath = rawPath; 294 this.rawQuery = rawQuery; 295 296 this.lock = new ReentrantLock(); 297 this.id = builderId == null ? this.idGenerator.generateId() : builderId; 298 this.httpMethod = builderHttpMethod; 299 this.corsPreflight = this.httpMethod == HttpMethod.OPTIONS ? CorsPreflight.fromHeaders(this.headers).orElse(null) : null; 300 this.cors = this.corsPreflight == null ? Cors.fromHeaders(this.httpMethod, this.headers).orElse(null) : null; 301 this.resourcePath = this.path.equals("*") ? ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH : ResourcePath.withPath(this.path); 302 this.multipart = this.contentType != null && this.contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/"); 303 this.contentTooLarge = builderContentTooLarge == null ? false : builderContentTooLarge; 304 305 // It's illegal to specify a body if the request is marked "content too large" 306 this.body = this.contentTooLarge ? null : builderBody; 307 308 // Cookies, form parameters, and multipart data are lazily parsed/instantiated when callers try to access them 309 } 310 311 @Override 312 public String toString() { 313 return format("%s{id=%s, httpMethod=%s, path=%s, cookies=%s, queryParameters=%s, headers=%s, body=%s}", 314 getClass().getSimpleName(), getId(), getHttpMethod(), getPath(), getCookies(), getQueryParameters(), 315 getHeaders(), format("%d bytes", getBody().isPresent() ? getBody().get().length : 0)); 316 } 317 318 @Override 319 public boolean equals(@Nullable Object object) { 320 if (this == object) 321 return true; 322 323 if (!(object instanceof Request request)) 324 return false; 325 326 return Objects.equals(getId(), request.getId()) 327 && Objects.equals(getHttpMethod(), request.getHttpMethod()) 328 && Objects.equals(getPath(), request.getPath()) 329 && Objects.equals(getQueryParameters(), request.getQueryParameters()) 330 && Objects.equals(getHeaders(), request.getHeaders()) 331 && Arrays.equals(this.body, request.body) 332 && Objects.equals(isContentTooLarge(), request.isContentTooLarge()); 333 } 334 335 @Override 336 public int hashCode() { 337 return Objects.hash(getId(), getHttpMethod(), getPath(), getQueryParameters(), getHeaders(), Arrays.hashCode(this.body), isContentTooLarge()); 338 } 339 340 /** 341 * An application-specific identifier for this request. 342 * <p> 343 * The identifier is not necessarily unique (for example, numbers that "wrap around" if they get too large). 344 * 345 * @return the request's identifier 346 */ 347 @Nonnull 348 public Object getId() { 349 return this.id; 350 } 351 352 /** 353 * The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP method</a> for this request. 354 * 355 * @return the request's HTTP method 356 */ 357 @Nonnull 358 public HttpMethod getHttpMethod() { 359 return this.httpMethod; 360 } 361 362 /** 363 * The percent-decoded path component of this request (no query string). 364 * 365 * @return the path for this request 366 */ 367 @Nonnull 368 public String getPath() { 369 return this.path; 370 } 371 372 /** 373 * Convenience method to acquire a {@link ResourcePath} representation of {@link #getPath()}. 374 * 375 * @return the resource path for this request 376 */ 377 @Nonnull 378 public ResourcePath getResourcePath() { 379 return this.resourcePath; 380 } 381 382 /** 383 * The cookies provided by the client for this request. 384 * <p> 385 * The keys are the {@code Cookie} header names and the values are {@code Cookie} header values 386 * (it is possible for a client to send multiple {@code Cookie} headers with the same name). 387 * <p> 388 * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em> 389 * <p> 390 * Use {@link #getCookie(String)} for a convenience method to access cookie values when only one is expected. 391 * 392 * @return the request's cookies 393 */ 394 @Nonnull 395 public Map<String, Set<String>> getCookies() { 396 Map<String, Set<String>> result = this.cookies; 397 398 if (result == null) { 399 getLock().lock(); 400 401 try { 402 result = this.cookies; 403 404 if (result == null) { 405 result = Collections.unmodifiableMap(Utilities.extractCookiesFromHeaders(getHeaders())); 406 this.cookies = result; 407 } 408 } finally { 409 getLock().unlock(); 410 } 411 } 412 413 return result; 414 } 415 416 /** 417 * The decoded query parameters provided by the client for this request. 418 * <p> 419 * The keys are the query parameter names and the values are query parameter values 420 * (it is possible for a client to send multiple query parameters with the same name, e.g. {@code ?test=1&test=2}). 421 * <p> 422 * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em> 423 * <p> 424 * Use {@link #getQueryParameter(String)} for a convenience method to access query parameter values when only one is expected. 425 * 426 * @return the request's query parameters 427 */ 428 @Nonnull 429 public Map<String, Set<String>> getQueryParameters() { 430 return this.queryParameters; 431 } 432 433 /** 434 * The decoded HTML {@code application/x-www-form-urlencoded} form parameters provided by the client for this request. 435 * <p> 436 * The keys are the form parameter names and the values are form parameter values 437 * (it is possible for a client to send multiple form parameters with the same name, e.g. {@code ?test=1&test=2}). 438 * <p> 439 * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em> 440 * <p> 441 * Use {@link #getFormParameter(String)} for a convenience method to access form parameter values when only one is expected. 442 * 443 * @return the request's form parameters 444 */ 445 @Nonnull 446 public Map<String, Set<String>> getFormParameters() { 447 Map<String, Set<String>> result = this.formParameters; 448 449 if (result == null) { 450 getLock().lock(); 451 try { 452 result = this.formParameters; 453 454 if (result == null) { 455 if (this.body != null && this.contentType != null && this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) { 456 String bodyAsString = getBodyAsString().orElse(null); 457 result = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, getCharset().orElse(DEFAULT_CHARSET))); 458 } else { 459 result = Map.of(); 460 } 461 462 this.formParameters = result; 463 } 464 } finally { 465 getLock().unlock(); 466 } 467 } 468 469 return result; 470 } 471 472 /** 473 * The raw (un-decoded) path component of this request exactly as the client specified. 474 * <p> 475 * For example, {@code "/a%20b"} (never decoded). 476 * <p> 477 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 478 * generated by encoding the decoded path, which may not exactly match the original wire format.</em> 479 * 480 * @return the raw path for this request 481 */ 482 @Nonnull 483 public String getRawPath() { 484 return this.rawPath; 485 } 486 487 /** 488 * The raw (un-decoded) query component of this request exactly as the client specified. 489 * <p> 490 * For example, {@code "a=b&c=d+e"} (never decoded). 491 * <p> 492 * This is useful for special cases like HMAC signature verification, which relies on the exact client format. 493 * <p> 494 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 495 * generated by encoding the decoded query parameters, which may not exactly match the original wire format.</em> 496 * 497 * @return the raw query for this request, or {@link Optional#empty()} if none was specified 498 */ 499 @Nonnull 500 public Optional<String> getRawQuery() { 501 return Optional.ofNullable(this.rawQuery); 502 } 503 504 /** 505 * The raw (un-decoded) path and query components of this request exactly as the client specified. 506 * <p> 507 * For example, {@code "/my%20path?a=b&c=d%20e"} (never decoded). 508 * <p> 509 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 510 * generated by encoding the decoded path and query parameters, which may not exactly match the original wire format.</em> 511 * 512 * @return the raw path and query for this request 513 */ 514 @Nonnull 515 public String getRawPathAndQuery() { 516 if (this.rawQuery == null) 517 return this.rawPath; 518 519 return this.rawPath + "?" + this.rawQuery; 520 } 521 522 /** 523 * The headers provided by the client for this request. 524 * <p> 525 * The keys are the header names and the values are header values 526 * (it is possible for a client to send multiple headers with the same name). 527 * <p> 528 * <em>Note that request headers have case-insensitive names per the HTTP spec.</em> 529 * <p> 530 * Use {@link #getHeader(String)} for a convenience method to access header values when only one is expected. 531 * 532 * @return the request's headers 533 */ 534 @Nonnull 535 public Map<String, Set<String>> getHeaders() { 536 return this.headers; 537 } 538 539 /** 540 * The {@code Content-Type} header value, as specified by the client. 541 * 542 * @return the request's {@code Content-Type} header value, or {@link Optional#empty()} if not specified 543 */ 544 @Nonnull 545 public Optional<String> getContentType() { 546 return Optional.ofNullable(this.contentType); 547 } 548 549 /** 550 * The request's character encoding, as specified by the client in the {@code Content-Type} header value. 551 * 552 * @return the request's character encoding, or {@link Optional#empty()} if not specified 553 */ 554 @Nonnull 555 public Optional<Charset> getCharset() { 556 return Optional.ofNullable(this.charset); 557 } 558 559 /** 560 * Is this a request with {@code Content-Type} of {@code multipart/form-data}? 561 * 562 * @return {@code true} if this is a {@code multipart/form-data} request, {@code false} otherwise 563 */ 564 @Nonnull 565 public Boolean isMultipart() { 566 return this.multipart; 567 } 568 569 /** 570 * The decoded HTML {@code multipart/form-data} fields provided by the client for this request. 571 * <p> 572 * The keys are the multipart field names and the values are multipart field values 573 * (it is possible for a client to send multiple multipart fields with the same name). 574 * <p> 575 * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em> 576 * <p> 577 * Use {@link #getMultipartField(String)} for a convenience method to access a multipart parameter field value when only one is expected. 578 * <p> 579 * When using Soklet's default {@link Server}, multipart fields are parsed using the {@link MultipartParser} as configured by {@link Server.Builder#multipartParser(MultipartParser)}. 580 * 581 * @return the request's multipart fields, or the empty map if none are present 582 */ 583 @Nonnull 584 public Map<String, Set<MultipartField>> getMultipartFields() { 585 if (!isMultipart()) 586 return Map.of(); 587 588 Map<String, Set<MultipartField>> result = this.multipartFields; 589 590 if (result == null) { 591 getLock().lock(); 592 try { 593 result = this.multipartFields; 594 595 if (result == null) { 596 result = Collections.unmodifiableMap(getMultipartParser().extractMultipartFields(this)); 597 this.multipartFields = result; 598 } 599 } finally { 600 getLock().unlock(); 601 } 602 } 603 604 return result; 605 } 606 607 /** 608 * The raw bytes of the request body - <strong>callers should not modify this array; it is not defensively copied for performance reasons</strong>. 609 * <p> 610 * For convenience, {@link #getBodyAsString()} is available if you expect your request body to be of type {@link String}. 611 * 612 * @return the request body bytes, or {@link Optional#empty()} if none was supplied 613 */ 614 @Nonnull 615 public Optional<byte[]> getBody() { 616 return Optional.ofNullable(this.body); 617 618 // Note: it would be nice to defensively copy, but it's inefficient 619 // return Optional.ofNullable(this.body == null ? null : Arrays.copyOf(this.body, this.body.length)); 620 } 621 622 /** 623 * Was this request too large for the server to handle? 624 * <p> 625 * <em>If so, this request might have incomplete sets of headers/cookies. It will always have a zero-length body.</em> 626 * <p> 627 * 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. 628 * <p> 629 * When using Soklet's default {@link Server}, maximum request size is configured by {@link Server.Builder#maximumRequestSizeInBytes(Integer)}. 630 * 631 * @return {@code true} if this request is larger than the server is able to handle, {@code false} otherwise 632 */ 633 @Nonnull 634 public Boolean isContentTooLarge() { 635 return this.contentTooLarge; 636 } 637 638 /** 639 * Convenience method that provides the {@link #getBody()} bytes as a {@link String} encoded using the client-specified character set per {@link #getCharset()}. 640 * <p> 641 * If no character set is specified, {@link StandardCharsets#UTF_8} is used to perform the encoding. 642 * <p> 643 * 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. 644 * <p> 645 * This method is threadsafe. 646 * 647 * @return a {@link String} representation of this request's body, or {@link Optional#empty()} if no request body was specified by the client 648 */ 649 @Nonnull 650 public Optional<String> getBodyAsString() { 651 // Lazily instantiate a string instance using double-checked locking 652 String result = this.bodyAsString; 653 654 if (this.body != null && result == null) { 655 getLock().lock(); 656 657 try { 658 result = this.bodyAsString; 659 660 if (this.body != null && result == null) { 661 result = new String(this.body, getCharset().orElse(DEFAULT_CHARSET)); 662 this.bodyAsString = result; 663 } 664 } finally { 665 getLock().unlock(); 666 } 667 } 668 669 return Optional.ofNullable(result); 670 } 671 672 /** 673 * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Non-preflight CORS</a> request data. 674 * <p> 675 * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details. 676 * 677 * @return non-preflight CORS request data, or {@link Optional#empty()} if none was specified 678 */ 679 @Nonnull 680 public Optional<Cors> getCors() { 681 return Optional.ofNullable(this.cors); 682 } 683 684 /** 685 * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related request data. 686 * <p> 687 * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details. 688 * 689 * @return preflight CORS request data, or {@link Optional#empty()} if none was specified 690 */ 691 @Nonnull 692 public Optional<CorsPreflight> getCorsPreflight() { 693 return Optional.ofNullable(this.corsPreflight); 694 } 695 696 /** 697 * 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>. 698 * <p> 699 * 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. 700 * <p> 701 * This method is threadsafe. 702 * <p> 703 * See {@link #getLanguageRanges()} for a variant that pulls {@link LanguageRange} values. 704 * 705 * @return locale information for this request, or the empty list if none was specified 706 */ 707 @Nonnull 708 public List<Locale> getLocales() { 709 // Lazily instantiate our parsed locales using double-checked locking 710 List<Locale> result = this.locales; 711 712 if (result == null) { 713 getLock().lock(); 714 715 try { 716 result = this.locales; 717 718 if (result == null) { 719 Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language"); 720 721 if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) { 722 // Support data spread across multiple header lines, which spec allows 723 String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream() 724 .filter(value -> trimAggressivelyToEmpty(value).length() > 0) 725 .collect(Collectors.joining(",")); 726 727 try { 728 result = unmodifiableList(Utilities.extractLocalesFromAcceptLanguageHeaderValue(acceptLanguageHeaderValue)); 729 } catch (Exception ignored) { 730 // Malformed Accept-Language header; ignore it 731 result = List.of(); 732 } 733 } else { 734 result = List.of(); 735 } 736 737 this.locales = result; 738 } 739 } finally { 740 getLock().unlock(); 741 } 742 } 743 744 return result; 745 } 746 747 /** 748 * {@link LanguageRange} information for this request as specified by {@code Accept-Language} header value[s]. 749 * <p> 750 * 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. 751 * <p> 752 * This method is threadsafe. 753 * <p> 754 * See {@link #getLocales()} for a variant that pulls {@link Locale} values. 755 * 756 * @return language range information for this request, or the empty list if none was specified 757 */ 758 @Nonnull 759 public List<LanguageRange> getLanguageRanges() { 760 // Lazily instantiate our parsed language ranges using double-checked locking 761 List<LanguageRange> result = this.languageRanges; 762 763 if (result == null) { 764 getLock().lock(); 765 try { 766 result = this.languageRanges; 767 768 if (result == null) { 769 Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language"); 770 771 if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) { 772 // Support data spread across multiple header lines, which spec allows 773 String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream() 774 .filter(value -> trimAggressivelyToEmpty(value).length() > 0) 775 .collect(Collectors.joining(",")); 776 777 try { 778 result = Collections.unmodifiableList(LanguageRange.parse(acceptLanguageHeaderValue)); 779 } catch (Exception ignored) { 780 // Malformed Accept-Language header; ignore it 781 result = List.of(); 782 } 783 } else { 784 result = List.of(); 785 } 786 787 this.languageRanges = result; 788 } 789 } finally { 790 getLock().unlock(); 791 } 792 } 793 794 return result; 795 } 796 797 /** 798 * Convenience method to access a decoded query parameter's value when at most one is expected for the given {@code name}. 799 * <p> 800 * If a query parameter {@code name} can support multiple values, {@link #getQueryParameters()} should be used instead of this method. 801 * <p> 802 * If this method is invoked for a query parameter {@code name} with multiple values, Soklet will throw {@link IllegalQueryParameterException}. 803 * <p> 804 * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em> 805 * 806 * @param name the name of the query parameter 807 * @return the value for the query parameter, or {@link Optional#empty()} if none is present 808 * @throws IllegalQueryParameterException if the query parameter with the given {@code name} has multiple values 809 */ 810 @Nonnull 811 public Optional<String> getQueryParameter(@Nonnull String name) { 812 requireNonNull(name); 813 814 try { 815 return singleValueForName(name, getQueryParameters()); 816 } catch (MultipleValuesException e) { 817 @SuppressWarnings("unchecked") 818 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 819 throw new IllegalQueryParameterException(format("Multiple values specified for query parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 820 } 821 } 822 823 /** 824 * Convenience method to access a decoded form parameter's value when at most one is expected for the given {@code name}. 825 * <p> 826 * If a form parameter {@code name} can support multiple values, {@link #getFormParameters()} should be used instead of this method. 827 * <p> 828 * If this method is invoked for a form parameter {@code name} with multiple values, Soklet will throw {@link IllegalFormParameterException}. 829 * <p> 830 * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em> 831 * 832 * @param name the name of the form parameter 833 * @return the value for the form parameter, or {@link Optional#empty()} if none is present 834 * @throws IllegalFormParameterException if the form parameter with the given {@code name} has multiple values 835 */ 836 @Nonnull 837 public Optional<String> getFormParameter(@Nonnull String name) { 838 requireNonNull(name); 839 840 try { 841 return singleValueForName(name, getFormParameters()); 842 } catch (MultipleValuesException e) { 843 @SuppressWarnings("unchecked") 844 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 845 throw new IllegalFormParameterException(format("Multiple values specified for form parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 846 } 847 } 848 849 /** 850 * Convenience method to access a header's value when at most one is expected for the given {@code name}. 851 * <p> 852 * If a header {@code name} can support multiple values, {@link #getHeaders()} should be used instead of this method. 853 * <p> 854 * If this method is invoked for a header {@code name} with multiple values, Soklet will throw {@link IllegalRequestHeaderException}. 855 * <p> 856 * <em>Note that request headers have case-insensitive names per the HTTP spec.</em> 857 * 858 * @param name the name of the header 859 * @return the value for the header, or {@link Optional#empty()} if none is present 860 * @throws IllegalRequestHeaderException if the header with the given {@code name} has multiple values 861 */ 862 @Nonnull 863 public Optional<String> getHeader(@Nonnull String name) { 864 requireNonNull(name); 865 866 try { 867 return singleValueForName(name, getHeaders()); 868 } catch (MultipleValuesException e) { 869 @SuppressWarnings("unchecked") 870 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 871 throw new IllegalRequestHeaderException(format("Multiple values specified for request header '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 872 } 873 } 874 875 /** 876 * Convenience method to access a cookie's value when at most one is expected for the given {@code name}. 877 * <p> 878 * If a cookie {@code name} can support multiple values, {@link #getCookies()} should be used instead of this method. 879 * <p> 880 * If this method is invoked for a cookie {@code name} with multiple values, Soklet will throw {@link IllegalRequestCookieException}. 881 * <p> 882 * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em> 883 * 884 * @param name the name of the cookie 885 * @return the value for the cookie, or {@link Optional#empty()} if none is present 886 * @throws IllegalRequestCookieException if the cookie with the given {@code name} has multiple values 887 */ 888 @Nonnull 889 public Optional<String> getCookie(@Nonnull String name) { 890 requireNonNull(name); 891 892 try { 893 return singleValueForName(name, getCookies()); 894 } catch (MultipleValuesException e) { 895 @SuppressWarnings("unchecked") 896 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 897 throw new IllegalRequestCookieException(format("Multiple values specified for request cookie '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 898 } 899 } 900 901 /** 902 * Convenience method to access a decoded multipart field when at most one is expected for the given {@code name}. 903 * <p> 904 * If a {@code name} can support multiple multipart fields, {@link #getMultipartFields()} should be used instead of this method. 905 * <p> 906 * If this method is invoked for a {@code name} with multiple multipart field values, Soklet will throw {@link IllegalMultipartFieldException}. 907 * <p> 908 * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em> 909 * 910 * @param name the name of the multipart field 911 * @return the multipart field value, or {@link Optional#empty()} if none is present 912 * @throws IllegalMultipartFieldException if the multipart field with the given {@code name} has multiple values 913 */ 914 @Nonnull 915 public Optional<MultipartField> getMultipartField(@Nonnull String name) { 916 requireNonNull(name); 917 918 try { 919 return singleValueForName(name, getMultipartFields()); 920 } catch (MultipleValuesException e) { 921 @SuppressWarnings("unchecked") 922 MultipartField firstMultipartField = getMultipartFields().get(name).stream().findFirst().get(); 923 String valuesAsString = format("[%s]", e.getValues().stream() 924 .map(multipartField -> multipartField.toString()) 925 .collect(Collectors.joining(", "))); 926 927 throw new IllegalMultipartFieldException(format("Multiple values specified for multipart field '%s' (but expected single value): %s", name, valuesAsString), firstMultipartField); 928 } 929 } 930 931 @Nonnull 932 private MultipartParser getMultipartParser() { 933 return this.multipartParser; 934 } 935 936 @Nonnull 937 private IdGenerator<?> getIdGenerator() { 938 return this.idGenerator; 939 } 940 941 @Nonnull 942 private ReentrantLock getLock() { 943 return this.lock; 944 } 945 946 @Nonnull 947 private <T> Optional<T> singleValueForName(@Nonnull String name, 948 @Nullable Map<String, Set<T>> valuesByName) throws MultipleValuesException { 949 if (valuesByName == null) 950 return Optional.empty(); 951 952 Set<T> values = valuesByName.get(name); 953 954 if (values == null) 955 return Optional.empty(); 956 957 if (values.size() > 1) 958 throw new MultipleValuesException(name, values); 959 960 return values.stream().findFirst(); 961 } 962 963 @NotThreadSafe 964 private static class MultipleValuesException extends Exception { 965 @Nonnull 966 private final String name; 967 @Nonnull 968 private final Set<?> values; 969 970 private MultipleValuesException(@Nonnull String name, 971 @Nonnull Set<?> values) { 972 super(format("Expected single value but found %d values for '%s': %s", values.size(), name, values)); 973 974 requireNonNull(name); 975 requireNonNull(values); 976 977 this.name = name; 978 this.values = Collections.unmodifiableSet(new LinkedHashSet<>(values)); 979 } 980 981 @Nonnull 982 public String getName() { 983 return this.name; 984 } 985 986 @Nonnull 987 public Set<?> getValues() { 988 return this.values; 989 } 990 } 991 992 /** 993 * Builder used to construct instances of {@link Request} via {@link Request#withRawUrl(HttpMethod, String)}. 994 * <p> 995 * This class is intended for use by a single thread. 996 * 997 * @author <a href="https://www.revetkn.com">Mark Allen</a> 998 */ 999 @NotThreadSafe 1000 public static final class RawBuilder { 1001 @Nonnull 1002 private HttpMethod httpMethod; 1003 @Nonnull 1004 private String rawUrl; 1005 @Nullable 1006 private Object id; 1007 @Nullable 1008 private IdGenerator idGenerator; 1009 @Nullable 1010 private MultipartParser multipartParser; 1011 @Nullable 1012 private Map<String, Set<String>> headers; 1013 @Nullable 1014 private byte[] body; 1015 @Nullable 1016 private Boolean contentTooLarge; 1017 1018 protected RawBuilder(@Nonnull HttpMethod httpMethod, 1019 @Nonnull String rawUrl) { 1020 requireNonNull(httpMethod); 1021 requireNonNull(rawUrl); 1022 1023 this.httpMethod = httpMethod; 1024 this.rawUrl = rawUrl; 1025 } 1026 1027 @Nonnull 1028 public RawBuilder httpMethod(@Nonnull HttpMethod httpMethod) { 1029 requireNonNull(httpMethod); 1030 this.httpMethod = httpMethod; 1031 return this; 1032 } 1033 1034 @Nonnull 1035 public RawBuilder rawUrl(@Nonnull String rawUrl) { 1036 requireNonNull(rawUrl); 1037 this.rawUrl = rawUrl; 1038 return this; 1039 } 1040 1041 @Nonnull 1042 public RawBuilder id(@Nullable Object id) { 1043 this.id = id; 1044 return this; 1045 } 1046 1047 @Nonnull 1048 public RawBuilder idGenerator(@Nullable IdGenerator idGenerator) { 1049 this.idGenerator = idGenerator; 1050 return this; 1051 } 1052 1053 @Nonnull 1054 public RawBuilder multipartParser(@Nullable MultipartParser multipartParser) { 1055 this.multipartParser = multipartParser; 1056 return this; 1057 } 1058 1059 @Nonnull 1060 public RawBuilder headers(@Nullable Map<String, Set<String>> headers) { 1061 this.headers = headers; 1062 return this; 1063 } 1064 1065 @Nonnull 1066 public RawBuilder body(@Nullable byte[] body) { 1067 this.body = body; 1068 return this; 1069 } 1070 1071 @Nonnull 1072 public RawBuilder contentTooLarge(@Nullable Boolean contentTooLarge) { 1073 this.contentTooLarge = contentTooLarge; 1074 return this; 1075 } 1076 1077 @Nonnull 1078 public Request build() { 1079 return new Request(this, null); 1080 } 1081 } 1082 1083 /** 1084 * Builder used to construct instances of {@link Request} via {@link Request#withPath(HttpMethod, String)}. 1085 * <p> 1086 * This class is intended for use by a single thread. 1087 * 1088 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1089 */ 1090 @NotThreadSafe 1091 public static final class PathBuilder { 1092 @Nonnull 1093 private HttpMethod httpMethod; 1094 @Nonnull 1095 private String path; 1096 @Nullable 1097 private String rawPath; 1098 @Nullable 1099 private String rawQuery; 1100 @Nullable 1101 private Object id; 1102 @Nullable 1103 private IdGenerator idGenerator; 1104 @Nullable 1105 private MultipartParser multipartParser; 1106 @Nullable 1107 private Map<String, Set<String>> queryParameters; 1108 @Nullable 1109 private Map<String, Set<String>> headers; 1110 @Nullable 1111 private byte[] body; 1112 @Nullable 1113 private Boolean contentTooLarge; 1114 1115 protected PathBuilder(@Nonnull HttpMethod httpMethod, 1116 @Nonnull String path) { 1117 requireNonNull(httpMethod); 1118 requireNonNull(path); 1119 1120 this.httpMethod = httpMethod; 1121 this.path = path; 1122 } 1123 1124 @Nonnull 1125 public PathBuilder httpMethod(@Nonnull HttpMethod httpMethod) { 1126 requireNonNull(httpMethod); 1127 this.httpMethod = httpMethod; 1128 return this; 1129 } 1130 1131 @Nonnull 1132 public PathBuilder path(@Nonnull String path) { 1133 requireNonNull(path); 1134 this.path = path; 1135 return this; 1136 } 1137 1138 // Package-private setter for raw value (used by Copier) 1139 @Nonnull 1140 PathBuilder rawPath(@Nullable String rawPath) { 1141 this.rawPath = rawPath; 1142 return this; 1143 } 1144 1145 // Package-private setter for raw value (used by Copier) 1146 @Nonnull 1147 PathBuilder rawQuery(@Nullable String rawQuery) { 1148 this.rawQuery = rawQuery; 1149 return this; 1150 } 1151 1152 @Nonnull 1153 public PathBuilder id(@Nullable Object id) { 1154 this.id = id; 1155 return this; 1156 } 1157 1158 @Nonnull 1159 public PathBuilder idGenerator(@Nullable IdGenerator idGenerator) { 1160 this.idGenerator = idGenerator; 1161 return this; 1162 } 1163 1164 @Nonnull 1165 public PathBuilder multipartParser(@Nullable MultipartParser multipartParser) { 1166 this.multipartParser = multipartParser; 1167 return this; 1168 } 1169 1170 @Nonnull 1171 public PathBuilder queryParameters(@Nullable Map<String, Set<String>> queryParameters) { 1172 this.queryParameters = queryParameters; 1173 return this; 1174 } 1175 1176 @Nonnull 1177 public PathBuilder headers(@Nullable Map<String, Set<String>> headers) { 1178 this.headers = headers; 1179 return this; 1180 } 1181 1182 @Nonnull 1183 public PathBuilder body(@Nullable byte[] body) { 1184 this.body = body; 1185 return this; 1186 } 1187 1188 @Nonnull 1189 public PathBuilder contentTooLarge(@Nullable Boolean contentTooLarge) { 1190 this.contentTooLarge = contentTooLarge; 1191 return this; 1192 } 1193 1194 @Nonnull 1195 public Request build() { 1196 return new Request(null, this); 1197 } 1198 } 1199 1200 /** 1201 * Builder used to copy instances of {@link Request} via {@link Request#copy()}. 1202 * <p> 1203 * This class is intended for use by a single thread. 1204 * 1205 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1206 */ 1207 @NotThreadSafe 1208 public static final class Copier { 1209 @Nonnull 1210 private final PathBuilder builder; 1211 1212 // Track original raw values and modification state 1213 @Nullable 1214 private String originalRawPath; 1215 @Nullable 1216 private String originalRawQuery; 1217 @Nonnull 1218 private Boolean pathModified = false; 1219 @Nonnull 1220 private Boolean queryParametersModified = false; 1221 1222 Copier(@Nonnull Request request) { 1223 requireNonNull(request); 1224 1225 this.originalRawPath = request.getRawPath(); 1226 this.originalRawQuery = request.rawQuery; // Direct field access 1227 1228 this.builder = new PathBuilder(request.getHttpMethod(), request.getPath()) 1229 .id(request.getId()) 1230 .queryParameters(new LinkedHashMap<>(request.getQueryParameters())) 1231 .headers(new LinkedCaseInsensitiveMap<>(request.getHeaders())) 1232 .body(request.body) // Direct field access to avoid array copy 1233 .multipartParser(request.getMultipartParser()) 1234 .idGenerator(request.getIdGenerator()) 1235 .contentTooLarge(request.isContentTooLarge()) 1236 // Preserve original raw values initially 1237 .rawPath(this.originalRawPath) 1238 .rawQuery(this.originalRawQuery); 1239 } 1240 1241 @Nonnull 1242 public Copier httpMethod(@Nonnull HttpMethod httpMethod) { 1243 requireNonNull(httpMethod); 1244 this.builder.httpMethod(httpMethod); 1245 return this; 1246 } 1247 1248 @Nonnull 1249 public Copier path(@Nonnull String path) { 1250 requireNonNull(path); 1251 this.pathModified = true; 1252 // Clear preserved raw path since decoded path changed 1253 this.builder.rawPath(null); 1254 return this; 1255 } 1256 1257 @Nonnull 1258 public Copier id(@Nullable Object id) { 1259 this.builder.id(id); 1260 return this; 1261 } 1262 1263 @Nonnull 1264 public Copier queryParameters(@Nullable Map<String, Set<String>> queryParameters) { 1265 this.builder.queryParameters(queryParameters); 1266 this.queryParametersModified = true; 1267 // Clear preserved raw query since decoded query parameters changed 1268 this.builder.rawQuery(null); 1269 return this; 1270 } 1271 1272 // Convenience method for mutation 1273 @Nonnull 1274 public Copier queryParameters(@Nonnull Consumer<Map<String, Set<String>>> queryParametersConsumer) { 1275 requireNonNull(queryParametersConsumer); 1276 1277 if (this.builder.queryParameters == null) 1278 this.builder.queryParameters(new LinkedHashMap<>()); 1279 1280 queryParametersConsumer.accept(this.builder.queryParameters); 1281 this.queryParametersModified = true; 1282 // Clear preserved raw query since decoded query parameters changed 1283 this.builder.rawQuery(null); 1284 return this; 1285 } 1286 1287 @Nonnull 1288 public Copier headers(@Nullable Map<String, Set<String>> headers) { 1289 this.builder.headers(headers); 1290 return this; 1291 } 1292 1293 // Convenience method for mutation 1294 @Nonnull 1295 public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) { 1296 requireNonNull(headersConsumer); 1297 1298 if (this.builder.headers == null) 1299 this.builder.headers(new LinkedCaseInsensitiveMap<>()); 1300 1301 headersConsumer.accept(this.builder.headers); 1302 return this; 1303 } 1304 1305 @Nonnull 1306 public Copier body(@Nullable byte[] body) { 1307 this.builder.body(body); 1308 return this; 1309 } 1310 1311 @Nonnull 1312 public Copier contentTooLarge(@Nullable Boolean contentTooLarge) { 1313 this.builder.contentTooLarge(contentTooLarge); 1314 return this; 1315 } 1316 1317 @Nonnull 1318 public Request finish() { 1319 return this.builder.build(); 1320 } 1321 } 1322}