Skip to content

Commit 509ec0b

Browse files
committed
Add interceptor to copy HTTP headers
Closes gh-1313
1 parent 70e96ae commit 509ec0b

File tree

5 files changed

+287
-21
lines changed

5 files changed

+287
-21
lines changed

spring-graphql-docs/modules/ROOT/pages/controllers.adoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,18 @@ https://github.com/spring-projects/spring-graphql/issues/344#issuecomment-108281
703703
for links to relevant issues and a suggested workaround.
704704
====
705705

706+
707+
[[controllers.schema-mapping.httpheaders]]
708+
=== HTTP Headers
709+
710+
To access HTTP headers from controller methods, we recommend using a `WebGraphQlInterceptor`
711+
to copy HTTP headers of interest into the `GraphQLContext` from where they are available to
712+
any `DataFetcher`, and to annotated controller methods as `@GraphQlContext` arguments.
713+
714+
There is a built-in `HttpRequestHeaderInterceptor` to help with that.
715+
see xref:transports.adoc#server.interception.web[Web Interceptors].
716+
717+
706718
[[controllers.schema-mapping.localcontext]]
707719
=== Local Context
708720

spring-graphql-docs/modules/ROOT/pages/transports.adoc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ and the `DecoderHttpMessageReader/EncoderHttpMessageWriter` (WebFlux) configured
4040
In some cases, the application will configure the JSON codec for the HTTP endpoint in a way that is not compatible with the GraphQL payloads.
4141
Applications can instantiate `GraphQlHttpHandler` with a custom JSON codec that will be used for GraphQL payloads.
4242

43-
The 1.0.x branch of this repository contains a Spring MVC
44-
{github-10x-branch}/samples/webmvc-http[HTTP sample] application.
45-
4643

4744
[[server.transports.sse]]
4845
== Server-Sent Events
@@ -165,13 +162,16 @@ Interceptors allow applications to intercept incoming requests in order to:
165162
- Customize the `graphql.ExecutionResult`
166163
- and more
167164

168-
For example, an interceptor can pass an HTTP request header to a `DataFetcher`:
165+
Spring for GraphQL provides a built-in `HttpRequestHeaderInterceptor` that copies HTTP headers
166+
from the request to the GraphQL context, which then makes them available to data fetchers
167+
such as annotated controllers. For example in a Spring Boot application this may be done
168+
as follows:
169169

170-
include-code::RequestHeaderInterceptor[]
171-
<1> Interceptor adds HTTP request header value into GraphQLContext
170+
include-code::RequestHeaderInterceptorConfig[]
171+
<1> Create interceptor to copy an HTTP request header value into the GraphQLContext
172172
<2> Data controller method accesses the value
173173

174-
Reversely, an interceptor can access values added to the `GraphQLContext` by a controller:
174+
An interceptor can also access values added to the `GraphQLContext` by a controller:
175175

176176
include-code::ResponseHeaderInterceptor[]
177177
<1> Controller adds value to the `GraphQLContext`
Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,19 @@
1616

1717
package org.springframework.graphql.docs.server.interception.web;
1818

19-
import java.util.Collections;
20-
21-
import reactor.core.publisher.Mono;
22-
19+
import org.springframework.context.annotation.Bean;
20+
import org.springframework.context.annotation.Configuration;
2321
import org.springframework.graphql.data.method.annotation.ContextValue;
2422
import org.springframework.graphql.data.method.annotation.QueryMapping;
25-
import org.springframework.graphql.server.WebGraphQlInterceptor;
26-
import org.springframework.graphql.server.WebGraphQlRequest;
27-
import org.springframework.graphql.server.WebGraphQlResponse;
23+
import org.springframework.graphql.server.support.HttpRequestHeaderInterceptor;
2824
import org.springframework.stereotype.Controller;
2925

