[ 
https://issues.apache.org/jira/browse/CAMEL-25574?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen updated CAMEL-25574:
--------------------------------
    Fix Version/s: 4.24.0

> camel-zip-deflater - after unmarshal().gzipDeflater() the stream-cached body 
> cannot be read again from its start: a dead letter channel with 
> useOriginalMessage gets only the end of the original body
> ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25574
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25574
>             Project: Camel
>          Issue Type: Bug
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>             Fix For: 4.24.0
>
>
> {{GzipDeflaterDataFormat.unmarshal}} (lines 57-72 of main df85d15a3dcb) gives 
> the stream it gets to {{GzipCompressorInputStream}} of commons-compress. When 
> the body of the message is an {{InputStream}}, that stream is the body itself 
> ({{UnmarshalProcessor}}, lines 90-92), and with stream caching (on by 
> default) the body is an in-memory stream cache: {{InputStreamCache}}, which 
> extends {{ByteArrayInputStream}}, or {{ByteArrayInputStreamCache}}, which 
> delegates {{mark}} and {{reset}} to a {{ByteArrayInputStream}}.
> The gzip reader uses a stream with {{markSupported()}} as it is, and calls 
> {{mark}} before every buffer (8192 bytes) that it reads, to {{reset}} and 
> step back when the compressed data ends inside the buffer. So after the 
> unmarshal the mark of the stream cache is behind the 10 byte gzip header, or 
> at the start of the last buffer of a larger body. {{reset()}} of a 
> {{ByteArrayInputStream}} goes back to the last mark, so 
> {{StreamCache.reset()}} ("Resets the StreamCache for a new stream 
> consumption") no longer goes back to the start of the body.
> Camel resets the stream cache whenever the body is to be read again, and the 
> original message that a unit of work keeps for {{useOriginalMessage}} holds 
> the same stream cache object ({{DefaultUnitOfWork}}, lines 125-136). Effects, 
> with the default configuration:
> * A dead letter channel with {{useOriginalMessage()}}, in a route that fails 
> after {{unmarshal().gzipDeflater()}} (by the same mechanism 
> {{onException(...).useOriginalMessage()}}; not run): the dead letter gets the 
> original body without its first bytes. For a gzip body of 31 bytes it gets 
> the last 21, for one of 20028 bytes the last 3634. It is not a gzip stream 
> any more, and nothing is logged.
> * A route that keeps the compressed body (in a property, a variable or a 
> header) and uses it after the unmarshal, for example to archive it, gets the 
> same.
> Not affected: a body that is a {{byte[]}} or a file (a new stream is opened 
> for each read), and a stream cache that is spooled to disk 
> ({{FileInputStreamCache}} does not support {{mark}}, so the reader wraps it; 
> read in the code, not run).
> h3. Reproduction
> New {{GzipDataFormatBodyReadAgainTest}} (3 tests, plain {{direct}} routes, no 
> option set). With the {{GzipDeflaterDataFormat}} of main all 3 fail (run 
> again with the final test class, 2026-10-12):
> * {{deadLetterChannelGetsTheWholeOriginalBody}} 
> ({{errorHandler(deadLetterChannel("direct:dead").useOriginalMessage())}}, 
> {{unmarshal().gzipDeflater()}}, then an exception; the dead letter route 
> converts the body to {{byte[]}}): {{array lengths differ, expected: <31> but 
> was: <21>}}
> * {{deadLetterChannelGetsTheWholeOriginalBodyOfALargeMessage}}: {{array 
> lengths differ, expected: <20028> but was: <3634>}}
> * {{theCompressedBodyKeptInAPropertyCanBeReadAfterTheUnmarshal}} 
> ({{setProperty("compressed", body())}}, unmarshal, 
> {{setBody(exchangeProperty("compressed"))}}): {{array lengths differ, 
> expected: <31> but was: <21>}}
> h3. Proposed fix
> {{unmarshal}} wraps the stream in a {{BufferedInputStream}} before it gives 
> it to the gzip reader, so the reader marks and resets a stream of its own and 
> the body is only read. For a stream without mark support the reader already 
> did the same wrapping itself; for the in-memory stream caches this adds one 
> copy through a buffer of 8192 bytes.
> The unmarshalled result is the same, so there is no upgrade note. 
> camel-zip-deflater with the fix: 16 tests, 0 failures.
> Not changed: the in-memory stream caches themselves. Any reader that calls 
> {{mark}} in the middle of a stream-cached body moves the position that 
> {{StreamCache.reset()}} goes back to. There is one more place in the main 
> sources of Camel that moves the mark of such a stream, and the proposed fix 
> does not change it: {{IOHelper.copy(InputStream, OutputStream, int, boolean, 
> long)}} ({{IOHelper.java}}, lines 224-227) calls {{mark(0)}} and {{reset()}} 
> on any {{ByteArrayInputStream}} (so on an {{InputStreamCache}}, not on a 
> {{ByteArrayInputStreamCache}}) to get its size, which sets the mark to the 
> position the stream has when the copy starts. For a stream at its start (the 
> usual case, Camel resets the stream cache before each step) nothing changes; 
> for a stream that was already partly read, a later {{reset()}} goes back to 
> that position and not to the start. Its callers with a stream are 
> {{DefaultHttpBinding.copyStream}}, {{UndertowConsumer}} ({{useStreaming}}) 
> and {{CachedOutputStream}} (a new stream); a case where one of them gets a 
> partly read body was not run. Whether {{reset()}} of the stream caches should 
> always go to the start is a question for a separate ticket.
> Related, and independent of this ticket: the read contract of the request 
> body stream cache of camel-netty-http ({{NettyChannelBufferStreamCache}}) has 
> its own ticket. With the fix proposed there that stream cache does not 
> support {{mark}}, so the gzip reader wraps it by itself and neither fix needs 
> the other.
> Found with a Lean 4 model of the stream cache (length, position, mark) and of 
> the buffer loop of the gzip reader: the model shows that on main a 
> {{reset()}} after the unmarshal never goes back before the end of the gzip 
> header, for every size of body, so only the tail is returned (21 of 31 and 
> 3634 of 20028 bytes in the two tested cases). The fix is modelled as not 
> marking the body at all, so for the fix the model says nothing beyond that; 
> the tests are the evidence for it. Then confirmed with the data format as 
> above.
> Affected: main; camel-4.22.x, camel-4.18.x and camel-4.14.x use the same 
> reader on the body (read, not run). Since 3.12.0 (CAMEL-16953).
> Duplicate check (2026-10-12): JIRA text "gzip useOriginalMessage" (1, 
> unrelated), "gzipDeflater original message" (none), 
> "GzipCompressorInputStream" (1: CAMEL-16953, the change that introduced the 
> reader), "stream cache mark reset" (8, none about a reader moving the mark: 
> CAMEL-5008, CAMEL-12078, CAMEL-16252 about azure-storage-blob needing mark 
> support, CAMEL-25167, CAMEL-5940, CAMEL-24159, CAMEL-12955, CAMEL-12297), 
> "gzip unmarshal stream cache truncated" (none); GitHub pull requests 
> "gzipDeflater" (2 merged: #26619 YAML validation, #6864 docs), 
> "GzipDeflaterDataFormat" (none), "zip-deflater" in the title (1 merged: 
> #23642 deprecated methods), none open.
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to