Skip to content

Commit f4b9e21

Browse files
refactor: move server responseHeaders to server headers custom (#1402)
Co-authored-by: Tushar Mathur <[email protected]>
1 parent de4213e commit f4b9e21

17 files changed

+193
-62
lines changed

generated/.tailcallrc.graphql

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -286,12 +286,6 @@ directive @server(
286286
"""
287287
queryValidation: Boolean
288288
"""
289-
The `responseHeaders` are key-value pairs included in every server response. Useful
290-
for setting headers like `Access-Control-Allow-Origin` for cross-origin requests
291-
or additional headers for downstream services.
292-
"""
293-
responseHeaders: [KeyValue]
294-
"""
295289
`responseValidation` Tailcall automatically validates responses from upstream services
296290
using inferred schema. @default `false`.
297291
"""
@@ -577,6 +571,12 @@ input Headers {
577571
value is the least of the values received from upstream services. @default `false`.
578572
"""
579573
cacheControl: Boolean
574+
"""
575+
`headers` are key-value pairs included in every server response. Useful for setting
576+
headers like `Access-Control-Allow-Origin` for cross-origin requests or additional
577+
headers for downstream services.
578+
"""
579+
custom: [KeyValue]
580580
}
581581
"""
582582
The @http operator indicates that a field or node is backed by a REST API.For instance,

generated/.tailcallrc.schema.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,13 @@
12151215
"boolean",
12161216
"null"
12171217
]
1218+
},
1219+
"custom": {
1220+
"description": "`headers` are key-value pairs included in every server response. Useful for setting headers like `Access-Control-Allow-Origin` for cross-origin requests or additional headers for downstream services.",
1221+
"type": "array",
1222+
"items": {
1223+
"$ref": "#/definitions/KeyValue"
1224+
}
12181225
}
12191226
}
12201227
},
@@ -1588,13 +1595,6 @@
15881595
"null"
15891596
]
15901597
},
1591-
"responseHeaders": {
1592-
"description": "The `responseHeaders` are key-value pairs included in every server response. Useful for setting headers like `Access-Control-Allow-Origin` for cross-origin requests or additional headers for downstream services.",
1593-
"type": "array",
1594-
"items": {
1595-
"$ref": "#/definitions/KeyValue"
1596-
}
1597-
},
15981598
"responseValidation": {
15991599
"description": "`responseValidation` Tailcall automatically validates responses from upstream services using inferred schema. @default `false`.",
16001600
"type": [

src/blueprint/server.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ fn validate_hostname(hostname: String) -> Valid<IpAddr, String> {
160160
}
161161
}
162162

163-
fn handle_response_headers(resp_headers: BTreeMap<String, String>) -> Valid<HeaderMap, String> {
163+
fn handle_response_headers(resp_headers: Vec<(String, String)>) -> Valid<HeaderMap, String> {
164164
Valid::from_iter(resp_headers.iter(), |(k, v)| {
165165
let name = Valid::from(
166166
HeaderName::from_bytes(k.as_bytes())
@@ -173,7 +173,8 @@ fn handle_response_headers(resp_headers: BTreeMap<String, String>) -> Valid<Head
173173
name.zip(value)
174174
})
175175
.map(|headers| headers.into_iter().collect::<HeaderMap>())
176-
.trace("responseHeaders")
176+
.trace("custom")
177+
.trace("headers")
177178
.trace("@server")
178179
.trace("schema")
179180
}

src/config/headers.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::{Deserialize, Serialize};
22

3+
use crate::config::KeyValue;
34
use crate::is_default;
45

56
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, schemars::JsonSchema)]
@@ -10,10 +11,33 @@ pub struct Headers {
1011
/// activated. The `max-age` value is the least of the values received from
1112
/// upstream services. @default `false`.
1213
pub cache_control: Option<bool>,
14+
15+
#[serde(default, skip_serializing_if = "is_default")]
16+
/// `headers` are key-value pairs included in every server
17+
/// response. Useful for setting headers like `Access-Control-Allow-Origin`
18+
/// for cross-origin requests or additional headers for downstream services.
19+
pub custom: Vec<KeyValue>,
1320
}
1421

1522
impl Headers {
1623
pub fn enable_cache_control(&self) -> bool {
1724
self.cache_control.unwrap_or(false)
1825
}
1926
}
27+
28+
pub fn merge_headers(current: Option<Headers>, other: Option<Headers>) -> Option<Headers> {
29+
let mut headers = current.clone();
30+
31+
if let Some(other_headers) = other {
32+
if let Some(mut self_headers) = current.clone() {
33+
self_headers.cache_control = other_headers.cache_control.or(self_headers.cache_control);
34+
self_headers.custom.extend(other_headers.custom);
35+
36+
headers = Some(self_headers);
37+
} else {
38+
headers = Some(other_headers);
39+
}
40+
}
41+
42+
headers
43+
}

src/config/key_values.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ pub struct KeyValue {
2626
pub value: String,
2727
}
2828

29+
pub fn merge_key_value_vecs(current: &[KeyValue], other: &[KeyValue]) -> Vec<KeyValue> {
30+
let mut acc: BTreeMap<&String, &String> =
31+
current.iter().map(|kv| (&kv.key, &kv.value)).collect();
32+
33+
for kv in other {
34+
acc.insert(&kv.key, &kv.value);
35+
}
36+
37+
acc.iter()
38+
.map(|(k, v)| KeyValue { key: k.to_string(), value: v.to_string() })
39+
.collect()
40+
}
41+
2942
impl Serialize for KeyValues {
3043
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3144
where
@@ -97,4 +110,54 @@ mod tests {
97110
// Using the deref trait
98111
assert_eq!(kv["a"], "b");
99112
}
113+
114+
#[test]
115+
fn test_merge_with_both_empty() {
116+
let current = vec![];
117+
let other = vec![];
118+
let result = merge_key_value_vecs(&current, &other);
119+
assert!(result.is_empty());
120+
}
121+
122+
#[test]
123+
fn test_merge_with_current_empty() {
124+
let current = vec![];
125+
let other = vec![KeyValue { key: "key1".to_string(), value: "value1".to_string() }];
126+
let result = merge_key_value_vecs(&current, &other);
127+
assert_eq!(result.len(), 1);
128+
assert_eq!(result[0].key, "key1");
129+
assert_eq!(result[0].value, "value1");
130+
}
131+
132+
#[test]
133+
fn test_merge_with_other_empty() {
134+
let current = vec![KeyValue { key: "key1".to_string(), value: "value1".to_string() }];
135+
let other = vec![];
136+
let result = merge_key_value_vecs(&current, &other);
137+
assert_eq!(result.len(), 1);
138+
assert_eq!(result[0].key, "key1");
139+
assert_eq!(result[0].value, "value1");
140+
}
141+
142+
#[test]
143+
fn test_merge_with_unique_keys() {
144+
let current = vec![KeyValue { key: "key1".to_string(), value: "value1".to_string() }];
145+
let other = vec![KeyValue { key: "key2".to_string(), value: "value2".to_string() }];
146+
let result = merge_key_value_vecs(&current, &other);
147+
assert_eq!(result.len(), 2);
148+
assert_eq!(result[0].key, "key1");
149+
assert_eq!(result[0].value, "value1");
150+
assert_eq!(result[1].key, "key2");
151+
assert_eq!(result[1].value, "value2");
152+
}
153+
154+
#[test]
155+
fn test_merge_with_overlapping_keys() {
156+
let current = vec![KeyValue { key: "key1".to_string(), value: "value1".to_string() }];
157+
let other = vec![KeyValue { key: "key1".to_string(), value: "value2".to_string() }];
158+
let result = merge_key_value_vecs(&current, &other);
159+
assert_eq!(result.len(), 1);
160+
assert_eq!(result[0].key, "key1");
161+
assert_eq!(result[0].value, "value2");
162+
}
100163
}

src/config/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub use config::*;
22
pub use config_module::*;
33
pub use expr::*;
4+
pub use headers::*;
45
pub use key_values::*;
56
pub use link::*;
67
pub use reader_context::*;

src/config/server.rs

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
22

33
use serde::{Deserialize, Serialize};
44

5+
use super::{merge_headers, merge_key_value_vecs};
56
use crate::config::headers::Headers;
67
use crate::config::KeyValue;
78
use crate::is_default;
@@ -64,12 +65,6 @@ pub struct Server {
6465
/// @default `false`.
6566
pub query_validation: Option<bool>,
6667

67-
#[serde(skip_serializing_if = "is_default", default)]
68-
/// The `responseHeaders` are key-value pairs included in every server
69-
/// response. Useful for setting headers like `Access-Control-Allow-Origin`
70-
/// for cross-origin requests or additional headers for downstream services.
71-
pub response_headers: Vec<KeyValue>,
72-
7368
#[serde(default, skip_serializing_if = "is_default")]
7469
/// `responseValidation` Tailcall automatically validates responses from
7570
/// upstream services using inferred schema. @default `false`.
@@ -165,12 +160,16 @@ impl Server {
165160
.collect()
166161
}
167162

168-
pub fn get_response_headers(&self) -> BTreeMap<String, String> {
169-
self.response_headers
170-
.clone()
171-
.iter()
172-
.map(|kv| (kv.key.clone(), kv.value.clone()))
173-
.collect()
163+
pub fn get_response_headers(&self) -> Vec<(String, String)> {
164+
self.headers
165+
.as_ref()
166+
.map(|h| h.custom.clone())
167+
.map_or(Vec::new(), |headers| {
168+
headers
169+
.iter()
170+
.map(|kv| (kv.key.clone(), kv.value.clone()))
171+
.collect()
172+
})
174173
}
175174

176175
pub fn get_version(self) -> HttpVersion {
@@ -183,7 +182,7 @@ impl Server {
183182

184183
pub fn merge_right(mut self, other: Self) -> Self {
185184
self.apollo_tracing = other.apollo_tracing.or(self.apollo_tracing);
186-
self.headers = other.headers.or(self.headers);
185+
self.headers = merge_headers(self.headers, other.headers);
187186
self.graphiql = other.graphiql.or(self.graphiql);
188187
self.introspection = other.introspection.or(self.introspection);
189188
self.query_validation = other.query_validation.or(self.query_validation);
@@ -196,7 +195,7 @@ impl Server {
196195
self.workers = other.workers.or(self.workers);
197196
self.port = other.port.or(self.port);
198197
self.hostname = other.hostname.or(self.hostname);
199-
self.vars = other.vars.iter().fold(self.vars, |mut acc, kv| {
198+
self.vars = other.vars.iter().fold(self.vars.to_vec(), |mut acc, kv| {
200199
let position = acc.iter().position(|x| x.key == kv.key);
201200
if let Some(pos) = position {
202201
acc[pos] = kv.clone();
@@ -205,19 +204,7 @@ impl Server {
205204
};
206205
acc
207206
});
208-
self.response_headers =
209-
other
210-
.response_headers
211-
.iter()
212-
.fold(self.response_headers, |mut acc, kv| {
213-
let position = acc.iter().position(|x| x.key == kv.key);
214-
if let Some(pos) = position {
215-
acc[pos] = kv.clone();
216-
} else {
217-
acc.push(kv.clone());
218-
};
219-
acc
220-
});
207+
self.vars = merge_key_value_vecs(&self.vars, &other.vars);
221208
self.version = other.version.or(self.version);
222209
self.pipeline_flush = other.pipeline_flush.or(self.pipeline_flush);
223210
self.script = other.script.or(self.script);

tests/execution/custom-headers.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
```json @server
44
{
55
"server": {
6-
"responseHeaders": [
7-
{
8-
"key": "x-id",
9-
"value": "1"
10-
},
11-
{
12-
"key": "x-name",
13-
"value": "John Doe"
14-
}
15-
]
6+
"headers": {
7+
"custom": [
8+
{
9+
"key": "x-id",
10+
"value": "1"
11+
},
12+
{
13+
"key": "x-name",
14+
"value": "John Doe"
15+
}
16+
]
17+
}
1618
},
1719
"upstream": {},
1820
"schema": {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# test-response-header-value
2+
3+
```graphql @server
4+
schema @server(headers: {custom: [{key: "a", value: "a"}]}) {
5+
query: Query
6+
}
7+
8+
type User {
9+
name: String
10+
age: Int
11+
}
12+
13+
type Query {
14+
user: User @const(data: {name: "John"})
15+
}
16+
```
17+
18+
```graphql @server
19+
schema @server(headers: {custom: [{key: "a", value: "b"}]}) {
20+
query: Query
21+
}
22+
23+
type User {
24+
name: String
25+
age: Int
26+
}
27+
28+
type Query {
29+
user: User @const(data: {name: "John"})
30+
}
31+
```

tests/execution/test-response-header-value.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
###### sdl error
44

55
```graphql @server
6-
schema @server(responseHeaders: [{key: "a", value: "a \n b"}]) {
6+
schema @server(headers: {custom: [{key: "a", value: "a \n b"}]}) {
77
query: Query
88
}
99

0 commit comments

Comments
 (0)