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)

Reply via email to