- edited description
ResettableRequestServletWrapper reports request body as missing when the first byte is FF
Issue #367
resolved
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 messageRequired 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
callsBody::hasBody
Body#hasBody
reads the first byte.- Reading is delegated to
ResettableRequestServletWrapper.CachingServletInputStream#read
, which reads theint
value of255
from input stream, and executesgetLatestChunk(index)[index] = (byte) value;
. HeregetLatestChunk(index)
allocates a new emptybyte[]
and puts the value ofbyte
=-1
into the array (because(byte) 255
is =-1
). - The original
int
value of255
is returned. Body#hasBody
“unreads” the first byte and returnstrue
, 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 returnsgetChunkForCurrentPos()[(int) (pos++ % CHUNK_SIZE)]
, which now returns andint
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 ofreturn getChunkForCurrentPos()[(int) (pos % CHUNK_SIZE)];
Version used: 2.21.0
Comments (5)
-
reporter -
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 usingByte#toUnsignedInt
). -
Thanks for raising the issue. And I already can confirm this bug.
I’ll see that as major priority. -
With this detailed description I was already able to upload a PR fixing this issue. Kudos!
-
- changed status to resolved
Available in 2.21.1
- Log in to comment