ResettableRequestServletWrapper reports request body as missing when the first byte is FF

Issue #367 resolved
Gediminas Rimša created an issue

Context:

We have an image-upload endpoint with request validation enabled. The intention is that it would validate the content-type header.

Open API spec:

  /projects/{projectId}/site-images:
    post:
      tags: [ File Storage ]
      summary: Uploads a site image
      operationId: uploadSiteImage
      parameters:
        - in: path
          name: projectId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        description: Binary image content
        required: true
        content:
          image/png:
            schema:
              type: string
              format: binary
          image/jpeg:
            schema:
              type: string
              format: binary
          image/gif:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: Success

Everything works as expected when uploading GIF or PNG images.

Symptom:

  • Uploading JPEG image fails with org.springframework.http.converter.HttpMessageNotReadableException with message Required request body is missing
    An example image is attached.

Root cause:

  • ResettableRequestServletWrapper.CachingServletInputStream#read returns cached byte values in range [-128;127] instead of [0;255].

Details:

  • If you inspect the attached image file with a hex editor, you’d see FF D8 FF E0 00 .. at the very beginning - FF D8 being the “start of image” marker bytes, so this would affect all JPEG images.
  • Stepping with a debugger, this is what happens:

    • RequestBodyValidator#validateRequestBody calls Body::hasBody
    • Body#hasBody reads the first byte.
    • Reading is delegated to ResettableRequestServletWrapper.CachingServletInputStream#read, which reads the int value of 255 from input stream, and executes getLatestChunk(index)[index] = (byte) value; . Here getLatestChunk(index) allocates a new empty byte[] and puts the value of byte = -1 into the array (because (byte) 255 is = -1).
    • The original int value of 255 is returned.
    • Body#hasBody “unreads” the first byte and returns true, i.e. body is available.
    • Later in the process Spring Framework (org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.EmptyBodyCheckingHttpInputMessage) checks if body is available by reading the first byte.
    • Then ResettableRequestServletWrapper.CachingServletInputStream#read checks that the first byte is available in cache (pos < count) and returns getChunkForCurrentPos()[(int) (pos++ % CHUNK_SIZE)], which now returns and int value of -1 (which signifies end of stream)
    • This “end of stream” signal causes the exception

Likely fix:

  • return Byte.toUnsignedInt(getChunkForCurrentPos()[(int) (pos % CHUNK_SIZE)]); instead of return getChunkForCurrentPos()[(int) (pos % CHUNK_SIZE)];

Version used: 2.21.0

Comments (5)

  1. Gediminas Rimša reporter

    By the way, to check if the body exists, Spring Framework is using java.io.PushbackInputStream in which reading from cache is implemented like this: return buf[pos++] & 0xff (i.e. equivalent to using Byte#toUnsignedInt).

  2. Sven Döring

    Thanks for raising the issue. And I already can confirm this bug.
    I’ll see that as major priority.

  3. Log in to comment