shashank created CAMEL-25571:
--------------------------------
Summary: camel-servlet, camel-jetty - a request body with
Content-Encoding: gzip is passed to the route compressed when it arrives after
the headers (Expect: 100-continue, or a later packet)
Key: CAMEL-25571
URL: https://issues.apache.org/jira/browse/CAMEL-25571
Project: Camel
Issue Type: Bug
Components: camel-http-common
Reporter: shashank
Assignee: shashank
{{HttpConverter.toInputStream(HttpServletRequest, Exchange)}}
(camel-http-common, lines 81-98 at main 5987a263107b) reads the request body
for the consumers that use {{DefaultHttpBinding.parseBody}} (line 715, through
{{HttpHelper.readRequestBodyFromServletRequest}}, line 163): camel-servlet and
camel-jetty.
{code:java}
InputStream is = request.getInputStream();
if (is != null && is.available() <= 0) {
// there is no data, so we cannot uncompress etc.
return is;
}
if (exchange == null || !exchange.getProperty(Exchange.SKIP_GZIP_ENCODING,
Boolean.FALSE, Boolean.class)) {
String contentEncoding = request.getHeader(Exchange.CONTENT_ENCODING);
return GZIPHelper.uncompressGzip(contentEncoding, is);
}
{code}
{{available()}} is the number of bytes that can be read without blocking. It is
0 for an empty body, but also while the body is still on its way:
* a client that sends {{Expect: 100-continue}} waits for the interim response
of the server before it sends the body, so no byte of the body can be there
when the consumer starts to read it;
* a client can send the body in a later packet than the headers (a slow or
chunked upload).
The route then gets the compressed bytes of a request with {{Content-Encoding:
gzip}}, without an error. The same request is uncompressed when its first bytes
happen to have arrived, so the result depends on timing.
h3. History
The check was added by CAMEL-5806 (commit 1ba5aa2f4b, 2015-02-16, 2.15.0: GET
with a body), in {{HttpConverter}} and, as {{available() == 0}}, in
{{DefaultHttpBinding.parseBody}}. CAMEL-8643 (2.15.3 / 2.16.0, commit
7790c6e0fe) reported the check in {{DefaultHttpBinding}} for the same reason
("on a slow or remote client the binary stream isn't always immediately
available") and made it opt-in: {{eagerCheckContentAvailable}}, default false.
That commit did not change {{HttpConverter}}, where the check is still
unconditional. (CAMEL-1327 added the gzip support in 2009, without the check.)
h3. Scope
* Through {{HttpConverter}}: camel-servlet and camel-jetty, and what extends
them (camel-atmosphere-websocket). In camel-spring-boot,
{{SpringBootPlatformHttpBinding}} extends {{DefaultHttpBinding}} and calls
{{super.parseBody}} when the platform-http endpoint has {{useStreaming=true}}
(read on camel-spring-boot main, not run).
* Not through it: camel-platform-http-vertx ({{VertxPlatformHttpConsumer}},
Vert.x body handlers), camel-undertow
({{DefaultUndertowHttpBinding.readFromChannel}}), camel-netty-http
({{DefaultNettyHttpBinding}}); none of them depends on camel-http-common in
main scope.
h3. Reproduction
New {{JettyGzipRequestExpectContinueTest}} (camel-jetty): a socket client sends
{code}
POST /gzip HTTP/1.1
Content-Type: text/plain; charset=UTF-8
Content-Encoding: gzip
Content-Length: <n>
Expect: 100-continue
{code}
waits until it has read {{HTTP/1.1 100 Continue}}, then sends the gzip body.
The route is a jetty consumer followed by {{convertBodyTo(String.class,
"UTF-8")}} and {{to("mock:result")}}. On main the mock gets the compressed
bytes: {{mock://result Body of message: 0. Expected: <Hello World, sent with
Content-Encoding: gzip> but was: <...>}} (in all 3 surefire runs: the first run
and its 2 reruns). The test has no sleep: the client only sends the body after
the interim response.
New {{HttpConverterGzipTest}} (camel-http-common) calls the converter with a
servlet stream whose {{available()}} is 0 and a gzip body; on main: {{expected:
<Hello World> but was: <...>}} (the compressed bytes), with and without an
exchange.
The existing gzip request test of camel-jetty that is enabled,
{{JettyContentTypeTest.testContentTypeWithGZipEncoding}} (camel-http producer),
does not show the problem (it passes with the fix; not run separately on main);
{{HttpGZipEncodingTest}} is {{@Disabled}}.
h3. Proposed fix
For a request with gzip content encoding (and without the
{{CamelSkipGzipEncoding}} exchange property) the converter reads the first byte
through a {{PushbackInputStream}}: at the end of the stream the body is empty
and is passed on as it is, otherwise the byte is pushed back and the stream is
wrapped in a {{GZIPInputStream}}. Requests without gzip content encoding, and
exchanges with {{CamelSkipGzipEncoding}}, are not read in the converter, as
before.
Behaviour changes (all in the 4.24 upgrade guide):
* a gzip request body that arrives after the headers is now uncompressed;
* a body that is labelled gzip but is not gzip data already failed with
{{ZipException}} in the converter when its bytes had arrived; when it arrived
late, main passed the bytes to the route as they were (200), and now it fails
in the same way (Jetty answers 500, the route is not called);
* with {{disableStreamCache=true}} the converter now waits for the first byte
of a gzip body before the route is called, where main returned at once when
nothing had arrived (with stream caching the consumer already read the whole
body). A request without a body does not wait: {{Content-Length: 0}} returns in
{{DefaultHttpBinding.parseBody}} (line 699) before the converter, and a chunked
request without data gives {{read() == -1}}.
Tests with the fix: {{HttpConverterGzipTest}} 9 cases, of which 3 fail on main
(the two late gzip bodies, and the late body that is not gzip) and 6 pass on
main too (available body, empty body, {{CamelSkipGzipEncoding}} late and
available, no gzip header, a body that is not gzip and available);
{{JettyGzipRequestExpectContinueTest}} 2 cases, both fail on main (the second,
{{Expected 500 but was: HTTP/1.1 200 OK}}, pins the changed behaviour for a
body that is not gzip); {{JettyGzipRequestEmptyBodyTest}} 2 cases with
{{disableStreamCache=true}}, both pass on main too.
Suites with the fix: camel-http-common 48 tests, 0 failures; camel-servlet 110
tests, 0 failures, 2 skipped; camel-jetty 403 tests, 1 failure and 1 error, 23
skipped: {{JettyEmptyCharsetParameterTest}} and {{JettyXsltHttpTemplateTest}},
which fail in the same way with unchanged main code on the same machine.
Found with a small Lean 4 model of the converter (request = gzip flag, skip
flag, body, bytes arrived): "a gzip body is uncompressed unless it is empty or
skipped" fails on main for every non-empty gzip body of which nothing has
arrived; the fix is proved to meet it for every request and to equal main
whenever a byte has arrived or the body is empty (the model has no body that is
labelled gzip but is not gzip). Then confirmed with the Jetty consumer as above.
Affected: main; camel-4.22.x, camel-4.18.x and camel-4.14.x have the same
{{available() <= 0}} check (read, not run).
Duplicate check (2026-10-12): JIRA text "gzip available servlet" (none),
"HttpConverter gzip" (1: CAMEL-1327, which added the gzip support in 2009);
CAMEL-5806 and CAMEL-8643 are both Resolved/Fixed and about
{{DefaultHttpBinding}}, see History, "Content-Encoding gzip jetty consumer" (5,
none about the request body), "gzip 100-continue" (none), "GZIPInputStream
EOFException servlet" (none); GitHub pull requests "gzip HttpConverter": none;
no open pull request touches {{HttpConverter}}.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)