Skip to content

Commit b7c8320

Browse files
committed
Fixing issue with content-length and compression where the length was being sent back but was incorrect. This now chunks when compression is handled inline and not by the HTTPHandler.
1 parent 45f2730 commit b7c8320

File tree

11 files changed

+173
-68
lines changed

11 files changed

+173
-68
lines changed

Diff for: README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@ To add this library to your project, you can include this dependency in your Mav
1414
<dependency>
1515
<groupId>io.fusionauth</groupId>
1616
<artifactId>java-http</artifactId>
17-
<version>0.4.0-RC.2</version>
17+
<version>0.4.0-RC.3</version>
1818
</dependency>
1919
```
2020

2121
If you are using Gradle, you can add this to your build file:
2222

2323
```groovy
24-
implementation 'io.fusionauth:java-http:0.4.0-RC.2'
24+
implementation 'io.fusionauth:java-http:0.4.0-RC.3'
2525
```
2626

2727
If you are using Savant, you can add this to your build file:
2828

2929
```groovy
30-
dependency(id: "io.fusionauth:java-http:0.4.0-RC.2")
30+
dependency(id: "io.fusionauth:java-http:0.4.0-RC.3")
3131
```
3232

3333
## Examples Usages:

Diff for: build.savant

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jackson5Version = "3.0.1"
1717
restifyVersion = "4.2.1"
1818
testngVersion = "7.10.2"
1919

20-
project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.2", licenses: ["ApacheV2_0"]) {
20+
project(group: "io.fusionauth", name: "java-http", version: "0.4.0-RC.3", licenses: ["ApacheV2_0"]) {
2121
workflow {
2222
fetch {
2323
// Dependency resolution order:

Diff for: java-http.iml

+5-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
</CLASSES>
4444
<JAVADOC />
4545
<SOURCES>
46-
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-databind/2.15.4/jackson-databind-2.15.4-sources.jar!/" />
46+
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-databind/2.15.4/jackson-databind-2.15.4-src.jar!/" />
4747
</SOURCES>
4848
</library>
4949
</orderEntry>
@@ -54,7 +54,7 @@
5454
</CLASSES>
5555
<JAVADOC />
5656
<SOURCES>
57-
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-annotations/2.15.4/jackson-annotations-2.15.4-sources.jar!/" />
57+
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-annotations/2.15.4/jackson-annotations-2.15.4-src.jar!/" />
5858
</SOURCES>
5959
</library>
6060
</orderEntry>
@@ -65,7 +65,7 @@
6565
</CLASSES>
6666
<JAVADOC />
6767
<SOURCES>
68-
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-core/2.15.4/jackson-core-2.15.4-sources.jar!/" />
68+
<root url="jar://$MODULE_DIR$/.savant/cache/com/fasterxml/jackson/core/jackson-core/2.15.4/jackson-core-2.15.4-src.jar!/" />
6969
</SOURCES>
7070
</library>
7171
</orderEntry>
@@ -76,7 +76,7 @@
7676
</CLASSES>
7777
<JAVADOC />
7878
<SOURCES>
79-
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2-sources.jar!/" />
79+
<root url="jar://$MODULE_DIR$/.savant/cache/org/testng/testng/7.10.2/testng-7.10.2-src.jar!/" />
8080
</SOURCES>
8181
</library>
8282
</orderEntry>
@@ -109,7 +109,7 @@
109109
</CLASSES>
110110
<JAVADOC />
111111
<SOURCES>
112-
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1-sources.jar!/" />
112+
<root url="jar://$MODULE_DIR$/.savant/cache/org/webjars/jquery/3.7.1/jquery-3.7.1-src.jar!/" />
113113
</SOURCES>
114114
</library>
115115
</orderEntry>

Diff for: load-tests/self/build.savant

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ project(group: "io.fusionauth", name: "self", version: "0.1.0", licenses: ["Apac
3232

3333
dependencies {
3434
group(name: "compile") {
35-
dependency(id: "io.fusionauth:java-http:0.4.0-RC.2.{integration}")
35+
dependency(id: "io.fusionauth:java-http:0.4.0-RC.3.{integration}")
3636
}
3737
}
3838

Diff for: load-tests/self/src/main/java/io/fusionauth/http/load/LoadHandler.java

+24-9
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,32 @@
2626
public class LoadHandler implements HTTPHandler {
2727
@Override
2828
public void handle(HTTPRequest req, HTTPResponse res) {
29-
try (InputStream is = req.getInputStream()) {
30-
byte[] body = is.readAllBytes();
31-
String result = Base64.getEncoder().encodeToString(body);
29+
if (req.getPath().equals("/text")) {
30+
System.out.println("Text");
3231
res.setStatus(200);
32+
res.setContentType("text/plain");
3333

34-
OutputStream os = res.getOutputStream();
35-
os.write(result.getBytes());
36-
os.flush();
37-
os.close();
38-
} catch (Exception e) {
39-
res.setStatus(500);
34+
try (OutputStream os = res.getOutputStream()) {
35+
System.out.println("Wrote");
36+
os.write("Hello world".getBytes());
37+
os.flush();
38+
} catch (Exception e) {
39+
System.out.println("Failed");
40+
res.setStatus(500);
41+
}
42+
} else {
43+
try (InputStream is = req.getInputStream()) {
44+
byte[] body = is.readAllBytes();
45+
String result = Base64.getEncoder().encodeToString(body);
46+
res.setStatus(200);
47+
48+
try (OutputStream os = res.getOutputStream()) {
49+
os.write(result.getBytes());
50+
os.flush();
51+
}
52+
} catch (Exception e) {
53+
res.setStatus(500);
54+
}
4055
}
4156
}
4257
}

Diff for: pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<modelVersion>4.0.0</modelVersion>
33
<groupId>io.fusionauth</groupId>
44
<artifactId>java-http</artifactId>
5-
<version>0.4.0-RC.2</version>
5+
<version>0.4.0-RC.3</version>
66
<packaging>jar</packaging>
77

88
<name>Java HTTP library (client and server)</name>

Diff for: src/main/java/io/fusionauth/http/server/internal/HTTPServerThread.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public void run() {
113113
clients.add(new ClientInfo(client, runnable, throughput));
114114
} catch (SocketTimeoutException ignore) {
115115
// Completely smother since this is expected with the SO_TIMEOUT setting in the constructor
116-
logger.trace("Nothing accepted. Cleaning up existing connections.");
116+
logger.debug("Nothing accepted. Cleaning up existing connections.");
117117
} catch (SocketException e) {
118118
// This should only happen when the server is shutdown
119119
if (socket.isClosed()) {
@@ -164,7 +164,7 @@ public void run() {
164164
ClientInfo client = iterator.next();
165165
Thread thread = client.thread();
166166
if (!thread.isAlive()) {
167-
logger.info("Thread is dead. Removing.");
167+
logger.debug("Thread is dead. Removing.");
168168
iterator.remove();
169169
continue;
170170
}

Diff for: src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ public void run() {
9595
var request = new HTTPRequest(configuration.getContextPath(), configuration.getMultipartBufferSize(),
9696
listener.getCertificate() != null ? "https" : "http", listener.getPort(), socket.getInetAddress().getHostAddress());
9797

98-
logger.debug("Reading preamble");
9998
var inputStream = new ThroughputInputStream(socket.getInputStream(), throughput);
10099
var bodyBytes = HTTPTools.parseRequestPreamble(inputStream, request, buffers.requestBuffer(), instrumenter, () -> state = State.Read);
101100
var httpInputStream = new HTTPInputStream(configuration, request, inputStream, bodyBytes);

Diff for: src/main/java/io/fusionauth/http/server/io/HTTPOutputStream.java

+25-21
Original file line numberDiff line numberDiff line change
@@ -179,36 +179,40 @@ private void commit(boolean closing) throws IOException {
179179

180180
committed = true;
181181

182-
// +++++++++++ Step 1: Determine the extra headers first and add them +++++++++++
182+
// +++++++++++ Step 1: Determine if there is content and set up the compression, encoding, chunking, and length headers +++++++++++
183183
boolean twoOhFour = response.getStatus() == 204;
184+
boolean gzip = false;
185+
boolean deflate = false;
184186
boolean chunked = false;
185-
if (response.getContentLength() == null && !twoOhFour) { // 204 status is specifically "No Content" so we shouldn't write the transfer-encoding header if the status is 204
186-
response.setHeader(Headers.TransferEncoding, TransferEncodings.Chunked);
187-
chunked = true;
188-
}
189187

190188
// If the output stream is closing, but nothing has been written yet, we can safely set the Content-Length header to 0 to let the client
191189
// know nothing more is coming beyond the preamble
192190
if (closing && !twoOhFour) { // 204 status is specifically "No Content" so we shouldn't write the content-length header if the status is 204
193191
response.setContentLength(0L);
194-
}
195-
196-
boolean gzip = false;
197-
boolean deflate = false;
198-
if (compress && !twoOhFour) { // 204 status is specifically "No Content" so we shouldn't write the content-encoding and vary headers if the status is 204
199-
for (String encoding : acceptEncodings) {
200-
if (encoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
201-
response.setHeader(Headers.ContentEncoding, ContentEncodings.Gzip);
202-
response.setHeader(Headers.Vary, Headers.AcceptEncoding);
203-
gzip = true;
204-
break;
205-
} else if (encoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
206-
response.setHeader(Headers.ContentEncoding, ContentEncodings.Deflate);
207-
response.setHeader(Headers.Vary, Headers.AcceptEncoding);
208-
deflate = true;
209-
break;
192+
} else {
193+
// 204 status is specifically "No Content" so we shouldn't write the content-encoding and vary headers if the status is 204
194+
if (compress && !twoOhFour) {
195+
for (String encoding : acceptEncodings) {
196+
if (encoding.equalsIgnoreCase(ContentEncodings.Gzip)) {
197+
response.setHeader(Headers.ContentEncoding, ContentEncodings.Gzip);
198+
response.setHeader(Headers.Vary, Headers.AcceptEncoding);
199+
response.removeHeader(Headers.ContentLength); // Compression will change the length, so we'll chunk instead
200+
gzip = true;
201+
break;
202+
} else if (encoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
203+
response.setHeader(Headers.ContentEncoding, ContentEncodings.Deflate);
204+
response.setHeader(Headers.Vary, Headers.AcceptEncoding);
205+
response.removeHeader(Headers.ContentLength); // Compression will change the length, so we'll chunk instead
206+
deflate = true;
207+
break;
208+
}
210209
}
211210
}
211+
212+
if (response.getContentLength() == null && !twoOhFour) { // 204 status is specifically "No Content" so we shouldn't write the transfer-encoding header if the status is 204
213+
response.setHeader(Headers.TransferEncoding, TransferEncodings.Chunked);
214+
chunked = true;
215+
}
212216
}
213217

214218
// +++++++++++ Step 2: Write the preamble. This must be first without any other output stream interference +++++++++++

Diff for: src/test/java/io/fusionauth/http/CompressionTest.java

+58-24
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public void compress(String encoding, boolean chunked, String scheme) throws Exc
7272
res.setHeader(Headers.ContentType, "text/plain");
7373
res.setStatus(200);
7474

75+
// Technically, this is ignored anytime compression is used, but we are testing if folks don't set it here
7576
if (!chunked) {
7677
res.setContentLength(Files.size(file));
7778
}
@@ -112,17 +113,14 @@ public void compress(String encoding, boolean chunked, String scheme) throws Exc
112113
}
113114
}
114115

115-
@Test(dataProvider = "compressedChunkedSchemes")
116-
public void compress_onByDefault(String encoding, boolean chunked, String scheme) throws Exception {
116+
@Test
117+
public void compressBadContentLength() throws Exception {
117118
HTTPHandler handler = (req, res) -> {
118-
// Use case, do not call response.setCompress(true)
119+
res.setCompress(true);
120+
res.setContentLength(1_000_000); // Ignored
119121
res.setHeader(Headers.ContentType, "text/plain");
120122
res.setStatus(200);
121123

122-
if (!chunked) {
123-
res.setContentLength(Files.size(file));
124-
}
125-
126124
try (InputStream is = Files.newInputStream(file)) {
127125
OutputStream outputStream = res.getOutputStream();
128126
is.transferTo(outputStream);
@@ -132,27 +130,22 @@ public void compress_onByDefault(String encoding, boolean chunked, String scheme
132130
}
133131
};
134132

135-
CountingInstrumenter instrumenter = new CountingInstrumenter();
136-
try (var client = makeClient(scheme, null); var ignore = makeServer(scheme, handler, instrumenter).start()) {
137-
URI uri = makeURI(scheme, "");
138-
var response = client.send(
139-
HttpRequest.newBuilder().header(Headers.AcceptEncoding, encoding).uri(uri).GET().build(),
140-
r -> BodySubscribers.ofInputStream()
141-
);
142-
143-
var result = new String(
144-
encoding.equals(ContentEncodings.Deflate)
145-
? new InflaterInputStream(response.body()).readAllBytes()
146-
: new GZIPInputStream(response.body()).readAllBytes(), StandardCharsets.UTF_8);
147-
148-
assertEquals(response.headers().firstValue(Headers.ContentEncoding).orElse(null), encoding);
133+
try (var client = makeClient("http", null); var ignore = makeServer("http", handler).withCompressByDefault(false).start()) {
134+
URI uri = makeURI("http", "");
135+
var response = client.send(HttpRequest.newBuilder()
136+
.header(Headers.AcceptEncoding, ContentEncodings.Gzip)
137+
.uri(uri)
138+
.GET()
139+
.build(),
140+
r -> BodySubscribers.ofInputStream());
141+
assertEquals(response.headers().firstValue(Headers.ContentEncoding).orElse(null), ContentEncodings.Gzip);
149142
assertEquals(response.statusCode(), 200);
150-
assertEquals(result, Files.readString(file));
143+
assertEquals(new String(new GZIPInputStream(response.body()).readAllBytes(), StandardCharsets.UTF_8), Files.readString(file));
151144
}
152145
}
153146

154-
@Test(enabled = false, groups = "performance")
155-
public void compress_performance() throws Exception {
147+
@Test(groups = "performance")
148+
public void compressPerformance() throws Exception {
156149
HTTPHandler handler = (req, res) -> {
157150
// Use case, do not call response.setCompress(true)
158151
res.setHeader(Headers.ContentType, "text/plain");
@@ -202,7 +195,46 @@ public void compress_performance() throws Exception {
202195
System.out.println("[compression: " + mode + "][" + formatter.format(counter) + "] requests. Total time [" + (formatter.format(total)) + "] ms, Avg [" + avg + "] ms");
203196
}
204197
}
198+
}
199+
200+
@Test(dataProvider = "compressedChunkedSchemes")
201+
public void compress_onByDefault(String encoding, boolean chunked, String scheme) throws Exception {
202+
HTTPHandler handler = (req, res) -> {
203+
// Use case, do not call response.setCompress(true)
204+
res.setHeader(Headers.ContentType, "text/plain");
205+
res.setStatus(200);
205206

207+
// Technically, this is ignored anytime compression is used, but we are testing if folks don't set it here
208+
if (!chunked) {
209+
res.setContentLength(Files.size(file));
210+
}
211+
212+
try (InputStream is = Files.newInputStream(file)) {
213+
OutputStream outputStream = res.getOutputStream();
214+
is.transferTo(outputStream);
215+
outputStream.close();
216+
} catch (IOException e) {
217+
throw new RuntimeException(e);
218+
}
219+
};
220+
221+
CountingInstrumenter instrumenter = new CountingInstrumenter();
222+
try (var client = makeClient(scheme, null); var ignore = makeServer(scheme, handler, instrumenter).start()) {
223+
URI uri = makeURI(scheme, "");
224+
var response = client.send(
225+
HttpRequest.newBuilder().header(Headers.AcceptEncoding, encoding).uri(uri).GET().build(),
226+
r -> BodySubscribers.ofInputStream()
227+
);
228+
229+
var result = new String(
230+
encoding.equals(ContentEncodings.Deflate)
231+
? new InflaterInputStream(response.body()).readAllBytes()
232+
: new GZIPInputStream(response.body()).readAllBytes(), StandardCharsets.UTF_8);
233+
234+
assertEquals(response.headers().firstValue(Headers.ContentEncoding).orElse(null), encoding);
235+
assertEquals(response.statusCode(), 200);
236+
assertEquals(result, Files.readString(file));
237+
}
206238
}
207239

208240
@DataProvider(name = "compressedChunkedSchemes")
@@ -237,6 +269,7 @@ public void requestedButNotAccepted(boolean chunked, String scheme) throws Excep
237269
res.setHeader(Headers.ContentType, "text/plain");
238270
res.setStatus(200);
239271

272+
// Technically, this is ignored anytime compression is used, but we are testing if folks don't set it here
240273
if (!chunked) {
241274
res.setContentLength(Files.size(file));
242275
}
@@ -273,6 +306,7 @@ public void requestedButNotAccepted_unSupportedEncoding(boolean chunked, String
273306
res.setHeader(Headers.ContentType, "text/plain");
274307
res.setStatus(200);
275308

309+
// Technically, this is ignored anytime compression is used, but we are testing if folks don't set it here
276310
if (!chunked) {
277311
res.setContentLength(Files.size(file));
278312
}

0 commit comments

Comments
 (0)