-
Notifications
You must be signed in to change notification settings - Fork 226
fix: prevent set from being forwarded to client #2686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
79ae5b1
b154044
874bce1
f886c7a
6c6b23a
18489eb
098f642
4f2529e
2afca15
25fa12d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -130,10 +130,10 @@ The algorithm evaluates the following in order: | |||||
|
|
||||||
| By combining these mechanisms, the algorithm ensures that data handling adheres to the strictest cache control settings from all subgraph responses, promoting both security and performance integrity. Users can define global defaults to enforce a baseline cache policy, and can rely on `no-cache` or `no-store` directives for security sensitive subgraphs. | ||||||
|
|
||||||
| ### Overriding Cache Control Policy | ||||||
| ### Influencing Cache Control via Set Rules | ||||||
|
|
||||||
| <Info> | ||||||
| By using the `set` operation in their header propagation rules, users can overwrite the cache control policy if necessary. | ||||||
| By using the `set` operation in their header propagation rules, users can inject a `Cache-Control` value into a subgraph's response. The restrictive algorithm then includes this value when computing the most restrictive policy across all subgraph responses. | ||||||
| </Info> | ||||||
|
|
||||||
| For example, a configuration can be set like: | ||||||
|
|
@@ -152,4 +152,23 @@ headers: | |||||
| value: "max-age=5400" | ||||||
| ``` | ||||||
|
|
||||||
| For this configuration, any request which hits the `specific-subgraph` will have the desired subgraph cache control value set (`max-age=5400`). | ||||||
| For this configuration, any request which hits the `specific-subgraph` will have `Cache-Control: max-age=5400` injected into the subgraph response. The restrictive cache control algorithm then considers this value alongside other subgraph responses to compute the final `Cache-Control` header sent to the client. | ||||||
|
|
||||||
| This is however equivalent to the following | ||||||
|
|
||||||
| ``` | ||||||
| cache_control_policy: | ||||||
| enabled: true | ||||||
| value: "max-age=180, public" | ||||||
| subgraphs: | ||||||
| - name: "specific-subgraph" | ||||||
| value: "max-age=5400, public" | ||||||
| ``` | ||||||
|
|
||||||
| <Warning> | ||||||
| As such we recommend not using this method and simply sticking to using the `subgraph` configuration in `cache_control_policy`, as it makes things more explicit and clearer. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| </Warning> | ||||||
|
|
||||||
| <Note> | ||||||
| Until router version VERSION, setting a `Cache-Control` header would do a hard override of whatever value would be computed by the `cache_control_policy` configuration. This has been changed since. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. VERSION? how do we chicken-egg this with the release now that docs are in tree?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do a release -> then update and merge since its only my pr ideally -> and then merge
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean releasing from the PR branch? |
||||||
| </Note> | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -53,10 +53,6 @@ headers: | |||||
| default: "123" # Set the value when the header was not set | ||||||
| algorithm: "last_write" | ||||||
|
|
||||||
| - op: "set" | ||||||
| name: "X-Custom-Header" | ||||||
| value: "my-required-key" | ||||||
|
|
||||||
| subgraphs: | ||||||
| specific-subgraph: # Will only affect this subgraph | ||||||
| response: | ||||||
|
|
@@ -68,7 +64,7 @@ headers: | |||||
|
|
||||||
| ### What does the snippet do? | ||||||
|
|
||||||
| With `all` we address all subgraph requests. Next, we can define several rules on the client's request. The operation `propagate` forwards all matching client request headers to the subgraphs. The operation `set` sets a new header which is forward to the subgraphs. | ||||||
| With `all` we address all subgraph responses. Next, we can define several rules on the response headers. The operation `propagate` forwards matching subgraph response headers to the client. The operation `set` injects a header value into the subgraph response — for `Cache-Control`, this value is picked up by the cache control algorithm; for other headers, the value is not forwarded to the client unless a separate `propagate` rule matches it. | ||||||
|
|
||||||
| The `subgraphs` section allows to propagate headers for specific subgraphs. The name must match with the subgraph name in the Studio. | ||||||
|
|
||||||
|
|
@@ -90,12 +86,59 @@ Currently, we support the following header rules: | |||||
|
|
||||||
| * `default` - Fallback to this value when the `named`, `matching` or `rename` header could not be found. | ||||||
|
|
||||||
| * `set` - Sets a header on the request forward to the subgraph. You must set the following values: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the page still flow properly after this change? We still have |
||||||
| <Note> | ||||||
| Go canonicalizes headers by default e.g. `x-my-header` to `X-My-Header.` Write your rule accordingly or use `(?i)``^X-Test-.*` flags to make your regex case insensitive. | ||||||
| </Note> | ||||||
|
|
||||||
| ## Order of Execution | ||||||
|
|
||||||
| Response header rules are applied per subgraph fetch in the following order: | ||||||
|
|
||||||
| 1. **`all` rules** — Rules defined under `headers.all.response` are applied first, in the order they are defined. | ||||||
| 2. **Subgraph-specific rules** — Rules defined under `headers.subgraphs.<name>.response` are applied next, in the order they are defined. | ||||||
| 3. **Cache control policy** — The `cache_control_policy` rules run last. This ensures that all `set` rules (both global and subgraph-specific) have already injected their values into the subgraph response before the [restrictive cache control algorithm](/router/proxy-capabilities/adjusting-cache-control) reads them. | ||||||
|
|
||||||
| * `name` - The name of the header to set | ||||||
| Within each scope, rules execute in definition order. This means a `set` rule defined before a `propagate` rule in the same scope will inject the value into the subgraph response before the `propagate` rule evaluates it. | ||||||
|
|
||||||
| * `value` - The value to set for the header | ||||||
| ## Response Header Set | ||||||
|
|
||||||
| The `set` operation injects a header value into the subgraph response, making it appear as if the subgraph returned it. This value is then processed by downstream rules (e.g., `propagate` rules or the [restrictive cache control algorithm](/router/proxy-capabilities/adjusting-cache-control)). | ||||||
|
|
||||||
| <Note> | ||||||
| Go canonicalizes headers by default e.g. `x-my-header` to `X-My-Header.` Write your rule accordingly or use `(?i)``^X-Test-.*` flags to make your regex case insensitive. | ||||||
| The `set` value is **not** forwarded to the client response. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| </Note> | ||||||
|
|
||||||
|
|
||||||
| <Warning> | ||||||
| You can use `set` to set a header value and then use a `propagate` rule to forward that same header to the client. However, we would caution against this, as the header will not be propagated if no subgraph was actually called. | ||||||
| </Warning> | ||||||
|
Comment on lines
+112
to
+114
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we merge all this into one block? It's kinda whiplash to read "is not forwarded**" -> "do xyz to forward it" |
||||||
|
|
||||||
| ### Configuration | ||||||
|
|
||||||
| * `name` - The name of the header to set | ||||||
| * `value` - The value to set for the header | ||||||
|
|
||||||
| ### Example: Setting Cache Control on a Subgraph Response | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we can replace this with a use case |
||||||
|
|
||||||
| ```yaml | ||||||
| cache_control_policy: | ||||||
| enabled: true | ||||||
| value: "max-age=180, public" | ||||||
|
|
||||||
| headers: | ||||||
| subgraphs: | ||||||
| specific-subgraph: | ||||||
| response: | ||||||
| - op: "set" | ||||||
| name: "Cache-Control" | ||||||
| value: "max-age=5400" | ||||||
| ``` | ||||||
|
|
||||||
| In this example, when a request hits `specific-subgraph`, the `Cache-Control: max-age=5400` value is injected into the subgraph response. The restrictive cache control algorithm then processes this value alongside other subgraph responses to compute the final `Cache-Control` header sent to the client. This is equivalent to doing | ||||||
|
|
||||||
| ``` | ||||||
| cache_control_policy: | ||||||
| enabled: true | ||||||
| value: "max-age=180, public" | ||||||
|
|
||||||
| ``` | ||||||
|
Comment on lines
+116
to
+144
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't want people to do this, perhaps we should not include so many searchable examples of doing it in the docs |
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -958,7 +958,7 @@ func TestSingleFlight(t *testing.T) { | |
| } | ||
| }) | ||
| }) | ||
| t.Run("response header set rule with singleflight followers", func(t *testing.T) { | ||
| t.Run("response header set rule with singleflight followers is internal only", func(t *testing.T) { | ||
| t.Parallel() | ||
| testenv.Run(t, &testenv.Config{ | ||
| Subgraphs: testenv.SubgraphsConfig{ | ||
|
|
@@ -985,8 +985,8 @@ func TestSingleFlight(t *testing.T) { | |
| responses := runConcurrentSingleflightRequests(t, xEnv, `{ employee(id: 1) { id } }`, 5) | ||
| for i, res := range responses { | ||
| require.Equal(t, `{"data":{"employee":{"id":1}}}`, res.Body) | ||
| require.Equal(t, "test-value", res.Response.Header.Get("X-Custom-Header"), | ||
| "response %d missing X-Custom-Header", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Custom-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| } | ||
| }) | ||
| }) | ||
|
|
@@ -1028,7 +1028,7 @@ func TestSingleFlight(t *testing.T) { | |
| } | ||
| }) | ||
| }) | ||
| t.Run("multiple response set rules with singleflight followers", func(t *testing.T) { | ||
| t.Run("multiple response set rules with singleflight followers are internal only", func(t *testing.T) { | ||
| t.Parallel() | ||
| testenv.Run(t, &testenv.Config{ | ||
| Subgraphs: testenv.SubgraphsConfig{ | ||
|
|
@@ -1057,23 +1057,23 @@ func TestSingleFlight(t *testing.T) { | |
| }), | ||
| }, | ||
| }, func(t *testing.T, xEnv *testenv.Environment) { | ||
| // Verify single request works | ||
| // Verify single request works — set headers are internal only | ||
| res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{ | ||
| Query: `{ employee(id: 1) { id } }`, | ||
| }) | ||
| require.Equal(t, "value-a", res.Response.Header.Get("X-Header-A"), "single request should have X-Header-A") | ||
| require.Equal(t, "value-b", res.Response.Header.Get("X-Header-B"), "single request should have X-Header-B") | ||
| require.Equal(t, "", res.Response.Header.Get("X-Header-A"), "set response headers should not be forwarded to the client") | ||
| require.Equal(t, "", res.Response.Header.Get("X-Header-B"), "set response headers should not be forwarded to the client") | ||
|
|
||
| responses := runConcurrentSingleflightRequests(t, xEnv, `{ employee(id: 1) { id } }`, 5) | ||
| for i, res := range responses { | ||
| require.Equal(t, "value-a", res.Response.Header.Get("X-Header-A"), | ||
| "response %d missing X-Header-A", i) | ||
| require.Equal(t, "value-b", res.Response.Header.Get("X-Header-B"), | ||
| "response %d missing X-Header-B", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Header-A"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Header-B"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
|
Comment on lines
+1069
to
+1072
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| }) | ||
| }) | ||
| t.Run("multi-subgraph response header propagation with singleflight", func(t *testing.T) { | ||
| t.Run("multi-subgraph response set with singleflight is internal only", func(t *testing.T) { | ||
| t.Parallel() | ||
| testenv.Run(t, &testenv.Config{ | ||
| Subgraphs: testenv.SubgraphsConfig{ | ||
|
|
@@ -1103,12 +1103,12 @@ func TestSingleFlight(t *testing.T) { | |
| responses := runConcurrentSingleflightRequests(t, xEnv, query, 5) | ||
| for i, res := range responses { | ||
| require.Contains(t, res.Body, `"employee"`) | ||
| require.Equal(t, "multi-subgraph-value", res.Response.Header.Get("X-Custom-Header"), | ||
| "response %d missing X-Custom-Header from multi-subgraph query", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Custom-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| } | ||
| }) | ||
| }) | ||
| t.Run("subgraph-specific response header rule with singleflight", func(t *testing.T) { | ||
| t.Run("subgraph-specific response set rule with singleflight is internal only", func(t *testing.T) { | ||
| t.Parallel() | ||
| testenv.Run(t, &testenv.Config{ | ||
| Subgraphs: testenv.SubgraphsConfig{ | ||
|
|
@@ -1138,12 +1138,12 @@ func TestSingleFlight(t *testing.T) { | |
| responses := runConcurrentSingleflightRequests(t, xEnv, `{ employees { id } }`, 5) | ||
| for i, res := range responses { | ||
| require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body) | ||
| require.Equal(t, "employees-value", res.Response.Header.Get("X-Subgraph-Header"), | ||
| "response %d missing subgraph-specific X-Subgraph-Header", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Subgraph-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| } | ||
| }) | ||
| }) | ||
| t.Run("mixed global and subgraph-specific response header rules with singleflight", func(t *testing.T) { | ||
| t.Run("mixed global and subgraph-specific response set rules with singleflight are internal only", func(t *testing.T) { | ||
| t.Parallel() | ||
| testenv.Run(t, &testenv.Config{ | ||
| Subgraphs: testenv.SubgraphsConfig{ | ||
|
|
@@ -1193,12 +1193,12 @@ func TestSingleFlight(t *testing.T) { | |
| responses := runConcurrentSingleflightRequests(t, xEnv, query, 5) | ||
| for i, res := range responses { | ||
| require.Contains(t, res.Body, `"employee"`) | ||
| require.Equal(t, "global-value", res.Response.Header.Get("X-Global-Header"), | ||
| "response %d missing global X-Global-Header", i) | ||
| require.Equal(t, "employees-value", res.Response.Header.Get("X-Employees-Header"), | ||
| "response %d missing subgraph-specific X-Employees-Header", i) | ||
| require.Equal(t, "family-value", res.Response.Header.Get("X-Family-Header"), | ||
| "response %d missing subgraph-specific X-Family-Header", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Global-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Employees-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| require.Equal(t, "", res.Response.Header.Get("X-Family-Header"), | ||
| "response %d: set response headers should not be forwarded to the client", i) | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.