shashank created CAMEL-25574:
--------------------------------
Summary: 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
{{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)