Skip to content

Commit f114422

Browse files
authored
feat(gax): add getSingleHeader to HttpHeadersUtils (#14137)
The GAX HTTP/JSON stack stores response headers in a Map<String, Object> where values may be Strings, numbers, Iterables, etc., and header names may vary in casing depending on the HTTP version. This method provides case-insensitive lookup and safely extracts a single value in a typesafe way when it exists, returning null if not and throwing an exception if multiple values exist. This will be used in resumable upload support to retrieve protocol header values (e.g. X-Goog-Upload-URL, Location, and X-Goog-Upload-Status).
1 parent 1b28f13 commit f114422

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

‎sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpHeadersUtils.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.api.client.util.FieldInfo;
3535
import com.google.api.core.InternalApi;
3636
import com.google.common.collect.ImmutableList;
37+
import com.google.common.collect.Iterables;
3738
import java.util.List;
3839
import java.util.Map;
3940
import org.jspecify.annotations.NullMarked;
@@ -52,6 +53,34 @@ public class HttpHeadersUtils {
5253
return null;
5354
}
5455

56+
/**
57+
* Retrieves the single string value of a header by case-insensitive name from a headers map.
58+
*
59+
* @param headers the map of header names to header values
60+
* @param name the case-insensitive header name to look up
61+
* @return the single string value of the header, or {@code null} if not found
62+
* @throws IllegalArgumentException if multiple values are present for the header
63+
*/
64+
public static @Nullable String getSingleHeader(Map<String, Object> headers, String name) {
65+
return headers.entrySet().stream()
66+
.filter(entry -> name.equalsIgnoreCase(entry.getKey()))
67+
.findFirst()
68+
.map(entry -> extractSingleString(entry.getValue()))
69+
.orElse(null);
70+
}
71+
72+
private static @Nullable String extractSingleString(@Nullable Object headerValue) {
73+
if (headerValue == null) {
74+
return null;
75+
}
76+
// HttpHeaders stores single-value headers as single-element Lists
77+
if (headerValue instanceof Iterable) {
78+
Object onlyElement = Iterables.getOnlyElement((Iterable<?>) headerValue, null);
79+
return onlyElement != null ? onlyElement.toString() : null;
80+
}
81+
return headerValue.toString();
82+
}
83+
5584
public static HttpHeaders setHeaders(HttpHeaders headers, Map<String, String> headersMap) {
5685
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
5786
setHeader(headers, entry.getKey(), entry.getValue());

‎sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpHeadersUtilsTest.java‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@
2929
*/
3030
package com.google.api.gax.httpjson;
3131

32+
import static com.google.common.truth.Truth.assertThat;
3233
import static org.junit.jupiter.api.Assertions.assertEquals;
3334
import static org.junit.jupiter.api.Assertions.assertNull;
35+
import static org.junit.jupiter.api.Assertions.assertThrows;
3436

3537
import com.google.api.client.http.HttpHeaders;
3638
import com.google.common.collect.ImmutableList;
3739
import com.google.common.collect.ImmutableMap;
40+
import java.util.Collections;
41+
import java.util.HashMap;
3842
import java.util.Map;
3943
import org.junit.jupiter.api.Test;
4044

@@ -130,4 +134,56 @@ void testGetUserAgentValue() {
130134
headersMap = ImmutableMap.of("Custom-Header", "CustomHeader", "accept", "Accept");
131135
assertNull(HttpHeadersUtils.getUserAgentValue(headersMap));
132136
}
137+
138+
@Test
139+
void getSingleHeader_success() {
140+
Map<String, Object> headers =
141+
ImmutableMap.of(
142+
"X-Goog-Upload-URL",
143+
"https://test.googleapis.com/upload",
144+
"Location",
145+
ImmutableList.of("https://redirect.url"),
146+
"Content-Length",
147+
1024L);
148+
149+
// Case-insensitive & exact lookup
150+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "X-Goog-Upload-URL"))
151+
.isEqualTo("https://test.googleapis.com/upload");
152+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "x-goog-upload-url"))
153+
.isEqualTo("https://test.googleapis.com/upload");
154+
155+
// Single-element iterable
156+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "location"))
157+
.isEqualTo("https://redirect.url");
158+
159+
// Non-string object conversion
160+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "content-length")).isEqualTo("1024");
161+
}
162+
163+
@Test
164+
void getSingleHeader_multipleElements_throwsIllegalArgumentException() {
165+
Map<String, Object> headers =
166+
ImmutableMap.of(
167+
"Location", ImmutableList.of("https://redirect.url", "https://secondary.url"));
168+
169+
assertThrows(
170+
IllegalArgumentException.class,
171+
() -> HttpHeadersUtils.getSingleHeader(headers, "location"));
172+
}
173+
174+
@Test
175+
void getSingleHeader_missingOrEmpty_returnsNull() {
176+
Map<String, Object> headers = new HashMap<>();
177+
headers.put("Existing-Header", "value");
178+
headers.put("Null-Value-Header", null);
179+
headers.put("Empty-List-Header", Collections.emptyList());
180+
headers.put("Null-First-Header", Collections.singletonList(null));
181+
headers.put(null, "some-value");
182+
183+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "non-existent-header")).isNull();
184+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "null-value-header")).isNull();
185+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "empty-list-header")).isNull();
186+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "null-first-header")).isNull();
187+
assertThat(HttpHeadersUtils.getSingleHeader(headers, "any-header")).isNull();
188+
}
133189
}

0 commit comments

Comments
 (0)