Skip to content

Commit 2a66356

Browse files
committed
Update headers to only allocate names when needed
1 parent 18c0a0e commit 2a66356

File tree

3 files changed

+72
-18
lines changed

3 files changed

+72
-18
lines changed

http/http-api/src/main/java/software/amazon/smithy/java/http/api/HttpHeaders.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
/**
1212
* Contains case-insensitive HTTP headers.
13+
*
14+
* <p>Implementations must always normalize header names to lowercase.
1315
*/
1416
public interface HttpHeaders extends Iterable<Map.Entry<String, List<String>>> {
1517

@@ -109,4 +111,64 @@ default boolean isEmpty() {
109111
* @return the created modifiable headers.
110112
*/
111113
ModifiableHttpHeaders toModifiable();
114+
115+
/**
116+
* Normalizes an HTTP header name by trimming whitespace and converting ASCII uppercase to lowercase.
117+
*
118+
* <p>Trimming behavior matches {@link String#trim()}, removing characters {@code <= 'u0020'}.
119+
* Only ASCII uppercase letters (A-Z) are lowercased; non-ASCII characters pass through unchanged,
120+
* which is correct per RFC 7230 (HTTP/1.1) and RFC 9110 (HTTP semantics) since header field names
121+
* are defined as ASCII tokens.
122+
*
123+
* @param name the header name to normalize
124+
* @return the normalized header name, or the original instance if already normalized
125+
*/
126+
static String normalizeHeaderName(String name) {
127+
int len = name.length();
128+
int start = 0;
129+
int end = len - 1;
130+
boolean needsWork = false;
131+
132+
// Detect leading whitespace to trim if needed
133+
while (start <= end && name.charAt(start) <= ' ') {
134+
needsWork = true;
135+
start++;
136+
}
137+
138+
// Detect trailing whitespace to trim if needed
139+
while (end >= start && name.charAt(end) <= ' ') {
140+
needsWork = true;
141+
end--;
142+
}
143+
144+
// All whitespace
145+
if (start > end) {
146+
return "";
147+
}
148+
149+
// Scan for ASCII uppercase
150+
for (int i = start; i <= end; i++) {
151+
char c = name.charAt(i);
152+
if (c >= 'A' && c <= 'Z') {
153+
needsWork = true;
154+
break;
155+
}
156+
}
157+
158+
if (!needsWork) {
159+
return name;
160+
}
161+
162+
int outLen = end - start + 1;
163+
char[] chars = new char[outLen];
164+
for (int src = start, dst = 0; src <= end; src++, dst++) {
165+
char c = name.charAt(src);
166+
if (c >= 'A' && c <= 'Z') {
167+
c = (char) (c + 32);
168+
}
169+
chars[dst] = c;
170+
}
171+
172+
return new String(chars);
173+
}
112174
}

http/http-api/src/main/java/software/amazon/smithy/java/http/api/SimpleModifiableHttpHeaders.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public void addHeader(String name, List<String> values) {
3636
}
3737

3838
private List<String> getOrCreateValues(String name) {
39-
var key = formatPutKey(name);
39+
var key = HttpHeaders.normalizeHeaderName(name);
4040
var values = headers.get(key);
4141
if (values == null) {
4242
values = new ArrayList<>();
@@ -47,8 +47,8 @@ private List<String> getOrCreateValues(String name) {
4747

4848
@Override
4949
public void setHeader(String name, String value) {
50-
var key = formatPutKey(name);
51-
var list = headers.get(name);
50+
var key = HttpHeaders.normalizeHeaderName(name);
51+
var list = headers.get(key);
5252
if (list == null) {
5353
list = new ArrayList<>(1);
5454
headers.put(key, list);
@@ -61,8 +61,8 @@ public void setHeader(String name, String value) {
6161

6262
@Override
6363
public void setHeader(String name, List<String> values) {
64-
var key = formatPutKey(name);
65-
var list = headers.get(name);
64+
var key = HttpHeaders.normalizeHeaderName(name);
65+
var list = headers.get(key);
6666
if (list == null) {
6767
list = new ArrayList<>(values.size());
6868
headers.put(key, list);
@@ -77,10 +77,6 @@ public void setHeader(String name, List<String> values) {
7777
}
7878
}
7979

80-
private static String formatPutKey(String name) {
81-
return name.trim().toLowerCase(Locale.ENGLISH);
82-
}
83-
8480
@Override
8581
public void removeHeader(String name) {
8682
headers.remove(name.toLowerCase(Locale.ENGLISH));

http/http-api/src/main/java/software/amazon/smithy/java/http/api/SimpleUnmodifiableHttpHeaders.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class SimpleUnmodifiableHttpHeaders implements HttpHeaders {
3232
// Single pass to normalize, trim, and make immutable in one go
3333
Map<String, List<String>> result = HashMap.newHashMap(input.size());
3434
for (var entry : input.entrySet()) {
35-
var key = normalizeKey(entry.getKey());
35+
var key = HttpHeaders.normalizeHeaderName(entry.getKey());
3636
var values = entry.getValue();
3737
var existing = result.get(key);
3838
if (existing == null) {
@@ -49,10 +49,6 @@ final class SimpleUnmodifiableHttpHeaders implements HttpHeaders {
4949
}
5050
}
5151

52-
private static String normalizeKey(String key) {
53-
return key.trim().toLowerCase(Locale.ENGLISH);
54-
}
55-
5652
private static List<String> copyAndTrimValuesMutable(List<String> source) {
5753
int size = source.size();
5854
if (size == 0) {
@@ -145,7 +141,7 @@ static Map<String, List<String>> addHeaders(
145141
mutatedHeaders = copyHeaders(original.map());
146142
}
147143
for (var entry : from.entrySet()) {
148-
var key = normalizeKey(entry.getKey());
144+
var key = HttpHeaders.normalizeHeaderName(entry.getKey());
149145
var list = mutatedHeaders.get(key);
150146
if (list == null) {
151147
list = copyAndTrimValuesMutable(entry.getValue());
@@ -166,7 +162,7 @@ static Map<String, List<String>> addHeader(
166162
if (mutatedHeaders == null) {
167163
mutatedHeaders = copyHeaders(original.map());
168164
}
169-
field = normalizeKey(field);
165+
field = HttpHeaders.normalizeHeaderName(field);
170166
value = value.trim();
171167
var list = mutatedHeaders.get(field);
172168
if (list == null) {
@@ -183,7 +179,7 @@ static Map<String, List<String>> copyHeaders(Map<String, List<String>> from) {
183179
}
184180
Map<String, List<String>> into = HashMap.newHashMap(from.size());
185181
for (var entry : from.entrySet()) {
186-
into.put(normalizeKey(entry.getKey()), copyAndTrimValuesMutable(entry.getValue()));
182+
into.put(HttpHeaders.normalizeHeaderName(entry.getKey()), copyAndTrimValuesMutable(entry.getValue()));
187183
}
188184
return into;
189185
}
@@ -197,7 +193,7 @@ static Map<String, List<String>> replaceHeaders(
197193
mutated = copyHeaders(original.map());
198194
}
199195
for (var entry : replace.entrySet()) {
200-
mutated.put(normalizeKey(entry.getKey()), copyAndTrimValuesMutable(entry.getValue()));
196+
mutated.put(HttpHeaders.normalizeHeaderName(entry.getKey()), copyAndTrimValuesMutable(entry.getValue()));
201197
}
202198
return mutated;
203199
}

0 commit comments

Comments
 (0)