ardatan / graphql-mesh

🕸️ GraphQL Mesh - The Graph of Everything - Federated architecture for any API service

Home Page:https://the-guild.dev/graphql/mesh

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

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
       })
     },
   }
 }