diff --git a/.changeset/rude-squids-accept.md b/.changeset/rude-squids-accept.md new file mode 100644 index 0000000000..fd8cac4712 --- /dev/null +++ b/.changeset/rude-squids-accept.md @@ -0,0 +1,5 @@ +--- +'graphql-yoga': patch +--- + +Improve error messages in case of `operatinName` related errors. diff --git a/packages/graphql-yoga/__tests__/graphql-http.spec.ts b/packages/graphql-yoga/__tests__/graphql-http.spec.ts index aa3be7f3a1..5da9daa0d8 100644 --- a/packages/graphql-yoga/__tests__/graphql-http.spec.ts +++ b/packages/graphql-yoga/__tests__/graphql-http.spec.ts @@ -15,7 +15,7 @@ for (const audit of serverAudits({ url: 'http://yoga/graphql', fetchFn: yoga.fetch, })) { - test(audit.name, async () => { + test(`[${audit.id}] ${audit.name}`, async () => { const result = await audit.fn(); if (result.status !== 'ok') { throw result.reason; diff --git a/packages/graphql-yoga/src/plugins/request-validation/use-check-graphql-query-params.ts b/packages/graphql-yoga/src/plugins/request-validation/use-check-graphql-query-params.ts index ece4169e61..59290c76c1 100644 --- a/packages/graphql-yoga/src/plugins/request-validation/use-check-graphql-query-params.ts +++ b/packages/graphql-yoga/src/plugins/request-validation/use-check-graphql-query-params.ts @@ -1,4 +1,5 @@ -import { createGraphQLError } from '@graphql-tools/utils'; +import { Kind, type DocumentNode } from 'graphql'; +import { createGraphQLError, isDocumentNode } from '@graphql-tools/utils'; import type { GraphQLParams } from '../../types.js'; import type { Plugin } from '../types.js'; @@ -18,6 +19,21 @@ export function assertInvalidParams( }, }); } + + if ( + 'operationName' in params && + typeof params.operationName !== 'string' && + params.operationName != null + ) { + throw createGraphQLError(`Invalid operation name in the request body.`, { + extensions: { + http: { + status: 400, + }, + }, + }); + } + for (const paramKey in params) { if ((params as Record)[paramKey] == null) { continue; @@ -133,11 +149,78 @@ export function isValidGraphQLParams(params: unknown): params is GraphQLParams { } } +export function checkOperationName( + operationName: string | undefined, + document: DocumentNode, +): void { + const operations = listOperations(document); + + if (operationName != null) { + for (const operation of operations) { + if (operation.name?.value === operationName) { + return; + } + } + + throw createGraphQLError( + `Could not determine what operation to execute. There is no operation "${operationName}" in the query.`, + { + extensions: { + http: { + spec: true, + status: 400, + }, + }, + }, + ); + } + + operations.next(); + // If there is no operation name, we should have only one operation + if (!operations.next().done) { + throw createGraphQLError( + `Could not determine what operation to execute. The query contains multiple operation, an operation name should be provided.`, + { + extensions: { + http: { + spec: true, + status: 400, + }, + }, + }, + ); + } +} + +export function isValidOperationName( + operationName: string | undefined, + document: DocumentNode, +): boolean { + try { + checkOperationName(operationName, document); + return true; + } catch { + return false; + } +} + export function useCheckGraphQLQueryParams(extraParamNames?: string[]): Plugin { return { onParams({ params }) { checkGraphQLQueryParams(params, extraParamNames); }, + onParse() { + return ({ result, context }) => { + // Run only if this is a Yoga request + // the `request` might be missing when using graphql-ws for example + // in which case throwing an error would abruptly close the socket + if (!context.request || !context.params || !isDocumentNode(result)) { + return; + } + + checkOperationName(context.params.operationName, result); + }; + }, }; } @@ -166,3 +249,11 @@ function extendedTypeof( function isObject(val: unknown): val is Record { return extendedTypeof(val) === 'object'; } + +function* listOperations(document: DocumentNode) { + for (const definition of document.definitions) { + if (definition.kind === Kind.OPERATION_DEFINITION) { + yield definition; + } + } +} diff --git a/packages/graphql-yoga/src/plugins/request-validation/use-prevent-mutation-via-get.ts b/packages/graphql-yoga/src/plugins/request-validation/use-prevent-mutation-via-get.ts index 45ceab7a6d..bb381e6671 100644 --- a/packages/graphql-yoga/src/plugins/request-validation/use-prevent-mutation-via-get.ts +++ b/packages/graphql-yoga/src/plugins/request-validation/use-prevent-mutation-via-get.ts @@ -13,17 +13,7 @@ export function assertMutationViaGet( ? getOperationAST(document, operationName) ?? undefined : undefined; - if (!operation) { - throw createGraphQLError('Could not determine what operation to execute.', { - extensions: { - http: { - status: 400, - }, - }, - }); - } - - if (operation.operation === 'mutation' && method === 'GET') { + if (operation?.operation === 'mutation' && method === 'GET') { throw createGraphQLError('Can only perform a mutation operation from a POST request.', { extensions: { http: {