Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changesets/fix_bnjjj_router_1539.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### fix(response_cache): change plugin ordering to make sure we can customize caching behavior at the subgraph level ([PR #8652](https://github.com/apollographql/router/pull/8652))

With this change you'll now be able to customize cached response using rhai or coprocessors. It's also now possible to set a different [`private_id`](https://www.apollographql.com/docs/graphos/routing/performance/caching/response-caching/customization#configure-private_id) based on subgraph request (like headers for example).

Example of rhai script customizing the `private_id`:

```rhai
fn subgraph_service(service, subgraph) {
service.map_request(|request| {
if "private_id" in request.headers {
request.context["private_id"] = request.headers["private_id"];
}
});
}

```

By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/8652
2 changes: 1 addition & 1 deletion apollo-router/src/router_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,6 @@ pub(crate) async fn create_plugins(
add_optional_apollo_plugin!("authorization");
add_optional_apollo_plugin!("authentication");
add_oss_apollo_plugin!("preview_file_uploads");
add_optional_apollo_plugin!("preview_response_cache");
add_optional_apollo_plugin!("preview_entity_cache");
add_mandatory_apollo_plugin!("progressive_override");
add_optional_apollo_plugin!("demand_control");
Expand All @@ -896,6 +895,7 @@ pub(crate) async fn create_plugins(
add_oss_apollo_plugin!("connectors");
add_oss_apollo_plugin!("rhai");
add_optional_apollo_plugin!("coprocessor");
add_optional_apollo_plugin!("preview_response_cache");
add_user_plugins!();

// Because this plugin intercepts subgraph requests
Expand Down
7 changes: 7 additions & 0 deletions apollo-router/tests/integration/fixtures/test_cache.rhai
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn subgraph_service(service, subgraph) {
service.map_request(|request| {
if "private_id" in request.headers {
request.context["private_id"] = request.headers["private_id"];
}
});
}
130 changes: 130 additions & 0 deletions apollo-router/tests/integration/response_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ fn base_config() -> Value {
"include_subgraph_errors": {
"all": true,
},
"rhai": {
"scripts": "tests/integration/fixtures",
"main": "test_cache.rhai",
},
"headers": {
"all": {
"request": [
{
"propagate": {
"named": "private_id"
}
}
]
}
},
"preview_response_cache": {
"enabled": true,
"subgraph": {
Expand All @@ -63,6 +78,7 @@ fn base_config() -> Value {
"enabled": true,
"shared_key": INVALIDATION_SHARED_KEY,
},
"private_id": "private_id"
},
},
"invalidation": {
Expand Down Expand Up @@ -134,6 +150,37 @@ fn base_subgraphs() -> Value {
})
}

fn private_base_subgraphs() -> Value {
json!({
"products": {
"headers": {"cache-control": "private"},
"query": {
"topProducts": [
{"upc": "1", "__cacheTags": ["topProducts"]},
{"upc": "2"},
],
},
},
"reviews": {
"headers": {"cache-control": "private"},
"entities": [
{
"__cacheTags": ["product-1"],
"__typename": "Product",
"upc": "1",
"reviews": [{"id": "r1a"}, {"id": "r1b"}],
},
{
"__cacheTags": ["product-2"],
"__typename": "Product",
"upc": "2",
"reviews": [{"id": "r2"}],
},
],
},
})
}

async fn harness(
mut config: Value,
subgraphs: Value,
Expand Down Expand Up @@ -190,6 +237,20 @@ async fn make_debug_graphql_request(
make_http_request(router, request).await
}

async fn makegraphql_request_with_headers(
router: &mut HttpService,
headers: HeaderMap,
) -> (HeaderMap<String>, graphql::Response) {
let query = "{ topProducts { reviews { id } } }";
let request = graphql_request(query);
let mut request: http::Request<router::Body> = request.into();
for (key, value) in headers {
request.headers_mut().insert(key.unwrap(), value);
}

make_http_request(router, request).await
}

fn graphql_request(query: &str) -> router::Request {
supergraph::Request::fake_builder()
.query(query)
Expand Down Expand Up @@ -270,6 +331,75 @@ async fn basic_cache_skips_subgraph_request() {
");
}

/// This test ensures that the plugin ordering is correct and we can customize the private_id at the subgraph request level.
#[tokio::test(flavor = "multi_thread")]
async fn private_id_set_at_subgraph_request() {
if !graph_os_enabled() {
return;
}

let (mut router, subgraph_request_counters) =
harness(base_config(), private_base_subgraphs()).await;
insta::assert_yaml_snapshot!(subgraph_request_counters, @r"
products: 0
reviews: 0
");
let (headers, body) = make_graphql_request(&mut router).await;
assert!(headers["cache-control"].contains("private"));
insta::assert_yaml_snapshot!(body, @r"
data:
topProducts:
- reviews:
- id: r1a
- id: r1b
- reviews:
- id: r2
");
insta::assert_yaml_snapshot!(subgraph_request_counters, @r"
products: 1
reviews: 1
");
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("private_id"),
HeaderValue::from_static("123"),
);
let (resp_headers, body) = makegraphql_request_with_headers(&mut router, headers.clone()).await;
assert!(resp_headers["cache-control"].contains("private"));
insta::assert_yaml_snapshot!(body, @r"
data:
topProducts:
- reviews:
- id: r1a
- id: r1b
- reviews:
- id: r2
");
// Incremented because the previous one was not cached because it didn't contain the private_id set by the rhai script using private_id header value
insta::assert_yaml_snapshot!(subgraph_request_counters, @r"
products: 2
reviews: 2
");
// Needed because insert in the cache is async
tokio::time::sleep(Duration::from_millis(100)).await;
let (headers, body) = makegraphql_request_with_headers(&mut router, headers.clone()).await;
assert!(headers["cache-control"].contains("private"));
insta::assert_yaml_snapshot!(body, @r"
data:
topProducts:
- reviews:
- id: r1a
- id: r1b
- reviews:
- id: r2
");
// Unchanged, everything is in cache so we don’t need to make more subgraph requests:
insta::assert_yaml_snapshot!(subgraph_request_counters, @r"
products: 2
reviews: 2
");
}

fn check_cache_tags(response: &graphql::Response, cache_tags: Vec<Vec<String>>) {
let mut debugger_entries = response
.extensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@

For basic response caching setup, see the [Quickstart](/router/performance/caching/response-caching/quickstart) page.

<Note>
Response caching is happening at the subgraph service level and this plugin is happening right after coprocessor and Rhai plugins which means you'll always call the coprocessor or Rhai script even if the response is cached (both at request and response level).

Check warning on line 23 in docs/source/routing/performance/caching/response-caching/customization.mdx

View check run for this annotation

Apollo Librarian / AI Style Review

docs/source/routing/performance/caching/response-caching/customization.mdx#L23

Use the simple present tense ("operates", "runs") instead of present continuous ("is happening"). Split the run-on sentence for clarity. Use active voice and attribute the action to the router ("the router executes") rather than the user ("you'll always call"). ```suggestion Response caching operates at the subgraph service level. This plugin runs after coprocessor and Rhai plugins, so the router executes coprocessors and Rhai scripts even if the response is cached. ```
</Note>

## Private data caching

A subgraph can return a response with the header `Cache-Control: private`, indicating that it contains user-personalized data. Although this usually forbids intermediate servers from storing data, the router can recognize different users and store their data in different parts of the cache.
Expand Down