HTTP cache broken if response includes multiple headers of the same name
FGoessler opened this issue · comments
Issue workflow progress
Progress of the issue based on the
Contributor Workflow
- 1. The issue provides a reproduction available on
Github,
Stackblitz
or
CodeSandbox
Make sure to fork this template and run
yarn generate
in the terminal.Please make sure Mesh package versions under
package.json
matches yours.
- 2. A failing test has been provided
- 3. A local solution has been provided
- 4. A pull request is pending review
Describe the bug
The @graphql-mesh/plugin-http-cache
plugin crashes when it receives a response with multiple HTTP headers of the same name.
For example this can happen if the downstream servers adds multiple set-cookie
headers. Afaik for a HTTP request to have multiple headers of the same name is actually normal and standard. But either the http-cache plugin or its underlying networking library seem to have issues in this case.
Codesandbox for reproduction: https://codesandbox.io/p/devbox/rough-field-cg64g3
To Reproduce
- Add the http-cache plugin to your plugin list in .meshrc.yml
- Have a source configured that talks to a downstream HTTP based API (e.g. JSON schema based)
- Ensure that API response includes multiple headers of the same name (e.g.
set-cookie
) - Use any cache backend (file, redis, ... doesn't matter)
- Perform the GraphQL query
Expected behavior
- The query returns the data without error and puts it into the cache.
Actual behavior
- The query returns an error:
{
"data": {
"contentItem": null
},
"errors": [
{
"message": "'ownKeys' on proxy: trap returned duplicate entries",
"path": [
"contentItem"
]
}
]
}
Note: Despite the failure the requested item is usually put into the cache as it seems. So if the same query is executed again the cache can resolve and return it correctly.
Environment:
- OS: macOS 13.2
@graphql-mesh/...
:
"@graphql-mesh/cache-file": "^0.96.5",
"@graphql-mesh/cli": "0.88.7",
"@graphql-mesh/json-schema": "0.98.1",
"@graphql-mesh/plugin-http-cache": "^0.96.5",
- NodeJS: v18.19
Additional context
Example curl show casing a duplicate set-cookie
header:
url --location 'https://api.blinkist.com/content/item/book/5ddd524d6cee0700085f3207' -D -
HTTP/2 200
date: Tue, 13 Feb 2024 20:23:14 GMT
content-type: application/json; charset=utf-8
content-length: 3275
cf-ray: 854fc9be19c135d6-WAW
cf-cache-status: HIT
accept-ranges: bytes
age: 10269
cache-control: public, max-age=28800
expires: Wed, 14 Feb 2024 04:23:14 GMT
last-modified: Tue, 13 Feb 2024 17:32:05 GMT
strict-transport-security: max-age=31536000; includeSubDomains
access-control-allow-methods: GET
x-blinkist-cache-status: HIT
set-cookie: __cf_bm=f9Uxz.0xseR8VhhJPIqxN_k6VvqR2Bhi.Wcy6YqxJpE-1707855794-1-AWz9YLz3vTOJYJCiHonCprtqEFr6vh4LdSeBHwk/SbRgc2xXdxz4OF8B1nez1qTxzhZ3TimMxhUUX7WI5Pq9pFPBm8Thl0QPfZQuynHjrYYq; path=/; expires=Tue, 13-Feb-24 20:53:14 GMT; domain=.blinkist.com; HttpOnly; Secure; SameSite=None
set-cookie: _cfuvid=Db9IVCLjtF9KP.rX31v2iwG75.kfzoRWJA2RxnhFxPI-1707855794950-0-604800000; path=/; domain=.blinkist.com; HttpOnly; Secure; SameSite=None
server: cloudflare
alt-svc: h3=":443"; ma=86400
...
Example GraphQL query for the reproduction playground:
query ExampleQuery {
contentItem(id: "5ddd524d6cee0700085f3207") {
data {
creator_name
language
title
}
}
}
We actually found a workaround for our particular use case by creating a custom plugin that wraps the fetch
function and removes the problematic headers. Possible in our case as we do not care about the set-cookie
header.
The plugin code:
import { MeshPlugin } from "@graphql-mesh/types"
import { MeshContext } from "@graphql-mesh/runtime"
/**
* This plugin is a workaround for a bug in the cache plugin of graphql-mesh.
* See: https://github.com/ardatan/graphql-mesh/issues/6570
*
* The issue is that it crashes when the response contains multiple headers with the same name.
* The error then is: "'ownKeys' on proxy: trap returned duplicate entries".
* Despite the failure the requested item is usually put into the cache. So if the same query is executed again the
* cache can resolve and return it correctly. Making this issue harder to detect and debug!
* In our case the "duplicated" header is caused by Cloudflare adding multiple "set-cookie" headers to the response.
* As this API does not care about cookies the workaround is to drop the "set-cookie" header from the response.
*/
export function cacheFixingPlugin(): MeshPlugin<MeshContext> {
return {
onFetch: async (payload) => {
// Here we inject a custom fetch function that removes the "set-cookie" header from the response.
payload.setFetchFn(async (url, options, context, info) => {
const res = await payload.fetchFn(url, options, context, info)
res.headers.delete("set-cookie")
// Weirdly just deleting (or also setting the header to an empty string) does not work.
// We need to change the headers of the response in some other way too for it to actually take the desired effect.
res.headers.set("x-cache-fix", "1")
return res
})
},
}
}