30-
class RequestHeaderInterceptor implements WebGraphQlInterceptor { // <1>
26+
@Configuration
27+
class RequestHeaderInterceptorConfig {
3128

32-
@Override
33-
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
34-
String value = request.getHeaders().getFirst("myHeader");
35-
request.configureExecutionInput((executionInput, builder) ->
36-
builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
37-
return chain.next(request);
29+
@Bean
30+
public HttpRequestHeaderInterceptor headerInterceptor() { // <1>
31+
return HttpRequestHeaderInterceptor.builder().mapHeader("myHeader").build();
3832
}
3933
}
4034

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2020-present the original author or authors.
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+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.support;
18+
19+
import java.util.ArrayList;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.function.BiConsumer;
24+
25+
import org.jspecify.annotations.Nullable;
26+
import reactor.core.publisher.Mono;
27+
28+
import org.springframework.graphql.server.WebGraphQlInterceptor;
29+
import org.springframework.graphql.server.WebGraphQlRequest;
30+
import org.springframework.graphql.server.WebGraphQlResponse;
31+
import org.springframework.http.HttpHeaders;
32+
import org.springframework.util.ObjectUtils;
33+
34+
/**
35+
* Interceptor that copies HTTP request headers to the GraphQL context to make
36+
* them available to data fetchers such as annotated controllers, which can use
37+
* {@link org.springframework.graphql.data.method.annotation.ContextValue @ContextValue}
38+
* method parameters to access the headers as context values.
39+
*
40+
* <p>User {@link #builder()} to build an instance, and specify headers of
41+
* interest that should be copied to the GraphQL context.
42+
*
43+
* @author Rossen Stoyanchev
44+
* @since 2.0
45+
*/
46+
public final class HttpRequestHeaderInterceptor implements WebGraphQlInterceptor {
47+
48+
private final List<BiConsumer<HttpHeaders, Map<String, Object>>> mappers;
49+
50+
51+
private HttpRequestHeaderInterceptor(List<BiConsumer<HttpHeaders, Map<String, Object>>> mappers) {
52+
this.mappers = new ArrayList<>(mappers);
53+
}
54+
55+
56+
@Override
57+
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
58+
request.configureExecutionInput((executionInput, builder) -> {
59+
HttpHeaders headers = request.getHeaders();
60+
Map<String, Object> target = new HashMap<>(this.mappers.size());
61+
this.mappers.forEach((mapper) -> mapper.accept(headers, target));
62+
builder.graphQLContext(target);
63+
return builder.build();
64+
});
65+
return chain.next(request);
66+
}
67+
68+
69+
/**
70+
* Return a builder to create an {@link HttpRequestHeaderInterceptor}.
71+
*/
72+
public static Builder builder() {
73+
return new DefaultBuilder();
74+
}
75+
76+
77+
/**
78+
* Builder for {@link HttpRequestHeaderInterceptor}.
79+
*/
80+
public interface Builder {
81+
82+
/**
83+
* Add names of HTTP headers to copy to the GraphQL context, using keys
84+
* identical to the header names. Only the first value is copied.
85+
* @param headerName the name(s) of header(s) to copy
86+
*/
87+
Builder mapHeader(String... headerName);
88+
89+
/**
90+
* Add a mapping between an HTTP header name and the key under which it
91+
* should appear in the GraphQL context. Only the first value is copied.
92+
* @param headerName the name of a header to copy
93+
* @param contextKey the key to map to in the GraphQL context
94+
*/
95+
Builder mapHeaderToKey(String headerName, String contextKey);
96+
97+
/**
98+
* Add names of HTTP headers to copy to the GraphQL context, using keys
99+
* identical to the header names. All values are copied as a List.
100+
* @param headerName the name(s) of header(s) to copy
101+
*/
102+
Builder mapMultiValueHeader(String... headerName);
103+
104+
/**
105+
* Add a mapping between an HTTP header name and the key under which it
106+
* should appear in the GraphQL context. All values are copied as a List.
107+
* @param headerName the name of a header to copy
108+
* @param contextKey the key to map to in the GraphQL context
109+
*/
110+
Builder mapMultiValueHeaderToKey(String headerName, String contextKey);
111+
112+
/**
113+
* Create the interceptor instance.
114+
*/
115+
HttpRequestHeaderInterceptor build();
116+
}
117+
118+
119+
/**
120+
* Default implementation of {@link Builder}.
121+
*/
122+
private static final class DefaultBuilder implements Builder {
123+
124+
private final List<BiConsumer<HttpHeaders, Map<String, Object>>> mappers = new ArrayList<>();
125+
126+
@Override
127+
public DefaultBuilder mapHeader(String... headers) {
128+
for (String header : headers) {
129+
initMapper(header, null);
130+
}
131+
return this;
132+
}
133+
134+
@Override
135+
public DefaultBuilder mapHeaderToKey(String header, String contextKey) {
136+
initMapper(header, contextKey);
137+
return this;
138+
}
139+
140+
private void initMapper(String header, @Nullable String key) {
141+
this.mappers.add((headers, target) -> {
142+
Object value = headers.getFirst(header);
143+
if (value != null) {
144+
target.put((key != null) ? key : header, value);
145+
}
146+
});
147+
}
148+
149+
@Override
150+
public DefaultBuilder mapMultiValueHeader(String... headers) {
151+
for (String header : headers) {
152+
initMultiValueMapper(header, null);
153+
}
154+
return this;
155+
}
156+
157+
@Override
158+
public DefaultBuilder mapMultiValueHeaderToKey(String header, String contextKey) {
159+
initMultiValueMapper(header, contextKey);
160+
return this;
161+
}
162+
163+
private void initMultiValueMapper(String header, @Nullable String key) {
164+
this.mappers.add((headers, target) -> {
165+
List<?> list = headers.getValuesAsList(header);
166+
if (!ObjectUtils.isEmpty(list)) {
167+
target.put((key != null) ? key : header, list);
168+
}
169+
});
170+
}
171+
172+
@Override
173+
public HttpRequestHeaderInterceptor build() {
174+
return new HttpRequestHeaderInterceptor(this.mappers);
175+
}
176+
}
177+
178+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2020-present the original author or authors.
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+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.server.support;
18+
19+
import java.net.URI;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import graphql.GraphQLContext;
24+
import org.junit.jupiter.api.Test;
25+
import reactor.core.publisher.Mono;
26+
27+
import org.springframework.graphql.server.WebGraphQlInterceptor;
28+
import org.springframework.graphql.server.WebGraphQlRequest;
29+
import org.springframework.graphql.server.WebGraphQlResponse;
30+
import org.springframework.graphql.support.DefaultGraphQlRequest;
31+
import org.springframework.http.HttpHeaders;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Unit tests for {@link HttpRequestHeaderInterceptor}.
37+
* @author Rossen Stoyanchev
38+
*/
39+
class HttpRequestHeaderInterceptorTests {
40+
41+
@Test
42+
void map() {
43+
HttpHeaders headers = new HttpHeaders();
44+
headers.add("h1", "v1");
45+
headers.put("h2", List.of("v2A", "v2B"));
46+
headers.add("h3", "v3");
47+
headers.put("h4", List.of("v4A", "v4B"));
48+
headers.put("h5", List.of("v5A", "v5B"));
49+
50+
HttpRequestHeaderInterceptor interceptor = HttpRequestHeaderInterceptor.builder()
51+
.mapHeader("h1", "h2")
52+
.mapHeaderToKey("h3", "k3")
53+
.mapMultiValueHeader("h4")
54+
.mapMultiValueHeaderToKey("h5", "k5")
55+
.build();
56+
57+
WebGraphQlRequest request = new WebGraphQlRequest(
58+
URI.create("/"), headers, null, null, Collections.emptyMap(),
59+
new DefaultGraphQlRequest("{ q }"), "id", null);
60+
61+
interceptor.intercept(request, new TestChain()).block();
62+
63+
GraphQLContext context = request.toExecutionInput().getGraphQLContext();
64+
65+
assertThat(context.<String>get("h1")).isEqualTo("v1");
66+
assertThat(context.<String>get("h2")).isEqualTo("v2A");
67+
assertThat(context.<String>get("k3")).isEqualTo("v3");
68+
assertThat(context.<List<String>>get("h4")).containsExactly("v4A", "v4B");
69+
assertThat(context.<List<String>>get("k5")).containsExactly("v5A", "v5B");
70+
}
71+
72+
73+
@SuppressWarnings("NullableProblems")
74+
private static final class TestChain implements WebGraphQlInterceptor.Chain {
75+
76+
@Override
77+
public Mono<WebGraphQlResponse> next(WebGraphQlRequest request) {
78+
return Mono.empty();
79+
}
80+
}
81+
82+
}

0 commit comments

Comments
 (0)