Skip to content

Commit 1ebd6f3

Browse files
feat: add context support for case insensitive and multivalue (#39)
* feat: add context support for case insensitive and multivalue * refactor: update usages from deprecated versions
1 parent c9d8b57 commit 1ebd6f3

File tree

9 files changed

+213
-48
lines changed

9 files changed

+213
-48
lines changed

grpc-client-rx-utils/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
dependencies {
9-
api(platform("io.grpc:grpc-bom:1.45.1"))
9+
api(platform("io.grpc:grpc-bom:1.50.0"))
1010
api("io.reactivex.rxjava3:rxjava:3.1.4")
1111
api("io.grpc:grpc-stub")
1212
api(project(":grpc-context-utils"))

grpc-client-utils/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77

88
dependencies {
99

10-
api(platform("io.grpc:grpc-bom:1.47.0"))
10+
api(platform("io.grpc:grpc-bom:1.50.0"))
1111
api("io.grpc:grpc-context")
1212
api("io.grpc:grpc-api")
1313
api(platform("io.netty:netty-bom:4.1.79.Final")) {

grpc-client-utils/src/main/java/org/hypertrace/core/grpcutils/client/RequestContextAsCreds.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import io.grpc.CallCredentials;
77
import io.grpc.Metadata;
88
import io.grpc.Status;
9-
import java.util.Map;
109
import org.hypertrace.core.grpcutils.context.RequestContext;
10+
import org.hypertrace.core.grpcutils.context.RequestContext.RequestContextHeader;
1111
import org.slf4j.Logger;
1212
import org.slf4j.LoggerFactory;
1313

@@ -28,17 +28,17 @@ public void thisUsesUnstableApi() {}
2828
protected void applyRequestContext(MetadataApplier applier, RequestContext requestContext) {
2929
Metadata metadata = new Metadata();
3030
if (requestContext != null) {
31-
for (Map.Entry<String, String> entry : requestContext.getAll().entrySet()) {
31+
for (RequestContextHeader header : requestContext.getAllHeaders()) {
3232
// Exclude null headers
33-
if (entry.getValue() != null) {
34-
String key = entry.getKey();
33+
if (header.getValue() != null) {
34+
String key = header.getName();
3535
if (key.toLowerCase().endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
3636
metadata.put(
37-
Metadata.Key.of(entry.getKey(), BINARY_BYTE_MARSHALLER),
38-
entry.getValue().getBytes());
37+
Metadata.Key.of(header.getName(), BINARY_BYTE_MARSHALLER),
38+
header.getValue().getBytes());
3939
} else {
4040
metadata.put(
41-
Metadata.Key.of(entry.getKey(), ASCII_STRING_MARSHALLER), entry.getValue());
41+
Metadata.Key.of(header.getName(), ASCII_STRING_MARSHALLER), header.getValue());
4242
}
4343
}
4444
}

grpc-context-utils/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ tasks.test {
1010
}
1111

1212
dependencies {
13-
api(platform("io.grpc:grpc-bom:1.45.1"))
13+
api(platform("io.grpc:grpc-bom:1.50.0"))
1414
implementation("io.grpc:grpc-core")
1515

1616
implementation("com.auth0:java-jwt:3.19.1")
1717
implementation("com.auth0:jwks-rsa:0.21.1")
1818
implementation("com.google.guava:guava:31.1-jre")
1919
implementation("org.slf4j:slf4j-api:1.7.36")
2020

21+
annotationProcessor("org.projectlombok:lombok:1.18.24")
22+
compileOnly("org.projectlombok:lombok:1.18.24")
23+
2124
constraints {
2225
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.4.2") {
2326
because("https://nvd.nist.gov/vuln/detail/CVE-2022-42003")

grpc-context-utils/src/main/java/org/hypertrace/core/grpcutils/context/DefaultContextualKey.java

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
package org.hypertrace.core.grpcutils.context;
22

3+
import com.google.common.collect.ImmutableListMultimap;
4+
import com.google.common.collect.Multimap;
35
import java.util.Collection;
4-
import java.util.Map;
5-
import java.util.Map.Entry;
66
import java.util.Objects;
7-
import java.util.Set;
87
import java.util.function.Consumer;
98
import java.util.function.Function;
109
import java.util.function.Supplier;
11-
import java.util.stream.Collectors;
1210

1311
class DefaultContextualKey<T> implements ContextualKey<T> {
1412
private final RequestContext context;
1513
private final T data;
16-
private final Map<String, String> cacheableContextHeaders;
14+
private final Multimap<String, String> cacheableContextHeaders;
1715

1816
DefaultContextualKey(RequestContext context, T data, Collection<String> cacheableHeaderNames) {
1917
this.context = context;
2018
this.data = data;
21-
this.cacheableContextHeaders =
22-
this.extractCacheableHeaders(context.getRequestHeaders(), cacheableHeaderNames);
19+
this.cacheableContextHeaders = this.extractCacheableHeaders(context, cacheableHeaderNames);
2320
}
2421

2522
@Override
@@ -76,14 +73,12 @@ public String toString() {
7673
+ '}';
7774
}
7875

79-
private Map<String, String> extractCacheableHeaders(
80-
Map<String, String> allHeaders, Collection<String> cacheableHeaderNames) {
81-
Set<String> cacheableHeaderNameSet =
82-
cacheableHeaderNames.stream()
83-
.map(String::toLowerCase)
84-
.collect(Collectors.toUnmodifiableSet());
85-
return allHeaders.entrySet().stream()
86-
.filter(entry -> cacheableHeaderNameSet.contains(entry.getKey().toLowerCase()))
87-
.collect(Collectors.toUnmodifiableMap(Entry::getKey, Entry::getValue));
76+
private Multimap<String, String> extractCacheableHeaders(
77+
RequestContext requestContext, Collection<String> cacheableHeaderNames) {
78+
return cacheableHeaderNames.stream()
79+
.collect(
80+
ImmutableListMultimap.flatteningToImmutableListMultimap(
81+
Function.identity(),
82+
headerName -> requestContext.getAllHeaderValues(headerName).stream()));
8883
}
8984
}

grpc-context-utils/src/main/java/org/hypertrace/core/grpcutils/context/RequestContext.java

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package org.hypertrace.core.grpcutils.context;
22

33
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
4+
import static java.util.Objects.requireNonNull;
45
import static org.hypertrace.core.grpcutils.context.RequestContextConstants.CACHE_MEANINGFUL_HEADERS;
56
import static org.hypertrace.core.grpcutils.context.RequestContextConstants.TENANT_ID_HEADER_KEY;
67

7-
import com.google.common.collect.Maps;
8+
import com.google.common.collect.ListMultimap;
9+
import com.google.common.collect.Multimap;
10+
import com.google.common.collect.MultimapBuilder;
11+
import com.google.common.collect.Multimaps;
812
import io.grpc.Context;
913
import io.grpc.Metadata;
1014
import io.grpc.Metadata.Key;
1115
import java.nio.charset.StandardCharsets;
1216
import java.util.Collections;
13-
import java.util.HashMap;
1417
import java.util.List;
1518
import java.util.Map;
1619
import java.util.Optional;
20+
import java.util.Set;
1721
import java.util.UUID;
1822
import java.util.concurrent.Callable;
23+
import java.util.stream.Collectors;
1924
import javax.annotation.Nonnull;
25+
import lombok.Value;
2026

2127
/**
2228
* Context of the GRPC request that should be carried and can made available to the services so that
@@ -26,10 +32,9 @@ public class RequestContext {
2632
public static final Context.Key<RequestContext> CURRENT = Context.key("request_context");
2733

2834
public static RequestContext forTenantId(String tenantId) {
29-
RequestContext requestContext = new RequestContext();
30-
requestContext.add(RequestContextConstants.TENANT_ID_HEADER_KEY, tenantId);
31-
requestContext.add(RequestContextConstants.REQUEST_ID_HEADER_KEY, UUID.randomUUID().toString());
32-
return requestContext;
35+
return new RequestContext()
36+
.put(RequestContextConstants.TENANT_ID_HEADER_KEY, tenantId)
37+
.put(RequestContextConstants.REQUEST_ID_HEADER_KEY, UUID.randomUUID().toString());
3338
}
3439

3540
public static RequestContext fromMetadata(Metadata metadata) {
@@ -53,19 +58,20 @@ public static RequestContext fromMetadata(Metadata metadata) {
5358
}
5459
// The value could be null or empty for some keys so validate that.
5560
if (value != null && !value.isEmpty()) {
56-
requestContext.add(k, value);
61+
requestContext.put(k, value);
5762
}
5863
});
5964

6065
return requestContext;
6166
}
6267

63-
private final Map<String, String> headers = new HashMap<>();
68+
private final ListMultimap<String, RequestContextHeader> headers =
69+
MultimapBuilder.linkedHashKeys().linkedListValues().build();
6470
private final JwtParser jwtParser = new JwtParser();
6571

6672
/** Reads tenant id from this RequestContext based on the tenant id http header and returns it. */
6773
public Optional<String> getTenantId() {
68-
return get(RequestContextConstants.TENANT_ID_HEADER_KEY);
74+
return getHeaderValue(RequestContextConstants.TENANT_ID_HEADER_KEY);
6975
}
7076

7177
public Optional<String> getUserId() {
@@ -96,28 +102,110 @@ public Optional<JwtClaim> getClaim(String claimName) {
96102
}
97103

98104
public Optional<String> getRequestId() {
99-
return this.get(RequestContextConstants.REQUEST_ID_HEADER_KEY);
105+
return this.getHeaderValue(RequestContextConstants.REQUEST_ID_HEADER_KEY);
100106
}
101107

102108
private Optional<Jwt> getJwt() {
103-
return get(RequestContextConstants.AUTHORIZATION_HEADER).flatMap(jwtParser::fromAuthHeader);
109+
return this.getHeaderValue(RequestContextConstants.AUTHORIZATION_HEADER)
110+
.flatMap(jwtParser::fromAuthHeader);
104111
}
105112

106-
/** Method to read all GRPC request headers from this RequestContext. */
113+
/**
114+
* This is retained for backwards compatibility, but is based on the incorrect assumption that a
115+
* header only can have one value. For the updated API, please use {@link #getAllHeaders()}}
116+
*/
117+
@Deprecated
107118
public Map<String, String> getRequestHeaders() {
108119
return getAll();
109120
}
110121

111-
public void add(String headerKey, String headerValue) {
112-
this.headers.put(headerKey, headerValue);
122+
/**
123+
* This is retained for backwards compatibility. It previously was implemented with the assumption
124+
* of a single value per header, so overwrote on addition. This is maintained by first removing
125+
* any values for the given header name, then adding
126+
*
127+
* @param headerName
128+
* @param headerValue
129+
*/
130+
@Deprecated
131+
public void add(String headerName, String headerValue) {
132+
this.removeHeader(headerName);
133+
this.put(headerName, headerValue);
113134
}
114135

115-
public Optional<String> get(String headerKey) {
116-
return Optional.ofNullable(this.headers.get(headerKey));
136+
/** Prefer {@link #getHeaderValue} */
137+
@Deprecated
138+
public Optional<String> get(String headerName) {
139+
return this.getHeaderValue(headerName);
117140
}
118141

142+
/**
143+
* This is retained for backwards compatibility, but is based on the incorrect assumption that a
144+
* header only can have one value. For the updated API, please use {@link #getAllHeaders()} ()}
145+
*/
146+
@Deprecated
119147
public Map<String, String> getAll() {
120-
return Map.copyOf(headers);
148+
return this.getHeaderNames().stream()
149+
.flatMap(key -> this.headers.get(key).stream().findFirst().stream())
150+
.collect(
151+
Collectors.toUnmodifiableMap(
152+
RequestContextHeader::getName, RequestContextHeader::getValue));
153+
}
154+
155+
/**
156+
* Adds the provided header name and header value. Duplicates are allowed. For fluency, the
157+
* current instance is returned.
158+
*/
159+
public RequestContext put(String headerName, String headerValue) {
160+
this.headers.put(
161+
this.normalizeHeaderName(requireNonNull(headerName)),
162+
new RequestContextHeader(headerName, headerValue));
163+
return this;
164+
}
165+
166+
/** Returns all header names in normalized form (case not preserved) */
167+
@Nonnull
168+
public Set<String> getHeaderNames() {
169+
return Set.copyOf(this.headers.keySet());
170+
}
171+
172+
/**
173+
* Removes and returns all headers matching the provided name. Header names are case insensitive.
174+
* Returns an empty list if no headers have been removed.
175+
*/
176+
@Nonnull
177+
public List<String> removeHeader(String name) {
178+
return this.headers.removeAll(this.normalizeHeaderName(name)).stream()
179+
.map(RequestContextHeader::getValue)
180+
.collect(Collectors.toUnmodifiableList());
181+
}
182+
183+
/** Returns all header values matching the provided header name, case insensitively. */
184+
@Nonnull
185+
public List<String> getAllHeaderValues(String key) {
186+
return this.headers.get(this.normalizeHeaderName(key)).stream()
187+
.map(RequestContextHeader::getValue)
188+
.collect(Collectors.toUnmodifiableList());
189+
}
190+
191+
/**
192+
* Returns all header name-value pairs, with the original case preserved. Multiple headers with
193+
* the same name, and potentially same value may be returned.
194+
*/
195+
@Nonnull
196+
public List<RequestContextHeader> getAllHeaders() {
197+
return List.copyOf(this.headers.values());
198+
}
199+
200+
/**
201+
* Gets the first header value specified for the provided name (case insensitive), or empty if no
202+
* match.
203+
*/
204+
@Nonnull
205+
public Optional<String> getHeaderValue(String key) {
206+
return this.headers.get(this.normalizeHeaderName(key)).stream()
207+
.map(RequestContextHeader::getValue)
208+
.findFirst();
121209
}
122210

123211
public <V> V call(@Nonnull Callable<V> callable) {
@@ -198,8 +286,12 @@ public Metadata buildTrailers() {
198286
return trailers;
199287
}
200288

201-
private Map<String, String> getHeadersOtherThanAuth() {
202-
return Maps.filterKeys(
289+
private String normalizeHeaderName(@Nonnull String headerName) {
290+
return headerName.toLowerCase();
291+
}
292+
293+
private Multimap<String, RequestContextHeader> getHeadersOtherThanAuth() {
294+
return Multimaps.filterKeys(
203295
headers, key -> !key.equals(RequestContextConstants.AUTHORIZATION_HEADER));
204296
}
205297

@@ -213,4 +305,10 @@ public String toString() {
213305
+ getJwt().map(Jwt::toString).orElse(emptyValue)
214306
+ '}';
215307
}
308+
309+
@Value
310+
public static class RequestContextHeader {
311+
String name;
312+
String value;
313+
}
216314
}

0 commit comments

Comments
 (0)