Skip to content

Commit 1f638a8

Browse files
authored
Merge pull request #47 from FusionAuth/degroff/content_encoding
Add support for Content-Encoding on the HTTP request
2 parents cac427d + 2deed26 commit 1f638a8

18 files changed

+635
-239
lines changed

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
## FusionAuth HTTP client and server ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![test](https://github.com/FusionAuth/java-http/actions/workflows/test.yml/badge.svg)](https://github.com/FusionAuth/java-http/actions/workflows/test.yml)
1+
## Java HTTP client and server ![semver 2.0.0 compliant](http://img.shields.io/badge/semver-2.0.0-brightgreen.svg?style=flat-square) [![test](https://github.com/FusionAuth/java-http/actions/workflows/test.yml/badge.svg)](https://github.com/FusionAuth/java-http/actions/workflows/test.yml)
22

33
### Latest versions
44

5-
* Latest stable version: `1.3.1`
5+
* Latest stable version: `1.4.0`
66
* Now with 100% more virtual threads!
77
* Prior stable version `0.3.7`
88

@@ -27,20 +27,20 @@ To add this library to your project, you can include this dependency in your Mav
2727
<dependency>
2828
<groupId>io.fusionauth</groupId>
2929
<artifactId>java-http</artifactId>
30-
<version>1.3.1</version>
30+
<version>1.4.0</version>
3131
</dependency>
3232
```
3333

3434
If you are using Gradle, you can add this to your build file:
3535

3636
```groovy
37-
implementation 'io.fusionauth:java-http:1.3.1'
37+
implementation 'io.fusionauth:java-http:1.4.0'
3838
```
3939

4040
If you are using Savant, you can add this to your build file:
4141

4242
```groovy
43-
dependency(id: "io.fusionauth:java-http:1.3.1")
43+
dependency(id: "io.fusionauth:java-http:1.4.0")
4444
```
4545

4646
## Examples Usages:
@@ -231,14 +231,13 @@ The general requirements and roadmap are as follows:
231231
### Server tasks
232232

233233
* [x] Basic HTTP 1.1
234+
* [x] Support Accept-Encoding (gzip, deflate), by default and per response options.
235+
* [x] Support Content-Encoding (gzip, deflate)
234236
* [x] Support Keep-Alive
235237
* [x] Support Expect-Continue 100
236-
* [x] Support chunked request
237-
* [x] Support chunked response
238-
* [x] Support streaming entity bodies (via chunking likely)
239-
* [x] Support compression (default and per response options)
238+
* [x] Support Transfer-Encoding: chunked on request for streaming.
239+
* [x] Support Transfer-Encoding: chunked on response
240240
* [x] Support cookies in request and response
241-
* [x] Clean up HTTPRequest
242241
* [x] Support form data
243242
* [x] Support multipart form data
244243
* [x] Support TLS

build.savant

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ restifyVersion = "4.2.1"
1818
slf4jVersion = "2.0.17"
1919
testngVersion = "7.11.0"
2020

21-
project(group: "io.fusionauth", name: "java-http", version: "1.3.1", licenses: ["ApacheV2_0"]) {
21+
project(group: "io.fusionauth", name: "java-http", version: "1.4.0", licenses: ["ApacheV2_0"]) {
2222
workflow {
2323
fetch {
2424
// Dependency resolution order:

pom.xml

Lines changed: 2 additions & 2 deletions
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>1.3.1</version>
5+
<version>1.4.0</version>
66
<packaging>jar</packaging>
77

88
<name>Java HTTP library (client and server)</name>
@@ -200,4 +200,4 @@
200200
</build>
201201
</profile>
202202
</profiles>
203-
</project>
203+
</project>

src/main/java/io/fusionauth/http/HTTPValues.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public static final class ContentEncodings {
5151

5252
public static final String Gzip = "gzip";
5353

54+
public static final String XGzip = "x-gzip";
55+
5456
private ContentEncodings() {
5557
}
5658
}
@@ -216,6 +218,8 @@ public static final class Headers {
216218

217219
public static final String ContentEncoding = "Content-Encoding";
218220

221+
public static final String ContentEncodingLower = "content-encoding";
222+
219223
public static final String ContentLength = "Content-Length";
220224

221225
public static final String ContentLengthLower = "content-length";
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2025, FusionAuth, All Rights Reserved
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,
11+
* software distributed under the License is distributed on an
12+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific
14+
* language governing permissions and limitations under the License.
15+
*/
16+
package io.fusionauth.http.io;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
21+
/**
22+
* A filter InputStream that reads a fixed length body.
23+
*
24+
* @author Daniel DeGroff
25+
*/
26+
public class FixedLengthInputStream extends InputStream {
27+
private final byte[] b1 = new byte[1];
28+
29+
private final PushbackInputStream delegate;
30+
31+
private long bytesRemaining;
32+
33+
public FixedLengthInputStream(PushbackInputStream delegate, long contentLength) {
34+
this.delegate = delegate;
35+
this.bytesRemaining = contentLength;
36+
}
37+
38+
@Override
39+
public int read(byte[] b, int off, int len) throws IOException {
40+
if (bytesRemaining <= 0) {
41+
return -1;
42+
}
43+
44+
int read = delegate.read(b, off, len);
45+
int reportBytesRead = read;
46+
if (read > 0) {
47+
int extraBytes = (int) (read - bytesRemaining);
48+
if (extraBytes > 0) {
49+
reportBytesRead -= extraBytes;
50+
delegate.push(b, (int) bytesRemaining, extraBytes);
51+
}
52+
53+
bytesRemaining -= reportBytesRead;
54+
}
55+
56+
return reportBytesRead;
57+
}
58+
59+
@Override
60+
public int read() throws IOException {
61+
var read = read(b1);
62+
if (read <= 0) {
63+
return read;
64+
}
65+
66+
return b1[0] & 0xFF;
67+
}
68+
}

src/main/java/io/fusionauth/http/server/Configurable.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ default T withChunkedBufferSize(int chunkedBufferSize) {
6262
/**
6363
* Sets the default compression behavior for the HTTP response. This behavior can be optionally set per response. See
6464
* {@link HTTPResponse#setCompress(boolean)}. Defaults to true.
65+
* <p>
66+
* Set this configuration to <code>true</code> if you want to compress the response when the Accept-Encoding header is present. Set this
67+
* configuration to <code>false</code> if you want to require the request handler to use {@link HTTPResponse#setCompress(boolean)} in
68+
* order to compress the response.
69+
* <p>
70+
* Regardless of this configuration, you always have the option to use {@link HTTPResponse#setCompress(boolean)} on a per-response basis
71+
* as an override.
72+
* <p>
73+
* When the request does not contain an Accept-Encoding the response will not be compressed regardless of this configuration.
6574
*
6675
* @param compressByDefault true if you want to compress by default, or false to not compress by default.
6776
* @return This.

src/main/java/io/fusionauth/http/server/HTTPRequest.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import io.fusionauth.http.FileInfo;
4545
import io.fusionauth.http.HTTPMethod;
4646
import io.fusionauth.http.HTTPValues.Connections;
47+
import io.fusionauth.http.HTTPValues.ContentEncodings;
4748
import io.fusionauth.http.HTTPValues.ContentTypes;
4849
import io.fusionauth.http.HTTPValues.Headers;
4950
import io.fusionauth.http.HTTPValues.Protocols;
@@ -68,6 +69,8 @@ public class HTTPRequest implements Buildable<HTTPRequest> {
6869

6970
private final Map<String, Object> attributes = new HashMap<>();
7071

72+
private final List<String> contentEncodings = new LinkedList<>();
73+
7174
private final Map<String, Cookie> cookies = new HashMap<>();
7275

7376
private final List<FileInfo> files = new LinkedList<>();
@@ -147,6 +150,14 @@ public void addAcceptEncodings(List<String> encodings) {
147150
this.acceptEncodings.addAll(encodings);
148151
}
149152

153+
public void addContentEncoding(String encoding) {
154+
this.contentEncodings.add(encoding);
155+
}
156+
157+
public void addContentEncodings(List<String> encodings) {
158+
this.contentEncodings.addAll(encodings);
159+
}
160+
150161
public void addCookies(Cookie... cookies) {
151162
for (Cookie cookie : cookies) {
152163
this.cookies.put(cookie.name, cookie);
@@ -296,6 +307,15 @@ public void setCharacterEncoding(Charset encoding) {
296307
this.encoding = encoding;
297308
}
298309

310+
public List<String> getContentEncodings() {
311+
return contentEncodings;
312+
}
313+
314+
public void setContentEncodings(List<String> encodings) {
315+
this.contentEncodings.clear();
316+
this.contentEncodings.addAll(encodings);
317+
}
318+
299319
public Long getContentLength() {
300320
return contentLength;
301321
}
@@ -606,12 +626,16 @@ public void setURLParameters(Map<String, List<String>> parameters) {
606626
* {@code Content-Length} header was provided.
607627
*/
608628
public boolean hasBody() {
629+
if (isChunked()) {
630+
return true;
631+
}
632+
609633
Long contentLength = getContentLength();
610-
return isChunked() || (contentLength != null && contentLength > 0);
634+
return contentLength != null && contentLength > 0;
611635
}
612636

613637
public boolean isChunked() {
614-
return getTransferEncoding() != null && getTransferEncoding().equalsIgnoreCase(TransferEncodings.Chunked);
638+
return TransferEncodings.Chunked.equalsIgnoreCase(getTransferEncoding());
615639
}
616640

617641
/**
@@ -756,6 +780,25 @@ private void decodeHeader(String name, String value) {
756780
// Ignore the exception and keep the value null
757781
}
758782
break;
783+
case Headers.ContentEncodingLower:
784+
String[] encodings = value.split(",");
785+
List<String> contentEncodings = new ArrayList<>(1);
786+
for (String encoding : encodings) {
787+
encoding = encoding.trim();
788+
if (encoding.isEmpty()) {
789+
continue;
790+
}
791+
792+
// The HTTP/1.1 standard recommends that the servers supporting gzip also recognize x-gzip as an alias for compatibility.
793+
if (encoding.equalsIgnoreCase(ContentEncodings.XGzip)) {
794+
encoding = ContentEncodings.Gzip;
795+
}
796+
797+
contentEncodings.add(encoding);
798+
}
799+
800+
setContentEncodings(contentEncodings);
801+
break;
759802
case Headers.ContentTypeLower:
760803
this.encoding = null;
761804
this.multipart = false;

src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.fusionauth.http.HTTPProcessingException;
2626
import io.fusionauth.http.HTTPValues;
2727
import io.fusionauth.http.HTTPValues.Connections;
28+
import io.fusionauth.http.HTTPValues.ContentEncodings;
2829
import io.fusionauth.http.HTTPValues.Headers;
2930
import io.fusionauth.http.HTTPValues.Protocols;
3031
import io.fusionauth.http.ParseException;
@@ -417,7 +418,6 @@ private Integer validatePreamble(HTTPRequest request) {
417418
// However, as long as we ignore Content-Length we should be ok. Earlier specs indicate Transfer-Encoding should take precedence,
418419
// later specs imply it is an error. Seems ok to allow it and just ignore it.
419420
if (request.getHeader(Headers.TransferEncoding) == null) {
420-
var contentLength = request.getContentLength();
421421
var requestedContentLengthHeaders = request.getHeaders(Headers.ContentLength);
422422
if (requestedContentLengthHeaders != null) {
423423
if (requestedContentLengthHeaders.size() != 1) {
@@ -429,6 +429,7 @@ private Integer validatePreamble(HTTPRequest request) {
429429
return Status.BadRequest;
430430
}
431431

432+
var contentLength = request.getContentLength();
432433
if (contentLength == null || contentLength < 0) {
433434
if (debugEnabled) {
434435
logger.debug("Invalid request. The Content-Length must be >= 0 and <= 9,223,372,036,854,775,807. [{}]", requestedContentLengthHeaders.getFirst());
@@ -444,6 +445,19 @@ private Integer validatePreamble(HTTPRequest request) {
444445
request.removeHeader(Headers.ContentLength);
445446
}
446447

448+
// Validate Content-Encoding, we currently support deflate and gzip.
449+
// - If we see anything else we should fail, we will be unable to handle the request.
450+
var contentEncodings = request.getContentEncodings();
451+
for (var encoding : contentEncodings) {
452+
if (!encoding.equalsIgnoreCase(ContentEncodings.Gzip) && !encoding.equalsIgnoreCase(ContentEncodings.Deflate)) {
453+
// Note that while we do not expect multiple Content-Encoding headers, the last one will be used. For good measure,
454+
// use the last one in the debug message as well.
455+
var contentEncodingHeader = request.getHeaders(Headers.ContentEncoding).getLast();
456+
logger.debug("Invalid request. The Content-Type header contains an un-supported value. [{}]", contentEncodingHeader);
457+
return Status.UnsupportedMediaType;
458+
}
459+
}
460+
447461
return null;
448462
}
449463

@@ -465,5 +479,7 @@ private static class Status {
465479
public static final int HTTPVersionNotSupported = 505;
466480

467481
public static final int InternalServerError = 500;
482+
483+
public static final int UnsupportedMediaType = 415;
468484
}
469485
}

0 commit comments

Comments
 (0)