From baf0b15ef1c7c804b7689193053ce65a3caf1134 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 8 Mar 2022 15:35:10 +0100 Subject: [PATCH] Enable to configure allowed HTTP methods for subscriptions (#225) * Enable to configure allowed HTTP methods for subscriptions * Move test * Add changeset * Update packages/core/lib/process-request.ts Co-authored-by: Laurin Quast * adjust test Co-authored-by: Laurin Quast --- .changeset/sweet-icons-enjoy.md | 5 +++ packages/core/lib/process-request.ts | 19 +++++++-- packages/core/lib/types.ts | 4 ++ packages/core/test/process-request.test.ts | 49 ++++++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 .changeset/sweet-icons-enjoy.md create mode 100644 packages/core/test/process-request.test.ts diff --git a/.changeset/sweet-icons-enjoy.md b/.changeset/sweet-icons-enjoy.md new file mode 100644 index 00000000..da5f5d93 --- /dev/null +++ b/.changeset/sweet-icons-enjoy.md @@ -0,0 +1,5 @@ +--- +"graphql-helix": minor +--- + +Enable to configure allowed HTTP methods for subscriptions. diff --git a/packages/core/lib/process-request.ts b/packages/core/lib/process-request.ts index 095a0702..34e3c604 100644 --- a/packages/core/lib/process-request.ts +++ b/packages/core/lib/process-request.ts @@ -97,6 +97,7 @@ export const processRequest = async ( validate = defaultValidate, validationRules, variables, + allowedSubscriptionHttpMethods = ["GET", "POST"], } = options; let context: TContext | undefined; @@ -165,10 +166,20 @@ export const processRequest = async ( rootValue = rootValueFactory ? await rootValueFactory(executionContext) : ({} as TRootValue); if (operation.operation === "subscription") { - if (!isHttpMethod("GET", request.method)) { - throw new HttpError(405, "Can only perform subscription operation from a GET request.", { - headers: [...defaultSingleResponseHeaders, { name: "Allow", value: "GET" }], - }); + if (!allowedSubscriptionHttpMethods.some((method) => isHttpMethod(method, request.method))) { + throw new HttpError( + 405, + `Can only perform subscription operation from a ${allowedSubscriptionHttpMethods.join(" or ")} request.`, + { + headers: [ + ...defaultSingleResponseHeaders, + { + name: "Allow", + value: allowedSubscriptionHttpMethods.join(", "), + }, + ], + } + ); } const result = await subscribe({ schema, diff --git a/packages/core/lib/types.ts b/packages/core/lib/types.ts index 01f4baff..43908e8a 100644 --- a/packages/core/lib/types.ts +++ b/packages/core/lib/types.ts @@ -121,6 +121,10 @@ export interface ProcessRequestOptions { * Values for any Variables defined by the Operation. */ variables?: string | { [name: string]: any }; + /** + * HTTP methods that are allowed for subscriptions. + */ + allowedSubscriptionHttpMethods?: ReadonlyArray<"POST" | "GET">; } export interface FormatPayloadParams { diff --git a/packages/core/test/process-request.test.ts b/packages/core/test/process-request.test.ts new file mode 100644 index 00000000..e063f911 --- /dev/null +++ b/packages/core/test/process-request.test.ts @@ -0,0 +1,49 @@ +import { getGraphQLParameters, processRequest } from "../lib"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { GraphQLError } from "graphql"; + +const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + type Subscription { + countdown(from: Int): Int + } + `, + resolvers: { + Subscription: { + countdown: { + subscribe: async function* () { + yield "Hi"; + }, + }, + }, + }, +}); + +describe("process-request", () => { + it("should not allow POST if disabled", async () => { + const request = { + body: { query: "subscription { countdown }" }, + method: "POST", + headers: {}, + query: "", + }; + + const { operationName, query, variables } = getGraphQLParameters(request); + + const result = await processRequest({ + operationName, + query, + variables, + request, + schema, + allowedSubscriptionHttpMethods: ["GET"], + }); + + expect(result.type).toBe("RESPONSE"); + expect((result as any).status).toBe(405); + expect((result as any).payload.errors[0] instanceof GraphQLError).toBe(true); + }); +});