Skip to content

Commit bec39cc

Browse files
committed
cache-control example
uses cache-control headers from subgraphs to determine an overall cache-control policy partially addresses apollographql#326
1 parent 4e2cf70 commit bec39cc

File tree

14 files changed

+306
-24
lines changed

14 files changed

+306
-24
lines changed

Cargo.lock

+12
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,18 @@ dependencies = [
756756
"either",
757757
]
758758

759+
[[package]]
760+
name = "cache-control"
761+
version = "0.1.0"
762+
dependencies = [
763+
"anyhow",
764+
"apollo-router",
765+
"http",
766+
"serde_json",
767+
"tokio",
768+
"tower",
769+
]
770+
759771
[[package]]
760772
name = "cargo-scaffold"
761773
version = "0.8.7"

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"apollo-router-scaffold",
77
"examples/add-timestamp-header/rhai",
88
"examples/async-auth/rust",
9+
"examples/cache-control/rhai",
910
"examples/context/rust",
1011
"examples/cookies-to-headers/rhai",
1112
"examples/data-response-mutate/rhai",
@@ -35,7 +36,7 @@ strip = "debuginfo"
3536
incremental = false
3637

3738
# If building a dhat feature, you must use this profile
38-
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
39+
# e.g. heap allocation tracing: cargo build --profile release-dhat --features dhat-heap
3940
# e.g. heap and ad-hoc allocation tracing: cargo build --profile release-dhat --features dhat-heap,dhat-ad-hoc
4041
[profile.release-dhat]
4142
inherits = "release"

apollo-router/src/plugins/external_tests.rs

+2
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ mod tests {
377377
.errors(Vec::new())
378378
.extensions(crate::json_ext::Object::new())
379379
.context(req.context)
380+
.headers(HeaderMap::new())
380381
.build())
381382
});
382383

@@ -532,6 +533,7 @@ mod tests {
532533
.errors(Vec::new())
533534
.extensions(crate::json_ext::Object::new())
534535
.context(req.context)
536+
.headers(HeaderMap::new())
535537
.build())
536538
});
537539

apollo-router/src/plugins/rhai/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ macro_rules! gen_map_request {
641641
.and_data(body.data)
642642
.and_label(body.label)
643643
.and_path(body.path)
644+
.headers(HeaderMap::new())
644645
.build()
645646
} else {
646647
$base::Response::error_builder()

apollo-router/src/plugins/traffic_shaping/cache.rs

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ where
179179
.data(data)
180180
.extensions(Object::new())
181181
.context(request.context)
182+
.headers(http::HeaderMap::new())
182183
.build())
183184
}
184185
}

apollo-router/src/services/subgraph.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ impl Response {
138138
extensions: Object,
139139
status_code: Option<StatusCode>,
140140
context: Context,
141+
headers: http::HeaderMap<http::HeaderValue>,
141142
) -> Response {
142143
// Build a response
143144
let res = graphql::Response::builder()
@@ -149,11 +150,13 @@ impl Response {
149150
.build();
150151

151152
// Build an http Response
152-
let response = http::Response::builder()
153+
let mut response = http::Response::builder()
153154
.status(status_code.unwrap_or(StatusCode::OK))
154155
.body(res)
155156
.expect("Response is serializable; qed");
156157

158+
*response.headers_mut() = headers;
159+
157160
Self { response, context }
158161
}
159162

@@ -172,6 +175,7 @@ impl Response {
172175
extensions: JsonMap<ByteString, Value>,
173176
status_code: Option<StatusCode>,
174177
context: Option<Context>,
178+
headers: Option<http::HeaderMap<http::HeaderValue>>,
175179
) -> Response {
176180
Response::new(
177181
label,
@@ -181,6 +185,7 @@ impl Response {
181185
extensions,
182186
status_code,
183187
context.unwrap_or_default(),
188+
headers.unwrap_or_default(),
184189
)
185190
}
186191

@@ -201,6 +206,7 @@ impl Response {
201206
Default::default(),
202207
status_code,
203208
context,
209+
Default::default(),
204210
))
205211
}
206212
}

apollo-router/src/services/supergraph.rs

