CDN cache-control headers#1138
Conversation
| Object.keys(responseInit.headers).forEach(key => | ||
| response.header(key, responseInit.headers[key]), | ||
| ); | ||
| response.header( |
There was a problem hiding this comment.
The express variant contained this, so I thought that the hapi and koa variants should be at feature parity. The reason it's not inside of apollo-server-core is because of the cloudflare integration. We can move the calculation there and then decide how to do the Buffer length in a V8 environment later
| ctx.set(key, responseInit.headers[key]), | ||
| ); | ||
| ctx.set( | ||
| 'Content-Length', |
|
|
||
| return JSON.stringify(responses); | ||
| return { | ||
| gqlResponse: JSON.stringify(responses), |
There was a problem hiding this comment.
I think this might be better as graphqlResponse — do we really use "gql" much in actual APIs outside of graphql-tag?
| const responses = await Promise.all(requests); | ||
|
|
||
| const responseInit: ResponseInit = { | ||
| status: 200, |
There was a problem hiding this comment.
Seems like this isn't getting consistently used in all variants (even express lacks it).
|
|
||
| export interface HttpQueryResponse { | ||
| gqlResponse: string; | ||
| responseInit: ResponseInit; |
There was a problem hiding this comment.
The typing here is a little funky, since ResponseInit.headers is a HeadersInit which allows various options (Headers object, array of arrays, dictionary object) but the consumers assume that it's the latter. I'm almost surprised this typechecks, though maybe TS lets you [] index anything? Not sure what to do about this — maybe just roll your own type, or at least comment on it you can assume that the headers field is a record?
There was a problem hiding this comment.
Makes sense, we'll roll our own for now
18c1b61 to
cf75381
Compare
47a21d3 to
772d8a6
Compare
…through extensions
fc85b04 to
6890156
Compare
| get(key: string): Promise<string | null>; | ||
| } | ||
|
|
||
| export function calcualteCacheControlHeaders( |
| let maxAge = Number.MAX_VALUE; | ||
| let publicOrPrivate = 'public'; | ||
|
|
||
| for (let i = 0; i < responses.length; i++) { |
There was a problem hiding this comment.
So the key thing in the engineproxy calculation of cacheControl is you need to put a hint on every top-level data piece in order for it to count. See the rootDataKey loop in parseCacheControl.
| }); | ||
| describe('file uploads', () => { | ||
| it('enabled uploads', async () => { | ||
| //XXX This is currently a failing test for node 10 |
There was a problem hiding this comment.
Would you like to pair on tracking this down? Node 10 is released so it seems like we should make this work.
| let maxAge = Number.MAX_VALUE; | ||
| let publicOrPrivate = 'public'; | ||
|
|
||
| //Because of the early exit, we are unable to use forEach. While a reduce |
There was a problem hiding this comment.
I am hesitant to mention any formatting issues that prettier doesn't deal with automatically, but I've been meaning to mention that your style of not putting a space after // seems to be idiosyncratic in our codebases.
| } | ||
|
|
||
| const rootHints = new Set<string>(); | ||
| for (let y = 0; y < cacheControl.hints.length; y++) { |
There was a problem hiding this comment.
grab cacheControl.hints[y] immediately like you did in the outer loop? (Also a little odd that the loop indices are i/y.)
|
|
||
| const rootHints = new Set<string>(); | ||
| for (let y = 0; y < cacheControl.hints.length; y++) { | ||
| if (cacheControl.hints[y].scope === 'PRIVATE') { |
There was a problem hiding this comment.
In Engineproxy we do this comparison case-insensitively.
| maxAge = cacheControl.hints[y].maxAge; | ||
| } | ||
|
|
||
| rootHints.add(cacheControl.hints[y].path[0]); |
There was a problem hiding this comment.
This is not quite right (or at least, not quite what we did before). This would mean that if you got a hint for "foo.bar" but nothing else, we would count this as being a hint on "foo". But if there's also a "foo.baz" and that was the only hint, then there are no hints on "foo.baz", so it wouldn't be right to count this as covering everything.
(Arguably we could change the "all roots must have hints" logic to be something more like "every leaf must be the descendent of a node with a hint" although that wouldn't be quite right either, as even interior nodes have resolvers whose return values can change.)
| //we always want cacheControl to either set the CDN headers or for the | ||
| //engine proxy | ||
| cacheControl: | ||
| optionsObject.cacheControl !== null && |
There was a problem hiding this comment.
hmm, why not just optionsObject.cacheControl === undefined ? true : optionsObject.cacheControl? Looks like this will override explicitly cacheControl: false which seems wrong.
| if (!optionsObject.cacheControl) { | ||
| responseInit.headers = { | ||
| ...responseInit.headers, | ||
| ...calculateCacheControlHeaders(responses), |
There was a problem hiding this comment.
Shouldn't we do this even when you explicitly ask for cacheControl? Perhaps there is a second option to cacheControl, eg cacheControl.setHTTPHeaders, which default to true.
| ...calculateCacheControlHeaders(responses), | ||
| }; | ||
|
|
||
| //remove cacheControl headers. This could be done in production only, |
There was a problem hiding this comment.
It's extensions, not headers (so fix the comment).
However this all seems to be weirdly intricate. We create the CacheControlExtension object and ask it to put things in the JSON extension format, but then we turn it into headers and delete the extension. Why not just put header calculation (or perhaps, "max age and privacy tuple" calculation) into CacheControlExtension itself, and (via an option to the CacheControlExtension constructor that can be overridden) make format into a no-op?
Adds cache-control toggles for http header calculation and stripping out the cache control extensions from the respose. Brings the default calculation of headers in line with the proxy.
| With ENGINE_API_KEY set as an environment variable, Apollo Server creates a reporting agent that sends execution traces to the Engine UI. In addition by default, Apollo Server supports [persisted queries](./features/apq.html). | ||
|
|
||
| <!-- FIXME add something about CDN headers--> | ||
| Apollo Server 2 is able to completely replace the Engine Proxy. To enable metrics reporting, add `ENGINE_API_KEY` as an environment variable. Apollo Server will then create a reporting agent that sends execution traces to the Engine UI. In addition by default, Apollo Server supports [persisted queries](./features/apq.html) without needing the proxy's cache. Apollo Server also provides cache-control headers for consumption by a [CDN](./features/cdn.html). Integration with a CDN provides a replacement for the full response caching in Engine Proxy. |
There was a problem hiding this comment.
I don't think that last sentence really is true. After all that was already a feature of the Proxy.
| ``` | ||
|
|
||
| ## Starting Engine Proxy | ||
| ## Starting Engine Proxy as a Sidecar |
There was a problem hiding this comment.
I think we moved away from the "sidecar" terminology because it never made sense to anybody
| tracing: optionsObject.tracing, | ||
| cacheControl: optionsObject.cacheControl, | ||
| cacheControl: cacheControl | ||
| ? { defaultMaxAge: cacheControl.defaultMaxAge } |
There was a problem hiding this comment.
Is the implication that this is the only parameter that the underlying package supports? maybe use _.omit or whatever to blacklist instead of whitelist?
In order to se the cache-control headers inside of GraphQL execution, we need to return Response Headers. Currently
runHttpQuerymanufactures them from scratch. It could instead accept in a responseInit object and append it. However, none of the integrations currently use this functionality and it could be added in later, so to keep things simple, this PR takes the most direct route.