Skip to content

Commit a4b83b3

Browse files
authored
Prevent double compression when handler has already set content encoding (helidon-io#10887)
1 parent 64cd1a4 commit a4b83b3

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

docs/src/main/asciidoc/se/webserver/webserver.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,10 @@ effectively _negotiating_ the content encoding of the response. If none of the
10391039
advertised encodings is supported by the WebServer, the response is returned
10401040
uncompressed.
10411041
1042+
Handlers can encode the response and set the appropriate header to preempt encoding by the
1043+
WebServer. For instance, if a Handler sets the `Content-Encoding: gzip` header then the
1044+
response will not be additionally compressed.
1045+
10421046
=== Configuring HTTP Encoding
10431047
10441048
HTTP encoding support is discovered automatically by WebServer from the classpath, or it can be customized programmatically.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.helidon.webserver.tests;
17+
18+
import io.helidon.http.HeaderNames;
19+
import io.helidon.http.Method;
20+
import io.helidon.http.Status;
21+
import io.helidon.webclient.http1.Http1Client;
22+
import io.helidon.webserver.http.HttpRouting;
23+
import io.helidon.webserver.http.ServerResponse;
24+
import io.helidon.webserver.http1.Http1Route;
25+
import io.helidon.webserver.testing.junit5.ServerTest;
26+
import io.helidon.webserver.testing.junit5.SetUpRoute;
27+
import org.junit.jupiter.api.Test;
28+
29+
import java.io.ByteArrayInputStream;
30+
import java.io.ByteArrayOutputStream;
31+
import java.io.IOException;
32+
import java.net.HttpURLConnection;
33+
import java.net.URL;
34+
import java.nio.charset.StandardCharsets;
35+
import java.util.zip.GZIPInputStream;
36+
import java.util.zip.GZIPOutputStream;
37+
38+
import static org.hamcrest.CoreMatchers.is;
39+
import static org.hamcrest.MatcherAssert.assertThat;
40+
41+
/**
42+
* Verify that server prevents double compression when the handler has already set
43+
* the Content-Encoding header to gzip.
44+
*/
45+
@ServerTest
46+
class ContentEncodingDoubleEncodingTest {
47+
private static final String RESPONSE_STR = "This is the content in the response";
48+
49+
private final String baseUri;
50+
51+
ContentEncodingDoubleEncodingTest(Http1Client client) {
52+
this.baseUri = client.prototype().baseUri().get().toUri().toString();
53+
}
54+
55+
@SetUpRoute
56+
static void routing(HttpRouting.Builder router) {
57+
router.route(Http1Route.route(Method.GET,"/hello", (req, res) -> {
58+
try {
59+
String output = compress(RESPONSE_STR);
60+
var acceptEncoding = req.headers().value(HeaderNames.ACCEPT_ENCODING);
61+
if (acceptEncoding.isPresent() && acceptEncoding.get().contains("gzip")) {
62+
res.headers().add(HeaderNames.CONTENT_ENCODING, "gzip");
63+
send(res, output, false);
64+
} else {
65+
send(res, output, true);
66+
}
67+
} catch (Exception ex) {
68+
throw ex;
69+
}
70+
}));
71+
}
72+
73+
private static String compress(String input) throws IOException {
74+
ByteArrayOutputStream obj=new ByteArrayOutputStream();
75+
GZIPOutputStream gzip = new GZIPOutputStream(obj);
76+
gzip.write(input.getBytes(StandardCharsets.ISO_8859_1));
77+
gzip.close();
78+
return obj.toString(StandardCharsets.ISO_8859_1);
79+
}
80+
81+
private static void send(ServerResponse res, String output, boolean decompress) throws IOException {
82+
res.status(Status.OK_200);
83+
try (
84+
var outputStream = res.outputStream();
85+
var byteInputStream = new ByteArrayInputStream(output.getBytes(StandardCharsets.ISO_8859_1));
86+
var inputStream = decompress ? new GZIPInputStream(byteInputStream) : byteInputStream) {
87+
inputStream.transferTo(outputStream);
88+
}
89+
}
90+
91+
@Test
92+
void testPreventDoubleEncodingDespiteSettingContentEncodingHeader() throws IOException {
93+
var url = new URL(baseUri + "hello");
94+
var conn = (HttpURLConnection) url.openConnection();
95+
conn.setRequestProperty("Accept-Encoding", "gzip");
96+
assertThat(conn.getResponseCode(), is(200));
97+
var compressedInputStream = conn.getInputStream();
98+
var decompressedResponse = new String(new GZIPInputStream(compressedInputStream).readAllBytes());
99+
assertThat(decompressedResponse, is(RESPONSE_STR));
100+
}
101+
}

webserver/webserver/src/main/java/io/helidon/webserver/http/ServerResponseBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ protected byte[] entityBytes(byte[] configuredEntity, int position, int length)
267267
* @return output stream to write plain data to
268268
*/
269269
protected OutputStream contentEncode(OutputStream outputStream) {
270-
if (contentEncodingContext.contentEncodingEnabled()) {
270+
if (contentEncodingContext.contentEncodingEnabled() && !headers().contains(HeaderNames.CONTENT_ENCODING)) {
271271
ContentEncoder encoder = contentEncodingContext.encoder(requestHeaders);
272272
encoder.headers(headers());
273273

0 commit comments

Comments
 (0)