diff --git a/README.md b/README.md
index 24e06ff..af8750b 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
### Latest versions
-* Latest stable version: `1.3.0`
+* Latest stable version: `1.3.1`
* Now with 100% more virtual threads!
* Prior stable version `0.3.7`
@@ -27,20 +27,20 @@ To add this library to your project, you can include this dependency in your Mav
io.fusionauth
java-http
- 1.3.0
+ 1.3.1
```
If you are using Gradle, you can add this to your build file:
```groovy
-implementation 'io.fusionauth:java-http:1.3.0'
+implementation 'io.fusionauth:java-http:1.3.1'
```
If you are using Savant, you can add this to your build file:
```groovy
-dependency(id: "io.fusionauth:java-http:1.3.0")
+dependency(id: "io.fusionauth:java-http:1.3.1")
```
## Examples Usages:
diff --git a/build.savant b/build.savant
index 8f0013d..06b51d2 100644
--- a/build.savant
+++ b/build.savant
@@ -18,7 +18,7 @@ restifyVersion = "4.2.1"
slf4jVersion = "2.0.17"
testngVersion = "7.11.0"
-project(group: "io.fusionauth", name: "java-http", version: "1.3.0", licenses: ["ApacheV2_0"]) {
+project(group: "io.fusionauth", name: "java-http", version: "1.3.1", licenses: ["ApacheV2_0"]) {
workflow {
fetch {
// Dependency resolution order:
diff --git a/src/main/java/io/fusionauth/http/Cookie.java b/src/main/java/io/fusionauth/http/Cookie.java
index 1fd82c1..8b9cca0 100644
--- a/src/main/java/io/fusionauth/http/Cookie.java
+++ b/src/main/java/io/fusionauth/http/Cookie.java
@@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -245,7 +246,7 @@ public void addAttribute(String name, String value) {
return;
}
- switch (name.toLowerCase()) {
+ switch (name.toLowerCase(Locale.ROOT)) {
case HTTPValues.CookieAttributes.DomainLower:
domain = value;
break;
diff --git a/src/main/java/io/fusionauth/http/io/MultipartStream.java b/src/main/java/io/fusionauth/http/io/MultipartStream.java
index 478c878..283875c 100644
--- a/src/main/java/io/fusionauth/http/io/MultipartStream.java
+++ b/src/main/java/io/fusionauth/http/io/MultipartStream.java
@@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -264,7 +265,7 @@ private void readHeaders(Map headers) throws IOException, P
var nextState = state.next(b);
if (nextState != state) {
switch (state) {
- case HeaderName -> headerName = build.toString().toLowerCase();
+ case HeaderName -> headerName = build.toString().toLowerCase(Locale.ROOT);
case HeaderValue -> headers.put(headerName, HTTPTools.parseHeaderValue(build.toString()));
}
diff --git a/src/main/java/io/fusionauth/http/server/HTTPRequest.java b/src/main/java/io/fusionauth/http/server/HTTPRequest.java
index 3877369..be5651a 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPRequest.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPRequest.java
@@ -164,13 +164,13 @@ public void addCookies(Collection cookies) {
}
public void addHeader(String name, String value) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
headers.computeIfAbsent(name, key -> new ArrayList<>()).add(value);
decodeHeader(name, value);
}
public void addHeaders(String name, String... values) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
headers.computeIfAbsent(name, key -> new ArrayList<>()).addAll(List.of(values));
for (String value : values) {
@@ -179,7 +179,7 @@ public void addHeaders(String name, String... values) {
}
public void addHeaders(String name, Collection values) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
headers.computeIfAbsent(name, key -> new ArrayList<>()).addAll(values);
for (String value : values) {
@@ -254,12 +254,12 @@ public Map getAttributes() {
public String getBaseURL() {
// Setting the wrong value in the X-Forwarded-Proto header seems to be a common issue that causes an exception during URI.create.
// Assuming request.getScheme() is not the problem, and it is related to the proxy configuration.
- String scheme = getScheme().toLowerCase();
+ String scheme = getScheme().toLowerCase(Locale.ROOT);
if (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) {
throw new IllegalArgumentException("The request scheme is invalid. Only http or https are valid schemes. The X-Forwarded-Proto header has a value of [" + getHeader(Headers.XForwardedProto) + "], this is likely an issue in your proxy configuration.");
}
- String serverName = getHost().toLowerCase();
+ String serverName = getHost().toLowerCase(Locale.ROOT);
int serverPort = getBaseURLServerPort();
String uri = scheme + "://" + serverName;
@@ -381,7 +381,7 @@ public String getHeader(String name) {
}
public List getHeaders(String name) {
- return headers.get(name.toLowerCase());
+ return headers.get(name.toLowerCase(Locale.ROOT));
}
public Map> getHeaders() {
@@ -650,11 +650,11 @@ public Object removeAttribute(String name) {
}
public void removeHeader(String name) {
- headers.remove(name.toLowerCase());
+ headers.remove(name.toLowerCase(Locale.ROOT));
}
public void removeHeader(String name, String... values) {
- List actual = headers.get(name.toLowerCase());
+ List actual = headers.get(name.toLowerCase(Locale.ROOT));
if (actual != null) {
actual.removeAll(List.of(values));
}
@@ -671,13 +671,13 @@ public void setAttribute(String name, Object value) {
}
public void setHeader(String name, String value) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
this.headers.put(name, new ArrayList<>(List.of(value)));
decodeHeader(name, value);
}
public void setHeaders(String name, String... values) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
this.headers.put(name, new ArrayList<>(List.of(values)));
for (String value : values) {
@@ -686,7 +686,7 @@ public void setHeaders(String name, String... values) {
}
public void setHeaders(String name, Collection values) {
- name = name.toLowerCase();
+ name = name.toLowerCase(Locale.ROOT);
this.headers.put(name, new ArrayList<>(values));
for (String value : values) {
diff --git a/src/main/java/io/fusionauth/http/server/HTTPResponse.java b/src/main/java/io/fusionauth/http/server/HTTPResponse.java
index 874111f..56bcda3 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPResponse.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPResponse.java
@@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@@ -82,7 +83,7 @@ public void addHeader(String name, String value) {
return;
}
- headers.computeIfAbsent(name.toLowerCase(), key -> new ArrayList<>()).add(value);
+ headers.computeIfAbsent(name.toLowerCase(Locale.ROOT), key -> new ArrayList<>()).add(value);
}
public void clearHeaders() {
@@ -102,7 +103,7 @@ public void close() throws IOException {
}
public boolean containsHeader(String name) {
- String key = name.toLowerCase();
+ String key = name.toLowerCase(Locale.ROOT);
return headers.containsKey(key) && !headers.get(key).isEmpty();
}
@@ -174,12 +175,12 @@ public void setException(Throwable exception) {
}
public String getHeader(String name) {
- String key = name.toLowerCase();
+ String key = name.toLowerCase(Locale.ROOT);
return headers.containsKey(key) && !headers.get(key).isEmpty() ? headers.get(key).getFirst() : null;
}
public List getHeaders(String key) {
- return headers.get(key.toLowerCase());
+ return headers.get(key.toLowerCase(Locale.ROOT));
}
public Map> getHeadersMap() {
@@ -260,7 +261,7 @@ public void removeCookie(String name) {
*/
public void removeHeader(String name) {
if (name != null) {
- headers.remove(name.toLowerCase());
+ headers.remove(name.toLowerCase(Locale.ROOT));
}
}
@@ -304,7 +305,7 @@ public void setHeader(String name, String value) {
return;
}
- headers.put(name.toLowerCase(), new ArrayList<>(List.of(value)));
+ headers.put(name.toLowerCase(Locale.ROOT), new ArrayList<>(List.of(value)));
}
/**
diff --git a/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java b/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java
index 966b9fc..b04c168 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPServerConfiguration.java
@@ -20,6 +20,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -493,7 +494,8 @@ public HTTPServerConfiguration withMaxRequestBodySize(Map maxRe
this.maxRequestBodySize.clear();
// Add back a default to ensure we always have a fallback, can still be modified by the incoming configuration.
this.maxRequestBodySize.put("*", DefaultMaxRequestSizes.get("*"));
- this.maxRequestBodySize.putAll(maxRequestBodySize);
+ // Store lower case keys
+ maxRequestBodySize.forEach((k, v) -> this.maxRequestBodySize.put(k.toLowerCase(Locale.ROOT), v));
return this;
}
diff --git a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java
index 8267a2f..1ae36ff 100644
--- a/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java
+++ b/src/main/java/io/fusionauth/http/server/internal/HTTPWorker.java
@@ -234,8 +234,8 @@ public void run() {
logger.trace("[{}] Closing socket. Client closed the connection. Reason [{}].", Thread.currentThread().threadId(), e.getMessage());
closeSocketOnly(CloseSocketReason.Expected);
} catch (HTTPProcessingException e) {
- // Note that I am only tracing this, because this exception is mostly expected. Use closeSocketOnError so we can attempt to write a response.
- logger.trace("[{}] Closing socket with status [{}]. An unhandled [{}] exception was taken. Reason [{}].", Thread.currentThread().threadId(), e.getStatus(), e.getClass().getSimpleName(), e.getMessage());
+ // These are expected, but are things the client may want to know about. Use closeSocketOnError so we can attempt to write a response.
+ logger.debug("[{}] Closing socket with status [{}]. An unhandled [{}] exception was taken. Reason [{}].", Thread.currentThread().threadId(), e.getStatus(), e.getClass().getSimpleName(), e.getMessage());
closeSocketOnError(response, e.getStatus());
} catch (TooManyBytesToDrainException e) {
// The request handler did not read the entire InputStream, we tried to drain it but there were more bytes remaining than the configured maximum.
diff --git a/src/main/java/io/fusionauth/http/util/HTTPTools.java b/src/main/java/io/fusionauth/http/util/HTTPTools.java
index 3e9625d..d49b328 100644
--- a/src/main/java/io/fusionauth/http/util/HTTPTools.java
+++ b/src/main/java/io/fusionauth/http/util/HTTPTools.java
@@ -26,6 +26,7 @@
import java.util.HexFormat;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
@@ -58,6 +59,7 @@ public static int getMaxRequestBodySize(String contentType, Map
}
// Exact match
+ contentType = contentType.toLowerCase(Locale.ROOT);
Integer maximumSize = maxRequestBodySize.get(contentType);
if (maximumSize != null) {
return maximumSize;
@@ -426,10 +428,10 @@ private static void parseHeaderParameter(char[] chars, int start, int end, Map 128k
- .withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form, 128 * 1024)))
+ // - Use a UC Content-Type to make sure it still works
+ .withConfiguration(config -> config.withMaxRequestBodySize(Map.of(ContentTypes.Form.toUpperCase(Locale.ROOT), 128 * 1024)))
.expectResponse("""
HTTP/1.1 413 \r
connection: close\r
diff --git a/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java b/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java
index e3081b0..50f2e47 100644
--- a/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java
+++ b/src/test/java/io/fusionauth/http/util/HTTPToolsTest.java
@@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import com.inversoft.json.ToString;
@@ -64,6 +65,9 @@ public void getMaxRequestBodySize() {
assertMaxConfiguredSize("text/css", 6, configuration);
assertMaxConfiguredSize("text/html", 7, configuration);
+ // Mixed case
+ assertMaxConfiguredSize("Application/JSON", 3, configuration);
+
// We don't expect this at runtime, but ideally we won't explode. These would be invalid values.
// - Some of these are legit Content-Type headers, but at runtime we will have already parsed the header so we do
// not expect any attributes;
@@ -283,7 +287,7 @@ private String hex(byte[] bytes) {
List result = new ArrayList<>();
for (byte b : bytes) {
- result.add(Integer.toHexString(0xFF & b).toUpperCase());
+ result.add(Integer.toHexString(0xFF & b).toUpperCase(Locale.ROOT));
}
return String.join(" ", result);
}
@@ -291,7 +295,7 @@ private String hex(byte[] bytes) {
private String hex(String s) {
List result = new ArrayList<>();
for (char ch : s.toCharArray()) {
- result.add(Integer.toHexString(ch).toUpperCase());
+ result.add(Integer.toHexString(ch).toUpperCase(Locale.ROOT));
}
return String.join(" ", result);
}