[
https://issues.apache.org/jira/browse/CAMEL-25573?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-25573.
---------------------------------
Resolution: Fixed
Fixed by https://github.com/apache/camel/pull/27757 (merged to main as
1c8e8061c2b1, 4.24.0).
_Claude Code on behalf of Claus Ibsen_
> camel-netty-http - unmarshal().gzipDeflater() of a request body fails with
> "Input is not in the .gz format", and a byte-by-byte read of the body ends at
> the first 0xFF byte or throws at its end
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25573
> URL: https://issues.apache.org/jira/browse/CAMEL-25573
> Project: Camel
> Issue Type: Bug
> Components: camel-netty-http
> Reporter: shashank
> Assignee: shashank
> Priority: Major
> Fix For: 4.24.0
>
>
> The body of a request received by a netty-http consumer is a
> {{NettyChannelBufferStreamCache}}
> ({{DefaultNettyHttpBinding.toCamelMessage}}, lines 119-123 of main
> 2a0a2b54f4ba; not with {{disableStreamCache=true}} or {{httpProxy=true}}).
> The class is a {{java.io.InputStream}}, and a route step that asks for the
> body as an {{InputStream}} gets this object. Its {{read()}} does not keep the
> {{InputStream}} contract. With the default configuration:
> # Loud: {{from("netty-http:...").unmarshal().gzipDeflater()}} does not work.
> Every request fails with HTTP 500 and {{java.io.IOException: Input is not in
> the .gz format.}} in the log. The same holds for any reader that reads the
> first bytes one by one (to sniff a header, for example): it gets wrong values
> for bytes from 0x80, or an {{IndexOutOfBoundsException}} at the end of the
> body.
> # Silent, and narrower: user code or a library that reads the body byte by
> byte ({{while ((b = in.read()) != -1)}}) gets only the bytes before the first
> 0xFF, without any error, and negative values for bytes from 0x80. A byte 0xFF
> is common in binary payloads and never occurs in UTF-8 text, so text bodies
> are not cut; for them the loop ends with the {{IndexOutOfBoundsException}}
> (loud).
> Not affected: everything that reads in bulk. The type converters of Camel to
> {{byte[]}} and {{String}} ({{StreamCacheConverter.convertToByteArray}} calls
> {{writeTo}}) and {{NettyHttpConverter.toByteBuf}} read with {{read(byte[],
> int, int)}}, which is why most routes do not see this. Silent loss is not the
> general case.
> The causes in the class:
> * {{read()}} (line 56) is {{return buffer.readByte();}}.
> {{ByteBuf.readByte()}} returns a Java {{byte}} (-128 to 127) and throws
> {{IndexOutOfBoundsException}} when nothing is left. {{InputStream.read()}}
> must return "an int in the range 0 to 255" and -1 at the end of the stream.
> So a byte 0xFF is returned as -1, which every reader takes for the end of the
> stream, bytes 0x80 to 0xFE are returned as negative values, and at the end of
> the body the method throws instead of returning -1.
> * {{markSupported()}} (line 51) returns true, but {{mark(int)}} is not
> overridden (it is the empty method of {{InputStream}}). The gzip reader of
> commons-compress uses a stream with {{markSupported()}} as it is, so it reads
> the magic number with {{read()}} and gets -117 for the byte 0x8b.
> ({{java.util.zip.GZIPInputStream}} fails with
> {{NettyChannelBufferStreamCache.read() returned value out of range -1..255:
> -117}}.)
> * {{read(byte[], int, int)}} (lines 65-78) takes {{buffer.capacity()}} for
> the end of the content; for a buffer whose capacity is larger than its writer
> index it throws {{IndexOutOfBoundsException}} at the end of the content. (The
> aggregated content of a request has no spare capacity, so this one is not
> reached by the consumer today.)
> h3. Reproduction
> New {{NettyHttpGzipRequestBodyTest}} (4 tests, real consumer and producer)
> and {{NettyChannelBufferStreamCacheTest}} (7 tests on the class). With the
> class of main 10 of the 11 fail:
> * {{gzipDataFormatUnmarshalsTheRequestBody}},
> {{resetReturnsTheWholeRequestBodyAfterTheGzipUnmarshal}},
> {{writeToReturnsTheWholeRequestBodyAfterTheGzipUnmarshal}}:
> {{NettyHttpOperationFailedException ... statusCode: 500}}, in the log of the
> consumer {{java.io.IOException: Input is not in the .gz format.}} (all three
> fail at the unmarshal)
> * {{processorReadsTheRequestBodyByteByByte}} (body {{10 20 FF 30 40}}, the
> route counts the bytes with {{read()}}): {{expected: <5> but was: <2>}}
> * {{readReturnsEveryByteAsUnsignedValue}}: {{expected: <128> but was: <-128>}}
> * {{readLoopCopiesBinaryContent}}: {{array lengths differ, expected: <5> but
> was: <2>}}
> * {{readLoopEndsAtTheEndOfText}}: {{IndexOutOfBounds readerIndex(5) +
> length(1) exceeds writerIndex(5)}}
> * {{markIsNotSupported}}: {{expected: <false> but was: <true>}}
> * {{readStopsAtTheLastWrittenByte}}: {{IndexOutOfBounds readerIndex(0) +
> length(16) exceeds writerIndex(3)}}
> * {{gzipInputStreamReadsItsHeader}}: {{IOException:
> ...NettyChannelBufferStreamCache.read() returned value out of range -1..255:
> -117}}
> {{resetAndWriteToReturnTheWholeBodyAfterAReaderThatMarks}} passes on main too
> (there {{mark}} does nothing either); it guards the fix against a
> {{mark(int)}} that moves the reset position, see below.
> h3. Proposed fix
> * {{read()}} returns {{buffer.readByte() & 0xFF}}, and -1 when the buffer has
> no readable byte.
> * {{markSupported()}} returns false. {{mark(int)}} stays the empty method and
> {{reset()}} is not changed: it goes back to the start of the body, as
> {{StreamCache.reset()}} needs. A reader that needs mark support wraps the
> stream (the gzip reader of commons-compress does that by itself with a
> {{BufferedInputStream}}).
> * {{read(byte[], int, int)}} uses {{buffer.readableBytes()}}.
> Why not implement {{mark(int)}}: {{reset()}}, {{writeTo()}} (used by the
> converters to {{byte[]}} and {{String}}) and {{NettyHttpConverter.toByteBuf}}
> all go back to the marked reader index of the buffer, which today is always
> the start because only the constructor marks. If {{mark(int)}} moved it, a
> reader that marks in the middle (the gzip reader marks before every buffer of
> 8192 bytes) would leave the stream cache returning only its tail: with that
> variant the tests got 3634 of 20028 bytes from {{reset()}} and {{writeTo()}}
> after the gzip unmarshal. A stream cache without mark support whose
> {{reset()}} goes back to the start exists already: {{FileInputStreamCache}}
> (line 59, no {{markSupported}} or {{mark}} override; {{reset()}} at line 89
> drops the open stream, so the next read starts at the beginning of the file).
> The in-memory stream caches of camel-support differ: they are, or delegate
> to, a {{ByteArrayInputStream}}, which supports mark.
> Behaviour change: code that reads the body byte by byte now gets values from
> 0 to 255 and -1 at the end, and {{markSupported()}} is false. An upgrade note
> for 4.24 is included (the fix creates {{camel-4x-upgrade-guide-4_24.adoc}}).
> In the main sources of Camel the only caller of {{markSupported()}} on a body
> stream that can be this class is {{BlobClientWrapper}} of
> camel-azure-storage-blob (line 137; read, not run). camel-netty-http with the
> fix: 312 tests, 0 failures, 8 skipped (skipped before as well).
> Not run: a byte-by-byte reader of a text body in a route (the exception at
> the end is shown on the class only), a dead letter channel with
> {{useOriginalMessage}}.
> Related, and independent of this ticket: the gzip data format of
> camel-zip-deflater moving the mark of the in-memory stream caches of
> camel-support has its own ticket. This fix does not rely on that one.
> Found with a Lean 4 model of {{read()}} and of the copy loop of a reader:
> "the reader gets the whole body" fails for every body with a byte 0xFF (the
> reader ends normally with the bytes before it); a body without one ends with
> the exception in the tested case. For the fixed {{read()}} the copy loop is
> proved to return every body, and main and the fix are proved equal for bytes
> below 0x80. For {{reset()}} the model shows that no sequence of read, mark
> and reset calls of a reader changes where it goes back to (by construction:
> {{mark}} does nothing), and gives the 3634 of 20028 bytes for the variant
> with a marking {{mark(int)}}. Then confirmed with the consumer as above.
> Affected: main; camel-4.22.x, camel-4.18.x and camel-4.14.x have the same
> lines (read, not run).
> Duplicate check (2026-10-12): JIRA text "NettyChannelBufferStreamCache" (5:
> CAMEL-7884, CAMEL-8284, CAMEL-8408, CAMEL-13967, CAMEL-14166, none about
> {{read()}} or {{mark}}), "netty-http markSupported" (none), "not in the .gz
> format" (none), "netty-http gzip" (1, unrelated), "netty-http InputStream
> read negative" (1, unrelated); GitHub pull requests
> "NettyChannelBufferStreamCache": 1 merged (#25247, locks), none open.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)