diff --git a/.changeset/@graphql-yoga_nestjs-3188-dependencies.md b/.changeset/@graphql-yoga_nestjs-3188-dependencies.md new file mode 100644 index 0000000000..50512d0477 --- /dev/null +++ b/.changeset/@graphql-yoga_nestjs-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/nestjs': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_nestjs-federation-3188-dependencies.md b/.changeset/@graphql-yoga_nestjs-federation-3188-dependencies.md new file mode 100644 index 0000000000..bd4b67e984 --- /dev/null +++ b/.changeset/@graphql-yoga_nestjs-federation-3188-dependencies.md @@ -0,0 +1,10 @@ +--- +'@graphql-yoga/nestjs-federation': patch +--- +dependencies updates: + - Updated dependency [`@graphql-yoga/nestjs@3.2.0` + ↗︎](https://www.npmjs.com/package/@graphql-yoga/nestjs/v/3.2.0) (from `3.1.1`, in + `dependencies`) + - Updated dependency [`@graphql-yoga/plugin-apollo-inline-trace@3.2.0` + ↗︎](https://www.npmjs.com/package/@graphql-yoga/plugin-apollo-inline-trace/v/3.2.0) (from + `3.1.1`, in `dependencies`) diff --git a/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3188-dependencies.md new file mode 100644 index 0000000000..d3b11eeb2a --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-apollo-inline-trace-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-apollo-inline-trace': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-apq-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-apq-3188-dependencies.md new file mode 100644 index 0000000000..53945ad8c2 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-apq-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-apq': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-csrf-prevention-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-csrf-prevention-3188-dependencies.md new file mode 100644 index 0000000000..05bb820b0a --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-csrf-prevention-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-csrf-prevention': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-defer-stream-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-defer-stream-3188-dependencies.md new file mode 100644 index 0000000000..df6c185dc9 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-defer-stream-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-defer-stream': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-disable-introspection-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-disable-introspection-3188-dependencies.md new file mode 100644 index 0000000000..bdabf46d40 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-disable-introspection-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-disable-introspection': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-graphql-sse-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-graphql-sse-3188-dependencies.md new file mode 100644 index 0000000000..8fe0ef5d96 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-graphql-sse-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-graphql-sse': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-jwt-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-jwt-3188-dependencies.md new file mode 100644 index 0000000000..293232dd2c --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-jwt-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-jwt': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-persisted-operations-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-persisted-operations-3188-dependencies.md new file mode 100644 index 0000000000..5babc495c5 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-persisted-operations-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-persisted-operations': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-prometheus-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-prometheus-3188-dependencies.md new file mode 100644 index 0000000000..25ce773e83 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-prometheus-3188-dependencies.md @@ -0,0 +1,9 @@ +--- +'@graphql-yoga/plugin-prometheus': patch +--- +dependencies updates: + - Updated dependency [`@envelop/prometheus@^9.4.0` + ↗︎](https://www.npmjs.com/package/@envelop/prometheus/v/9.4.0) (from `^9.3.1`, in + `dependencies`) + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-response-cache-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-response-cache-3188-dependencies.md new file mode 100644 index 0000000000..64d3da3748 --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-response-cache-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-response-cache': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_plugin-sofa-3188-dependencies.md b/.changeset/@graphql-yoga_plugin-sofa-3188-dependencies.md new file mode 100644 index 0000000000..53d113ebaa --- /dev/null +++ b/.changeset/@graphql-yoga_plugin-sofa-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/plugin-sofa': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/@graphql-yoga_render-graphiql-3188-dependencies.md b/.changeset/@graphql-yoga_render-graphiql-3188-dependencies.md new file mode 100644 index 0000000000..38e59e6f42 --- /dev/null +++ b/.changeset/@graphql-yoga_render-graphiql-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'@graphql-yoga/render-graphiql': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `peerDependencies`) diff --git a/.changeset/breezy-shirts-run.md b/.changeset/breezy-shirts-run.md new file mode 100644 index 0000000000..ff6a836f7f --- /dev/null +++ b/.changeset/breezy-shirts-run.md @@ -0,0 +1,5 @@ +--- +'graphql-yoga': minor +--- + +Add `allowedHeaders` option to allow filtering Response and Request headers diff --git a/.changeset/graphql-yoga-3188-dependencies.md b/.changeset/graphql-yoga-3188-dependencies.md new file mode 100644 index 0000000000..7f18d957d1 --- /dev/null +++ b/.changeset/graphql-yoga-3188-dependencies.md @@ -0,0 +1,10 @@ +--- +'graphql-yoga': patch +--- +dependencies updates: + - Updated dependency [`@graphql-tools/executor@^1.2.2` + ↗︎](https://www.npmjs.com/package/@graphql-tools/executor/v/1.2.2) (from `^1.0.0`, in + `dependencies`) + - Updated dependency [`@graphql-tools/utils@^10.1.0` + ↗︎](https://www.npmjs.com/package/@graphql-tools/utils/v/10.1.0) (from `^10.0.0`, in + `dependencies`) diff --git a/.changeset/graphql-yoga-cloud-run-guide-3188-dependencies.md b/.changeset/graphql-yoga-cloud-run-guide-3188-dependencies.md new file mode 100644 index 0000000000..cf372bfd58 --- /dev/null +++ b/.changeset/graphql-yoga-cloud-run-guide-3188-dependencies.md @@ -0,0 +1,6 @@ +--- +'graphql-yoga-cloud-run-guide': patch +--- +dependencies updates: + - Updated dependency [`graphql-yoga@^5.2.0` + ↗︎](https://www.npmjs.com/package/graphql-yoga/v/5.2.0) (from `^5.1.1`, in `dependencies`) diff --git a/packages/graphql-yoga/src/plugins/allowed-headers.spec.ts b/packages/graphql-yoga/src/plugins/allowed-headers.spec.ts new file mode 100644 index 0000000000..223d899b21 --- /dev/null +++ b/packages/graphql-yoga/src/plugins/allowed-headers.spec.ts @@ -0,0 +1,64 @@ +import { createSchema } from '../schema'; +import { createYoga } from '../server'; +import { useAllowedResponseHeaders } from './allowed-headers'; +import { Plugin } from './types'; + +describe('useAllowedHeaders', () => { + it('should strip headers from responses', async () => { + const response = await query({ + plugins: [ + useAllowedResponseHeaders(['content-type', 'content-length', 'x-allowed-custom-header']), + ], + responseHeaders: { + 'x-allowed-custom-header': 'value', + // Verify that we can strip 2 headers in a row + 'x-disallowed-custom-header-1': 'value', + 'x-disallowed-custom-header-2': 'value', + }, + }); + + expect(response.headers.get('x-allowed-custom-header')).toEqual('value'); + expect(response.headers.get('x-disallowed-custom-header-1')).toBeNull(); + expect(response.headers.get('x-disallowed-custom-header-2')).toBeNull(); + }); + + const schema = createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + _: String + } + `, + }); + + function query({ + responseHeaders = {}, + requestHeaders = {}, + plugins = [], + }: { + requestHeaders?: Record; + responseHeaders?: Record; + plugins?: Plugin[]; + } = {}) { + const yoga = createYoga({ + schema, + plugins: [ + { + onResponse: ({ response }) => { + for (const [header, value] of Object.entries(responseHeaders)) { + response.headers.set(header, value); + } + }, + }, + ...plugins, + ], + }); + return yoga.fetch('/graphql', { + body: JSON.stringify({ query: '{ __typename }' }), + method: 'POST', + headers: { + 'content-type': 'application/json', + ...requestHeaders, + }, + }); + } +}); diff --git a/packages/graphql-yoga/src/plugins/allowed-headers.ts b/packages/graphql-yoga/src/plugins/allowed-headers.ts new file mode 100644 index 0000000000..d2f222dc24 --- /dev/null +++ b/packages/graphql-yoga/src/plugins/allowed-headers.ts @@ -0,0 +1,25 @@ +import { Plugin } from './types.js'; + +export function useAllowedResponseHeaders(allowedHeaders: string[]): Plugin { + return { + onResponse({ response }) { + removeDisallowedHeaders(response.headers, allowedHeaders); + }, + }; +} + +export function useAllowedRequestHeaders(allowedHeaders: string[]): Plugin { + return { + onRequest({ request }) { + removeDisallowedHeaders(request.headers, allowedHeaders); + }, + }; +} + +function removeDisallowedHeaders(headers: Headers, allowedHeaders: string[]) { + for (const headerName of headers.keys()) { + if (!allowedHeaders.includes(headerName)) { + headers.delete(headerName); + } + } +} diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index 031e988624..ac720ea408 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -20,6 +20,7 @@ import { useErrorHandling, } from '@whatwg-node/server'; import { handleError } from './error.js'; +import { useAllowedRequestHeaders, useAllowedResponseHeaders } from './plugins/allowed-headers.js'; import { isGETRequest, parseGETRequest } from './plugins/request-parser/get.js'; import { isPOSTFormUrlEncodedRequest, @@ -161,6 +162,16 @@ export type YogaServerOptions = { * @default false */ batching?: BatchingOptions | undefined; + + /** + * Allowed headers. Headers not part of this list will be striped out. + */ + allowedHeaders?: { + /** Allowed headers for outgoing responses */ + response?: string[] | undefined; + /** Allowed headers for ingoing requests */ + request?: string[] | undefined; + }; }; export type BatchingOptions = @@ -275,7 +286,8 @@ export class YogaServer< }), // Use the schema provided by the user !!options?.schema && useSchema(options.schema), - + options?.allowedHeaders?.request != null && + useAllowedRequestHeaders(options.allowedHeaders.request), options?.context != null && useExtendContext(initialContext => { if (options?.context) { @@ -341,45 +353,28 @@ export class YogaServer< }), ...(options?.plugins ?? []), - // To make sure those are called at the end - { - onPluginInit({ addPlugin }) { - if (options?.parserAndValidationCache !== false) { - addPlugin( - // @ts-expect-error Add plugins has context but this hook doesn't care - useParserAndValidationCache( - !options?.parserAndValidationCache || options?.parserAndValidationCache === true - ? {} - : options?.parserAndValidationCache, - ), - ); - } - // @ts-expect-error Add plugins has context but this hook doesn't care - addPlugin(useLimitBatching(batchingLimit)); - // @ts-expect-error Add plugins has context but this hook doesn't care - addPlugin(useCheckGraphQLQueryParams()); - addPlugin( - // @ts-expect-error Add plugins has context but this hook doesn't care - useUnhandledRoute({ - graphqlEndpoint, - showLandingPage: options?.landingPage ?? true, - }), - ); - // We check the method after user-land plugins because the plugin might support more methods (like graphql-sse). - // @ts-expect-error Add plugins has context but this hook doesn't care - addPlugin(useCheckMethodForGraphQL()); - // We make sure that the user doesn't send a mutation with GET - // @ts-expect-error Add plugins has context but this hook doesn't care - addPlugin(usePreventMutationViaGET()); - if (maskedErrors) { - addPlugin(useMaskedErrors(maskedErrors)); - } - addPlugin( - // We handle validation errors at the end - useHTTPValidationError(), - ); - }, - }, + + options?.parserAndValidationCache !== false && + useParserAndValidationCache( + !options?.parserAndValidationCache || options?.parserAndValidationCache === true + ? {} + : options?.parserAndValidationCache, + ), + useLimitBatching(batchingLimit), + useCheckGraphQLQueryParams(), + useUnhandledRoute({ + graphqlEndpoint, + showLandingPage: options?.landingPage ?? true, + }), + // We check the method after user-land plugins because the plugin might support more methods (like graphql-sse). + useCheckMethodForGraphQL(), + // We make sure that the user doesn't send a mutation with GET + usePreventMutationViaGET(), + maskedErrors !== null && useMaskedErrors(maskedErrors), + options?.allowedHeaders?.response != null && + useAllowedResponseHeaders(options.allowedHeaders.response), + // We handle validation errors at the end + useHTTPValidationError(), ]; this.getEnveloped = envelop({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55d7725040..5f2b2363d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1463,7 +1463,7 @@ importers: version: 1.0.0(@apollo/client@3.7.15)(graphql@16.6.0) '@graphql-tools/executor-http': specifier: ^1.0.4 - version: 1.0.4(@types/node@18.16.16)(graphql@16.6.0) + version: 1.0.4(graphql@16.6.0) graphql: specifier: ^15.2.0 || ^16.0.0 version: 16.6.0 @@ -1483,7 +1483,7 @@ importers: dependencies: '@graphql-tools/executor-http': specifier: ^1.0.4 - version: 1.0.4(@types/node@18.16.16)(graphql@16.6.0) + version: 1.0.4(graphql@16.6.0) '@graphql-tools/executor-urql-exchange': specifier: ^1.0.0 version: 1.0.0(@urql/core@4.0.10)(graphql@16.6.0)(wonka@6.3.2) @@ -1844,7 +1844,7 @@ importers: devDependencies: '@graphql-tools/executor-http': specifier: ^1.0.4 - version: 1.0.4(@types/node@18.16.16)(graphql@16.6.0) + version: 1.0.4(graphql@16.6.0) '@whatwg-node/fetch': specifier: ^0.9.7 version: 0.9.12 @@ -7970,7 +7970,7 @@ packages: - bufferutil - utf-8-validate - /@graphql-tools/executor-http@1.0.4(@types/node@18.16.16)(graphql@16.6.0): + /@graphql-tools/executor-http@1.0.4(graphql@16.6.0): resolution: {integrity: sha512-lSoPFWrGU6XT9nGGBogUI8bSOtP0yce2FhXTrU5akMZ35BDCNWbkmgryzRhxoAH/yDOaZtKkHQB3xrYX3uo5zA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -8086,7 +8086,7 @@ packages: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/executor-http': 1.0.4(@types/node@18.16.16)(graphql@16.6.0) + '@graphql-tools/executor-http': 1.0.5(@types/node@18.16.16)(graphql@16.6.0) '@graphql-tools/graphql-tag-pluck': 8.0.0(@babel/core@7.22.1)(graphql@16.6.0) '@graphql-tools/utils': 10.0.1(graphql@16.6.0) '@whatwg-node/fetch': 0.9.12 @@ -8315,7 +8315,7 @@ packages: '@ardatan/sync-fetch': 0.0.1 '@graphql-tools/delegate': 10.0.0(graphql@16.6.0) '@graphql-tools/executor-graphql-ws': 1.0.0(graphql@16.6.0) - '@graphql-tools/executor-http': 1.0.4(@types/node@18.16.16)(graphql@16.6.0) + '@graphql-tools/executor-http': 1.0.5(@types/node@18.16.16)(graphql@16.6.0) '@graphql-tools/executor-legacy-ws': 1.0.0(graphql@16.6.0) '@graphql-tools/utils': 10.0.1(graphql@16.6.0) '@graphql-tools/wrap': 10.0.0(graphql@16.6.0)