001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.httppart; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023 024import java.io.*; 025import java.lang.reflect.*; 026import java.util.*; 027 028import org.apache.juneau.commons.conversion.*; 029import org.apache.juneau.commons.http.*; 030import org.apache.juneau.commons.httppart.*; 031import org.apache.juneau.commons.io.*; 032import org.apache.juneau.http.header.*; 033import org.apache.juneau.http.response.*; 034import org.apache.juneau.marshall.*; 035import org.apache.juneau.marshall.collections.*; 036import org.apache.juneau.marshall.encoders.*; 037import org.apache.juneau.marshall.httppart.*; 038import org.apache.juneau.marshall.marshaller.*; 039import org.apache.juneau.marshall.parser.*; 040import org.apache.juneau.rest.server.*; 041import org.apache.juneau.rest.server.util.*; 042 043import jakarta.servlet.*; 044 045/** 046 * Contains the content of the HTTP request. 047 * 048 * <p> 049 * The {@link RequestContent} object is the API for accessing the content of an HTTP request. 050 * It can be accessed by passing it as a parameter on your REST Java method: 051 * </p> 052 * <p class='bjava'> 053 * <ja>@RestPost</ja>(...) 054 * <jk>public</jk> Object myMethod(RequestContent <jv>content</jv>) {...} 055 * </p> 056 * 057 * <h5 class='figure'>Example:</h5> 058 * <p class='bjava'> 059 * <ja>@RestPost</ja>(...) 060 * <jk>public void</jk> doPost(RequestContent <jv>content</jv>) { 061 * <jc>// Convert content to a linked list of Person objects.</jc> 062 * List<Person> <jv>list</jv> = <jv>content</jv>.as(LinkedList.<jk>class</jk>, Person.<jk>class</jk>); 063 * ... 064 * } 065 * </p> 066 * 067 * <p> 068 * Some important methods on this class are: 069 * </p> 070 * <ul class='javatree'> 071 * <li class='jc'>{@link RequestContent} 072 * <ul class='spaced-list'> 073 * <li>Methods for accessing the raw contents of the request content: 074 * <ul class='javatreec'> 075 * <li class='jm'>{@link RequestContent#asBytes() asBytes()} 076 * <li class='jm'>{@link RequestContent#asHex() asHex()} 077 * <li class='jm'>{@link RequestContent#asSpacedHex() asSpacedHex()} 078 * <li class='jm'>{@link RequestContent#asString() asString()} 079 * <li class='jm'>{@link RequestContent#getInputStream() getInputStream()} 080 * <li class='jm'>{@link RequestContent#getReader() getReader()} 081 * </ul> 082 * <li>Methods for parsing the contents of the request content: 083 * <ul class='javatreec'> 084 * <li class='jm'>{@link RequestContent#as(Class) as(Class)} 085 * <li class='jm'>{@link RequestContent#as(Type, Type...) as(Type, Type...)} 086 * <li class='jm'>{@link RequestContent#setSchema(HttpPartSchema) setSchema(HttpPartSchema)} 087 * </ul> 088 * <li>Other methods: 089 * <ul class='javatreec'> 090 * <li class='jm'>{@link RequestContent#cache() cache()} 091 * <li class='jm'>{@link RequestContent#getParserMatch() getParserMatch()} 092 * </ul> 093 * </ul> 094 * </ul> 095 * 096 * <h5 class='section'>See Also:</h5><ul> 097 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerHttpParts">HTTP parts in REST servers</a> 098 * </ul> 099 */ 100@SuppressWarnings({ 101 "resource", // Resource management handled externally 102 "unchecked" // Type erasure requires unchecked casts in content parsing 103}) 104public class RequestContent { 105 106 private byte[] content; 107 private final RestRequest req; 108 private EncoderSet encoders; 109 private Encoder encoder; 110 @SuppressWarnings({ 111 "java:S1450" // Field backs the public fluent parsers(ParserSet) setter; the value is set externally and consumed later in getParserMatch(), so it cannot be a local variable. 112 }) 113 private ParserSet parsers; 114 private long maxInput; 115 private int contentLength; 116 private MediaType mediaType; 117 private Parser parser; 118 private HttpPartSchema schema; 119 120 /** 121 * Constructor. 122 * 123 * @param req The request creating this bean. 124 */ 125 public RequestContent(RestRequest req) { 126 this.req = req; 127 } 128 129 /** 130 * Reads the input from the HTTP request parsed into a POJO. 131 * 132 * <p> 133 * The parser used is determined by the matching <c>Content-Type</c> header on the request. 134 * 135 * <p> 136 * If type is <jk>null</jk> or <code>Object.<jk>class</jk></code>, then the actual type will be determined 137 * automatically based on the following input: 138 * <table class='styled'> 139 * <tr><th>Type</th><th>JSON input</th><th>XML input</th><th>Return type</th></tr> 140 * <tr> 141 * <td>object</td> 142 * <td><js>"{...}"</js></td> 143 * <td><code><xt><object></xt>...<xt></object></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'object'</xs><xt>></xt>...<xt></x></xt></code></td> 144 * <td>{@link JsonMap}</td> 145 * </tr> 146 * <tr> 147 * <td>array</td> 148 * <td><js>"[...]"</js></td> 149 * <td><code><xt><array></xt>...<xt></array></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'array'</xs><xt>></xt>...<xt></x></xt></code></td> 150 * <td>{@link JsonList}</td> 151 * </tr> 152 * <tr> 153 * <td>string</td> 154 * <td><js>"'...'"</js></td> 155 * <td><code><xt><string></xt>...<xt></string></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'string'</xs><xt>></xt>...<xt></x></xt></code></td> 156 * <td>{@link String}</td> 157 * </tr> 158 * <tr> 159 * <td>number</td> 160 * <td><c>123</c></td> 161 * <td><code><xt><number></xt>123<xt></number></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'number'</xs><xt>></xt>...<xt></x></xt></code></td> 162 * <td>{@link Number}</td> 163 * </tr> 164 * <tr> 165 * <td>boolean</td> 166 * <td><jk>true</jk></td> 167 * <td><code><xt><boolean></xt>true<xt></boolean></xt></code><br><code><xt><x</xt> <xa>type</xa>=<xs>'boolean'</xs><xt>></xt>...<xt></x></xt></code></td> 168 * <td>{@link Boolean}</td> 169 * </tr> 170 * <tr> 171 * <td>null</td> 172 * <td><jk>null</jk> or blank</td> 173 * <td><code><xt><null/></xt></code> or blank<br><code><xt><x</xt> <xa>type</xa>=<xs>'null'</xs><xt>/></xt></code></td> 174 * <td><jk>null</jk></td> 175 * </tr> 176 * </table> 177 * 178 * <p> 179 * Refer to <a class="doclink" href="https://juneau.apache.org/docs/topics/PojoCategories">POJO Categories</a> for a complete definition of supported POJOs. 180 * 181 * <h5 class='section'>Examples:</h5> 182 * <p class='bjava'> 183 * <jc>// Parse into an integer.</jc> 184 * <jk>int</jk> <jv>content1</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>.<jk>class</jk>); 185 * 186 * <jc>// Parse into an int array.</jc> 187 * <jk>int</jk>[] <jv>content2</jv> = <jv>req</jv>.getContent().as(<jk>int</jk>[].<jk>class</jk>); 188 * 189 * <jc>// Parse into a bean.</jc> 190 * MyBean <jv>content3</jv> = <jv>req</jv>.getContent().as(MyBean.<jk>class</jk>); 191 * 192 * <jc>// Parse into a linked-list of objects.</jc> 193 * List <jv>content4</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>); 194 * 195 * <jc>// Parse into a map of object keys/values.</jc> 196 * Map <jv>content5</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>); 197 * </p> 198 * 199 * <h5 class='section'>Notes:</h5><ul> 200 * <li class='note'> 201 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 202 * </ul> 203 * 204 * @param type 205 * The class type to instantiate. 206 * <br>Can be <jk>null</jk> or <code>Object.<jk>class</jk></code> to auto-detect the type based on the input. 207 * @param <T> The class type to instantiate. 208 * @return The input parsed to a POJO. 209 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 210 * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers. 211 * @throws InternalServerError Thrown if an {@link IOException} occurs. 212 */ 213 public <T> T as(Class<T> type) throws BadRequest, UnsupportedMediaType, InternalServerError { 214 return getInner(getClassMeta(type)); 215 } 216 217 /** 218 * Reads the input from the HTTP request parsed into a POJO. 219 * 220 * <p> 221 * This is similar to {@link #as(Class)} but allows for complex collections of POJOs to be created. 222 * 223 * <h5 class='section'>Examples:</h5> 224 * <p class='bjava'> 225 * <jc>// Parse into a linked-list of strings.</jc> 226 * List<String> <jv>content1</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, String.<jk>class</jk>); 227 * 228 * <jc>// Parse into a linked-list of linked-lists of strings.</jc> 229 * List<List<String>> <jv>content2</jv> = <jv>req</jv>.getContent().as(LinkedList.<jk>class</jk>, LinkedList.<jk>class</jk>, String.<jk>class</jk>); 230 * 231 * <jc>// Parse into a map of string keys/values.</jc> 232 * Map<String,String> <jv>content3</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, String.<jk>class</jk>); 233 * 234 * <jc>// Parse into a map containing string keys and values of lists containing beans.</jc> 235 * Map<String,List<MyBean>> <jv>content4</jv> = <jv>req</jv>.getContent().as(TreeMap.<jk>class</jk>, String.<jk>class</jk>, List.<jk>class</jk>, MyBean.<jk>class</jk>); 236 * </p> 237 * 238 * <h5 class='section'>Notes:</h5><ul> 239 * <li class='note'> 240 * <c>Collections</c> must be followed by zero or one parameter representing the value type. 241 * <li class='note'> 242 * <c>Maps</c> must be followed by zero or two parameters representing the key and value types. 243 * <li class='note'> 244 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 245 * </ul> 246 * 247 * @param type 248 * The type of object to create. 249 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 250 * @param args 251 * The type arguments of the class if it's a collection or map. 252 * <br>Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType} 253 * <br>Ignored if the main type is not a map or collection. 254 * @param <T> The class type to instantiate. 255 * @return The input parsed to a POJO. 256 * @throws BadRequest Thrown if input could not be parsed or fails schema validation. 257 * @throws UnsupportedMediaType Thrown if the Content-Type header value is not supported by one of the parsers. 258 * @throws InternalServerError Thrown if an {@link IOException} occurs. 259 */ 260 public <T> T as(Type type, Type...args) throws BadRequest, UnsupportedMediaType, InternalServerError { 261 return getInner(this.<T>getClassMeta(type, args)); 262 } 263 264 /** 265 * Returns the HTTP content as a byte array. 266 * 267 * <h5 class='section'>Notes:</h5><ul> 268 * <li class='note'> 269 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 270 * </ul> 271 * 272 * @return The incoming input from the connection as a byte array. 273 * @throws IOException If a problem occurred trying to read from the reader. 274 */ 275 public byte[] asBytes() throws IOException { 276 cache(); 277 return content; 278 } 279 280 /** 281 * Returns the HTTP content content as a simple hexadecimal character string. 282 * 283 * <h5 class='section'>Example:</h5> 284 * <p class='bcode'> 285 * 0123456789ABCDEF 286 * </p> 287 * 288 * @return The incoming input from the connection as a plain string. 289 * @throws IOException If a problem occurred trying to read from the reader. 290 */ 291 public String asHex() throws IOException { 292 cache(); 293 return toHex(content); 294 } 295 296 /** 297 * Returns the HTTP content content as a simple space-delimited hexadecimal character string. 298 * 299 * <h5 class='section'>Example:</h5> 300 * <p class='bcode'> 301 * 01 23 45 67 89 AB CD EF 302 * </p> 303 * 304 * @return The incoming input from the connection as a plain string. 305 * @throws IOException If a problem occurred trying to read from the reader. 306 */ 307 public String asSpacedHex() throws IOException { 308 cache(); 309 return toSpacedHex(content); 310 } 311 312 /** 313 * Returns the HTTP content content as a plain string. 314 * 315 * <h5 class='section'>Notes:</h5><ul> 316 * <li class='note'> 317 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 318 * </ul> 319 * 320 * @return The incoming input from the connection as a plain string. 321 * @throws IOException If a problem occurred trying to read from the reader. 322 */ 323 public String asString() throws IOException { 324 cache(); 325 return new String(content, UTF8); 326 } 327 328 /** 329 * Caches the content in memory for reuse. 330 * 331 * @return This object. 332 * @throws IOException If error occurs while reading stream. 333 */ 334 public RequestContent cache() throws IOException { 335 if (content == null) 336 content = readBytes(getInputStream()); 337 return this; 338 } 339 340 /** 341 * Sets the contents of this content. 342 * 343 * @param value The new value for this setting. 344 * @return This object. 345 */ 346 public RequestContent content(byte[] value) { 347 content = value; 348 return this; 349 } 350 351 /** 352 * Sets the encoders to use for decoding this content. 353 * 354 * @param value The new value for this setting. 355 * @return This object. 356 */ 357 public RequestContent encoders(EncoderSet value) { 358 encoders = value; 359 return this; 360 } 361 362 /** 363 * Returns the content length of the content. 364 * 365 * @return The content length of the content in bytes. 366 */ 367 public int getContentLength() { return contentLength == 0 ? req.getHttpServletRequest().getContentLength() : contentLength; } 368 369 /** 370 * Returns the HTTP content content as an {@link InputStream}. 371 * 372 * @return The negotiated input stream. 373 * @throws IOException If any error occurred while trying to get the input stream or wrap it in the GZIP wrapper. 374 */ 375 public ServletInputStream getInputStream() throws IOException { 376 377 if (nn(content)) 378 return new BoundedServletInputStream(content); 379 380 var enc = getEncoder(); 381 382 var is = req.getHttpServletRequest().getInputStream(); 383 384 if (enc == null) 385 return new BoundedServletInputStream(is, maxInput); 386 387 return new BoundedServletInputStream(enc.getInputStream(is), maxInput); 388 } 389 390 /** 391 * Returns the parser and media type matching the request <c>Content-Type</c> header. 392 * 393 * @return 394 * The parser matching the request <c>Content-Type</c> header, or {@link Optional#empty()} if no matching parser was 395 * found. 396 * Includes the matching media type. 397 */ 398 public Optional<ParserMatch> getParserMatch() { 399 if (nn(mediaType) && nn(parser)) 400 return o(new ParserMatch(mediaType, parser)); 401 var mt = getMediaType(); 402 return o(mt).flatMap(parsers::getParserMatch); 403 } 404 405 /** 406 * Returns the HTTP content content as a {@link Reader}. 407 * 408 * <h5 class='section'>Notes:</h5><ul> 409 * <li class='note'> 410 * If {@code allowContentParam} init parameter is true, then first looks for {@code &content=xxx} in the URL query string. 411 * <li class='note'> 412 * Automatically handles GZipped input streams. 413 * <li class='note'> 414 * The initial buffer is sized off the request's declared {@code Content-Length}, but never past the 415 * effective {@code @Rest(maxInput)} ceiling — a client can send whatever length it likes in that 416 * header, regardless of how much (or little) content actually follows. 417 * </ul> 418 * 419 * @return The content contents as a reader. 420 * @throws IOException Thrown by underlying stream. 421 */ 422 public BufferedReader getReader() throws IOException { 423 var r = getUnbufferedReader(); 424 if (r instanceof BufferedReader r2) 425 return r2; 426 int len = req.getHttpServletRequest().getContentLength(); 427 return new BufferedReader(r, computeReaderBufferSize(len, maxInput)); 428 } 429 430 /** 431 * Computes the initial char-buffer size for {@link #getReader()}. 432 * 433 * <p> 434 * Package-private (rather than folded directly into {@link #getReader()}) so the sizing decision can be 435 * exercised directly in unit tests independent of a live servlet request. 436 * 437 * @param declaredContentLength The request's declared {@code Content-Length}, as returned by 438 * {@code HttpServletRequest.getContentLength()}. May be {@code <= 0} if absent/unparsable. 439 * @param maxInput The effective {@code @Rest(maxInput)} ceiling in bytes. Values {@code <= 0} are 440 * treated as "no additional ceiling beyond {@code Integer.MAX_VALUE}". 441 * @return The buffer size to use, at least the historical {@code 8192}-byte default and never past 442 * {@code min(maxInput, Integer.MAX_VALUE)}. 443 */ 444 static int computeReaderBufferSize(int declaredContentLength, long maxInput) { 445 long ceiling = maxInput > 0 ? maxInput : Integer.MAX_VALUE; 446 long buffSize = declaredContentLength <= 0 ? 8192 : Math.min(Math.max(declaredContentLength, 8192), ceiling); 447 return (int)Math.min(buffSize, Integer.MAX_VALUE); 448 } 449 450 /** 451 * Sets the max input value for this content. 452 * 453 * @param value The new value for this setting. 454 * @return This object. 455 */ 456 public RequestContent maxInput(long value) { 457 maxInput = value; 458 return this; 459 } 460 461 /** 462 * Sets the media type of this content. 463 * 464 * @param value The new value for this setting. 465 * @return This object. 466 */ 467 public RequestContent mediaType(MediaType value) { 468 mediaType = value; 469 return this; 470 } 471 472 /** 473 * Sets the parser to use for this content. 474 * 475 * @param value The new value for this setting. 476 * @return This object. 477 */ 478 public RequestContent parser(Parser value) { 479 parser = value; 480 return this; 481 } 482 483 /** 484 * Sets the parsers to use for parsing this content. 485 * 486 * @param value The new value for this setting. 487 * @return This object. 488 */ 489 public RequestContent parsers(ParserSet value) { 490 parsers = value; 491 return this; 492 } 493 494 /** 495 * Sets the schema for this content. 496 * 497 * @param schema The new schema for this content. 498 * @return This object. 499 */ 500 public RequestContent setSchema(HttpPartSchema schema) { 501 this.schema = schema; 502 return this; 503 } 504 505 private <T> ClassMeta<T> getClassMeta(Class<T> type) { 506 return req.getMarshallingSession().getClassMeta(type); 507 } 508 509 private <T> ClassMeta<T> getClassMeta(Type type, Type...args) { 510 return req.getMarshallingSession().getClassMeta(type, args); 511 } 512 513 private Encoder getEncoder() throws UnsupportedMediaType { 514 if (encoder == null) { 515 var ce = req.getHeaderParam("content-encoding").orElse(null); 516 if (ine(ce)) { 517 ce = ce.trim(); 518 encoder = encoders.getEncoder(ce); 519 if (encoder == null) 520 throw new UnsupportedMediaType("Unsupported encoding in request header 'Content-Encoding': '%s'\n\tSupported codings: %s", 521 req.getHeaderParam("content-encoding").orElse(null), Json5.of(encoders.getSupportedEncodings())); 522 } 523 524 if (nn(encoder)) 525 contentLength = -1; 526 } 527 // Note that if this is the identity encoder, we want to return null 528 // so that we don't needlessly wrap the input stream. 529 if (encoder == IdentityEncoder.INSTANCE) 530 return null; 531 return encoder; 532 } 533 534 private <T> T getInner(ClassMeta<T> cm) throws BadRequest, UnsupportedMediaType, InternalServerError { 535 try { 536 return parse(cm); 537 } catch (UnsupportedMediaType e) { 538 throw e; 539 } catch (SchemaValidationException e) { 540 throw new BadRequest("Validation failed on request content. " + localizedMessage(e)); 541 } catch (ParseException e) { 542 throw new BadRequest(e, "Could not convert request content content to class type '%s'.", cm); 543 } catch (IOException e) { 544 throw new InternalServerError(e, "I/O exception occurred while parsing request content."); 545 } catch (Exception e) { 546 throw new InternalServerError(e, "Exception occurred while parsing request content."); 547 } 548 } 549 550 private MediaType getMediaType() { 551 if (nn(mediaType)) 552 return mediaType; 553 var ct = req.getHeader(ContentType.class); 554 if (! ct.isPresent() && nn(content)) 555 return MediaType.UON; 556 return ct.isPresent() ? ct.get().asMediaType().orElse(null) : null; 557 } 558 559 /* Workhorse method */ 560 @SuppressWarnings({ 561 "java:S3776" // Cognitive complexity acceptable for content parsing/schema validation 562 }) 563 private <T> T parse(ClassMeta<T> cm) throws SchemaValidationException, ParseException, UnsupportedMediaType, IOException { 564 565 if (cm.isReader()) 566 return (T)getReader(); 567 568 if (cm.isInputStream()) 569 return (T)getInputStream(); 570 571 var timeZone = req.getTimeZone(); 572 var locale = req.getLocale(); 573 var pm = getParserMatch().orElse(null); 574 575 if (schema == null) 576 schema = HttpPartSchema.DEFAULT; 577 578 if (nn(pm)) { 579 var p = pm.getParser(); 580 var mediaType2 = pm.getMediaType(); 581 // @formatter:off 582 var session = p 583 .createSession() 584 .properties(req.getParserSessionPropertyMap()) 585 .javaMethod(req.getOpContext().getJavaMethod()) 586 .locale(locale) 587 .timeZone(timeZone.orElse(null)) 588 .mediaType(mediaType2) 589 .apply(ReaderParser.Builder.class, x -> x.streamCharset(req.getCharset())) 590 .schema(schema) 591 .debug(req.isDebugMarshalling() ? true : null) 592 .build(); 593 // @formatter:on 594 595 try (Closeable in = session.isReaderParser() ? getUnbufferedReader() : getInputStream()) { 596 var o = session.read(in, cm); 597 drainQuietly(in); 598 if (nn(schema)) 599 schema.validateOutput(o); 600 return o; 601 } 602 } 603 604 if (cm.hasReaderMutater()) 605 return convertUnparsed(getReader(), cm); 606 607 if (cm.hasInputStreamMutater()) 608 return convertUnparsed(getInputStream(), cm); 609 610 var mt = getMediaType(); 611 612 if ((isEmpty(s(mt)) || mt.toString().startsWith("text/plain")) && cm.hasStringMutater()) 613 return BasicConverter.INSTANCE.to(asString(), cm.inner()); 614 615 throw unsupportedMediaType(null); 616 } 617 618 // Converts the unparsed content using the type's own Reader/InputStream constructor or factory method. 619 // 620 // When the request named a Content-Type that matched no parser, a conversion failure means the content was in a 621 // media type nobody here understands (for example JSON5 into the strict-JSON JsonMap(Reader) constructor), so it is 622 // reported as 415 rather than as a 500. Without a Content-Type the failure propagates as before. 623 private <T> T convertUnparsed(Object in, ClassMeta<T> cm) throws UnsupportedMediaType { 624 try { 625 return BasicConverter.INSTANCE.to(in, cm.inner()); 626 } catch (RuntimeException e) { 627 if (isEmpty(s(req.getHeader(ContentType.class).flatMap(ContentType::asMediaType).orElse(null)))) 628 throw e; 629 throw unsupportedMediaType(e); 630 } 631 } 632 633 private UnsupportedMediaType unsupportedMediaType(Throwable cause) { 634 var ct = req.getHeader(ContentType.class); 635 return new UnsupportedMediaType(cause, "Unsupported media-type in request header 'Content-Type': '%s'\n\tSupported media-types: %s", 636 ct.isPresent() ? ct.get().asMediaType().orElse(null) : "not-specified", Json5.of(req.getOpContext().getParsers().getSupportedMediaTypes())); 637 } 638 639 // Reads and discards whatever remains of the request entity after a parser has returned. 640 // 641 // A parser stops as soon as the top-level value is complete, leaving the end of the entity unread: for a 642 // Transfer-Encoding: chunked request, at least the zero-length terminating chunk whenever the client flushes it 643 // separately from the content. If that is still unread when the response completes, the container finds 644 // unconsumed request content and aborts the connection. By then the response is usually committed, so no 645 // Connection: close can be added, and a client reusing the pooled keep-alive connection fails its next request 646 // with an immediate EOF. Draining here (blocking until end-of-stream) keeps the connection reusable. 647 // 648 // Best effort: the stream is still bounded by @Rest(maxInput), and any failure (limit exceeded, client gone) 649 // just falls back to the container's own unconsumed-content handling. 650 /** 651 * Same as {@link #getReader()}, but doesn't encapsulate the result in a {@link BufferedReader}; 652 * 653 * @return An unbuffered reader. 654 * @throws IOException Thrown by underlying stream. 655 */ 656 protected Reader getUnbufferedReader() throws IOException { 657 if (nn(content)) 658 return new CharSequenceReader(new String(content, UTF8)); 659 return new InputStreamReader(getInputStream(), req.getCharset()); 660 } 661 662 boolean isLoaded() { return nn(content); } 663}