+14-12
Original file line numberDiff line numberDiff line change
@@ -131,29 +131,31 @@ impl Request {
131131
/// Create a request with an example query, for tests
132132
#[builder(visibility = "pub")]
133133
fn canned_new(
134+
query: Option<String>,
134135
operation_name: Option<String>,
135136
// Skip the `Object` type alias in order to use buildstructor’s map special-casing
136137
extensions: JsonMap<ByteString, Value>,
137138
context: Option<Context>,
138139
headers: MultiMap<TryIntoHeaderName, TryIntoHeaderValue>,
139140
) -> Result<Request, BoxError> {
140-
let query = "
141-
query TopProducts($first: Int) {
142-
topProducts(first: $first) {
143-
upc
144-
name
145-
reviews {
146-
id
147-
product { name }
148-
author { id name }
149-
}
150-
}
141+
let default_query = "
142+
query TopProducts($first: Int) {
143+
topProducts(first: $first) {
144+
upc
145+
name
146+
reviews {
147+
id
148+
product { name }
149+
author { id name }
150+
}
151+
}
151152
}
152153
";
154+
let query = query.unwrap_or(default_query.to_string());
153155
let mut variables = JsonMap::new();
154156
variables.insert("first", 2_usize.into());
155157
Self::fake_new(
156-
Some(query.to_owned()),
158+
Some(query),
157159
operation_name,
158160
variables,
159161
extensions,

apollo-router/src/test_harness.rs

+1
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ impl<'a> TestHarness<'a> {
214214
let empty_response = subgraph::Response::builder()
215215
.extensions(crate::json_ext::Object::new())
216216
.context(request.context)
217+
.headers(http::HeaderMap::new())
217218
.build();
218219
std::future::ready(Ok(empty_response))
219220
})

apollo-router/tests/fixtures/request_response_test.rhai

+10-10
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ fn process_supergraph_request(request) {
4141
}
4242
let expected_query = `
4343

44-
query TopProducts($first: Int) {
45-
topProducts(first: $first) {
46-
upc
47-
name
48-
reviews {
49-
id
50-
product { name }
51-
author { id name }
52-
}
53-
}
44+
query TopProducts($first: Int) {
45+
topProducts(first: $first) {
46+
upc
47+
name
48+
reviews {
49+
id
50+
product { name }
51+
author { id name }
52+
}
53+
}
5454
}
5555
`;
5656
if request.body.query != expected_query {
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "cache-control"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
[dependencies]
8+
anyhow = "1"
9+
apollo-router = { path = "../../../apollo-router" }
10+
http = "0.2"
11+
serde_json = "1"
12+
tokio = { version = "1", features = ["full"] }
13+
tower = { version = "0.4", features = ["full"] }

examples/cache-control/rhai/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Rhai script
2+
3+
Demonstrates header and context manipulation via Rhai script.
4+
5+
Usage:
6+
7+
```bash
8+
cargo run -- -s ../../graphql/supergraph.graphql -c ./router.yaml
9+
```
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rhai:
2+
scripts: src
3+
main: cache_control.rhai
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
fn subgraph_service(service, subgraph) {
2+
// collect the max-age and scope values from cache-control headers and store
3+
// on the context for use in supergraph_service
4+
service.map_response(|response| {
5+
let cache_control = response.headers.values("cache-control").get(0);
6+
7+
// if a subgraph response is uncacheable, the whole response is uncacheable
8+
if cache_control == () {
9+
response.context.cache_control_uncacheable = true;
10+
return;
11+
}
12+
13+
let max_age = get_max_age(cache_control);
14+
15+
// use the smallest max age
16+
response.context.upsert("cache_control_max_age", |current| {
17+
if current == () {
18+
max_age
19+
} else if max_age < current {
20+
max_age
21+
} else {
22+
current
23+
}
24+
});
25+
26+
let scope = if cache_control.contains("public") {
27+
"public"
28+
} else {
29+
"private"
30+
};
31+
32+
// if the scope is ever private, it cannot become public
33+
response.context.upsert("cache_control_scope", |current| {
34+
if current == "private" || scope == "private" {
35+
"private"
36+
} else {
37+
scope
38+
}
39+
});
40+
});
41+
}
42+
43+
fn supergraph_service(service) {
44+
// attach the cache-control header if enough data is available
45+
service.map_response(|response| {
46+
let uncacheable = response.context.cache_control_uncacheable;
47+
let max_age = response.context.cache_control_max_age;
48+
let scope = response.context.cache_control_scope;
49+
50+
if uncacheable != true && max_age != () && scope != () {
51+
response.headers["cache-control"] = `max-age=${max_age}, ${scope}`;
52+
}
53+
});
54+
}
55+
56+
// find the the max-age= part and parse the value into an integer
57+
fn get_max_age(str) {
58+
let max_age = 0;
59+
60+
for part in str.split(",") {
61+
part.remove(" ");
62+
63+
if part.starts_with("max-age=") {
64+
let num = part.split("=").get(1);
65+
66+
if num == () || num == "" {
67+
break;
68+
}
69+
70+
try {
71+
max_age = num.parse_int();
72+
} catch (err) {
73+
log_error(`error parsing max-age from "${str}": ${err}`);
74+
}
75+
break;
76+
}
77+
}
78+
79+
max_age
80+
}

0 commit comments

Comments
 (0)