@nifrajs/graphql 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ws.js CHANGED
@@ -12,13 +12,124 @@
12
12
  * `graphql-ws` is an optional peer dependency: import this module only when you need subscriptions, and
13
13
  * the dependency (and its bundle weight) stays off every app that doesn't.
14
14
  */
15
- import { execute, subscribe } from "graphql";
15
+ import { execute, GraphQLError, parse, specifiedRules, subscribe, validate, } from "graphql";
16
16
  import { GRAPHQL_TRANSPORT_WS_PROTOCOL, makeServer, } from "graphql-ws";
17
17
  import { buildContext } from "./context.js";
18
+ import { documentLimitError, documentMetrics, graphqlLimits, requestLimitError, } from "./limits.js";
18
19
  /** A frame is a string for JSON control messages; graphql-ws only speaks text frames. */
19
20
  function asText(data) {
20
21
  return typeof data === "string" ? data : new TextDecoder().decode(data);
21
22
  }
23
+ function recordOf(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value)
25
+ ? value
26
+ : undefined;
27
+ }
28
+ function graphqlWsLimits(options) {
29
+ return graphqlLimits(options);
30
+ }
31
+ function assertWsNumber(value, name, minimum) {
32
+ if (!Number.isSafeInteger(value) || value < minimum) {
33
+ throw new RangeError(`GraphQL ${name} must be a safe integer >= ${minimum}`);
34
+ }
35
+ }
36
+ function subscriptionFrameError(message, schema, limits) {
37
+ let frame;
38
+ try {
39
+ frame = JSON.parse(message);
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ const envelope = recordOf(frame);
45
+ if (envelope?.type !== "subscribe")
46
+ return undefined;
47
+ const id = envelope.id;
48
+ const payload = recordOf(envelope.payload);
49
+ const query = payload?.query;
50
+ if (typeof query !== "string")
51
+ return { id, message: "A subscription query is required." };
52
+ const variablesValue = payload?.variables;
53
+ const variables = variablesValue === undefined || variablesValue === null ? null : recordOf(variablesValue);
54
+ if (variablesValue !== undefined && variablesValue !== null && variables === undefined) {
55
+ return { id, message: "Subscription variables must be an object." };
56
+ }
57
+ const requestError = requestLimitError(query, variables ?? null, limits);
58
+ if (requestError !== undefined)
59
+ return { id, message: requestError };
60
+ let document;
61
+ try {
62
+ document = parse(query);
63
+ }
64
+ catch (error) {
65
+ return { id, message: error instanceof GraphQLError ? error.message : "Invalid GraphQL query." };
66
+ }
67
+ const validationErrors = validate(schema, document, specifiedRules);
68
+ if (validationErrors.length > 0)
69
+ return { id, message: validationErrors[0]?.message ?? "Invalid GraphQL query." };
70
+ const limitError = documentLimitError(documentMetrics(document), limits);
71
+ return limitError === undefined ? undefined : { id, message: limitError };
72
+ }
73
+ function maskWsError(error) {
74
+ const record = recordOf(error);
75
+ if (record === undefined)
76
+ return { message: "Internal server error." };
77
+ return {
78
+ message: "Internal server error.",
79
+ ...(record.locations === undefined ? {} : { locations: record.locations }),
80
+ ...(Array.isArray(record.path) ? { path: record.path } : {}),
81
+ };
82
+ }
83
+ /**
84
+ * A frame graphql-ws sends, execution errors masked. A `complete` or `error` frame ends its operation,
85
+ * so the id stops counting against `maxSubscriptions`: a client is not expected to complete an
86
+ * operation the server already ended.
87
+ */
88
+ function outboundFrame(message, subscriptions) {
89
+ try {
90
+ const parsed = recordOf(JSON.parse(message));
91
+ if (parsed === undefined)
92
+ return message;
93
+ if ((parsed.type === "complete" || parsed.type === "error") && typeof parsed.id === "string") {
94
+ subscriptions.delete(parsed.id);
95
+ }
96
+ if (parsed.type === "next") {
97
+ const payload = recordOf(parsed.payload);
98
+ if (payload !== undefined && Array.isArray(payload.errors)) {
99
+ return JSON.stringify({
100
+ ...parsed,
101
+ payload: { ...payload, errors: payload.errors.map(maskWsError) },
102
+ });
103
+ }
104
+ }
105
+ if (parsed.type === "error" && Array.isArray(parsed.payload)) {
106
+ return JSON.stringify({ ...parsed, payload: parsed.payload.map(maskWsError) });
107
+ }
108
+ }
109
+ catch {
110
+ return message;
111
+ }
112
+ return message;
113
+ }
114
+ /**
115
+ * A timeout is that operation's error result. graphql-ws does not catch a rejected execute or
116
+ * subscribe, so a rejection would end the whole socket instead of the one operation.
117
+ */
118
+ async function withWsTimeout(work, timeoutMs) {
119
+ if (!(work instanceof Promise) || timeoutMs === 0)
120
+ return work;
121
+ let timer;
122
+ const timeout = new Promise((resolve) => {
123
+ timer = setTimeout(() => resolve({ errors: [new GraphQLError("Execution timed out.")] }), timeoutMs);
124
+ });
125
+ try {
126
+ return await Promise.race([work, timeout]);
127
+ }
128
+ finally {
129
+ if (timer !== undefined)
130
+ clearTimeout(timer);
131
+ }
132
+ }
22
133
  /**
23
134
  * Build a nifra {@link WebSocketHandler} that terminates the `graphql-transport-ws` protocol. Register
24
135
  * it on a route enabled with `.use(websocket())`:
@@ -34,23 +145,38 @@ function asText(data) {
34
145
  * `graphql-ws` client to connect.
35
146
  */
36
147
  export function graphqlWebSocket(options) {
148
+ const limits = graphqlWsLimits(options);
149
+ const maxSubscriptions = options.maxSubscriptions ?? 20;
150
+ assertWsNumber(maxSubscriptions, "maxSubscriptions", 1);
151
+ const executionTimeoutMs = options.executionTimeoutMs ?? 10_000;
152
+ assertWsNumber(executionTimeoutMs, "executionTimeoutMs", 0);
153
+ const executeWithTimeout = (args) => withWsTimeout(execute(args), executionTimeoutMs);
154
+ const subscribeWithTimeout = (args) => withWsTimeout(subscribe(args), executionTimeoutMs);
37
155
  const gqlServer = makeServer({
156
+ // Keep the raw escape hatch for auxiliary graphql-ws hooks, but make the package's safety
157
+ // controls authoritative: callers cannot accidentally replace the bounded execute/subscribe
158
+ // wrappers or the context/authentication seams with an unbounded implementation.
159
+ ...options.serverOptions,
38
160
  schema: options.schema,
39
- execute,
40
- subscribe,
161
+ execute: executeWithTimeout,
162
+ subscribe: subscribeWithTimeout,
41
163
  onConnect: options.onConnect
42
164
  ? async (ctx) => {
43
- return await options.onConnect?.(ctx.connectionParams, ctx.extra.request);
165
+ try {
166
+ return await options.onConnect?.(ctx.connectionParams, ctx.extra.request);
167
+ }
168
+ catch {
169
+ return false;
170
+ }
44
171
  }
45
172
  : undefined,
46
173
  context: options.context
47
174
  ? async (ctx) => await buildContext(options.context, { request: ctx.extra.request })
48
175
  : undefined,
49
- ...options.serverOptions,
50
176
  });
51
177
  return {
52
178
  upgrade(c) {
53
- return { request: c.req };
179
+ return { request: c.req, subscriptions: new Set() };
54
180
  },
55
181
  open(ws) {
56
182
  const protocol = ws.raw?.protocol ?? GRAPHQL_TRANSPORT_WS_PROTOCOL;
@@ -58,7 +184,7 @@ export function graphqlWebSocket(options) {
58
184
  const closed = gqlServer.opened({
59
185
  protocol,
60
186
  send: async (data) => {
61
- ws.send(data);
187
+ ws.send(outboundFrame(data, ws.data.subscriptions));
62
188
  },
63
189
  close: (code, reason) => {
64
190
  ws.close(code, reason);
@@ -68,17 +194,58 @@ export function graphqlWebSocket(options) {
68
194
  },
69
195
  }, { request: ws.data.request });
70
196
  ws.data.closed = closed;
71
- ws.data.dispatch = (message) => onFrame?.(message);
197
+ ws.data.dispatch = async (message) => {
198
+ try {
199
+ await onFrame?.(message);
200
+ }
201
+ catch {
202
+ // graphql-ws leaves a failed frame (a throwing context builder, say) to its socket adapter.
203
+ // Unhandled, the rejection would end a Bun or Node process; the protocol closes one socket.
204
+ ws.close(4500, "Internal server error");
205
+ }
206
+ };
72
207
  },
73
208
  message(ws, data) {
74
209
  // Fire-and-forget: graphql-ws's per-message handler does not resolve until a subscription's
75
210
  // iterator completes (`await for await`), so awaiting it here would block every later frame on
76
211
  // this socket - including the client's `Complete` that stops the subscription. graphql-ws drives
77
212
  // its own sends and error frames from inside that loop; we only feed it inbound frames.
78
- void ws.data.dispatch?.(asText(data));
213
+ const message = asText(data);
214
+ const rejected = subscriptionFrameError(message, options.schema, limits);
215
+ if (rejected !== undefined) {
216
+ ws.send(JSON.stringify({
217
+ id: rejected.id,
218
+ type: "error",
219
+ payload: [{ message: rejected.message }],
220
+ }));
221
+ return;
222
+ }
223
+ try {
224
+ const frame = recordOf(JSON.parse(message));
225
+ if (frame?.type === "subscribe" && typeof frame.id === "string") {
226
+ if (ws.data.subscriptions.size >= maxSubscriptions) {
227
+ ws.send(JSON.stringify({
228
+ id: frame.id,
229
+ type: "error",
230
+ payload: [{ message: "Subscription limit exceeded." }],
231
+ }));
232
+ return;
233
+ }
234
+ ws.data.subscriptions.add(frame.id);
235
+ }
236
+ else if (frame?.type === "complete" && typeof frame.id === "string") {
237
+ ws.data.subscriptions.delete(frame.id);
238
+ }
239
+ }
240
+ catch {
241
+ // graphql-ws owns malformed protocol-frame handling; pass it through unchanged.
242
+ }
243
+ void ws.data.dispatch?.(message);
79
244
  },
80
245
  close(ws, code, reason) {
81
- void ws.data.closed?.(code, reason);
246
+ ws.data.subscriptions.clear();
247
+ // The socket is gone: a failing iterator return() or onClose hook has no one left to answer.
248
+ void Promise.resolve(ws.data.closed?.(code, reason)).catch(() => { });
82
249
  },
83
250
  };
84
251
  }
package/dist/ws.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,EAAE,OAAO,EAAsB,SAAS,EAAE,MAAM,SAAS,CAAA;AAChE,OAAO,EAEL,6BAA6B,EAE7B,UAAU,GAEX,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAA8B,MAAM,cAAc,CAAA;AA8BvE,yFAAyF;AACzF,SAAS,MAAM,CAAC,IAAyB;IACvC,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AACzE,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAkC;IAElC,MAAM,SAAS,GAAG,UAAU,CAAgD;QAC1E,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO;QACP,SAAS;QACT,SAAS,EAAE,OAAO,CAAC,SAAS;YAC1B,CAAC,CAAC,KAAK,EAAE,GAAoE,EAAE,EAAE;gBAC7E,OAAO,MAAM,OAAO,CAAC,SAAS,EAAE,CAC9B,GAAG,CAAC,gBAAoD,EACxD,GAAG,CAAC,KAAK,CAAC,OAAO,CAClB,CAAA;YACH,CAAC;YACH,CAAC,CAAC,SAAS;QACb,OAAO,EAAE,OAAO,CAAC,OAAO;YACtB,CAAC,CAAC,KAAK,EAAE,GAAoE,EAAE,EAAE,CAC7E,MAAM,YAAY,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YACvE,CAAC,CAAC,SAAS;QACb,GAAG,OAAO,CAAC,aAAa;KACzB,CAAC,CAAA;IAEF,OAAO;QACL,OAAO,CAAC,CAAmB;YACzB,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAA;QAC3B,CAAC;QAED,IAAI,CAAC,EAAuC;YAC1C,MAAM,QAAQ,GACX,EAAE,CAAC,GAAyC,EAAE,QAAQ,IAAI,6BAA6B,CAAA;YAC1F,IAAI,OAAgE,CAAA;YACpE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAC7B;gBACE,QAAQ;gBACR,IAAI,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE;oBAC3B,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACf,CAAC;gBACD,KAAK,EAAE,CAAC,IAAY,EAAE,MAAc,EAAE,EAAE;oBACtC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;gBACxB,CAAC;gBACD,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE;oBAChB,OAAO,GAAG,EAAE,CAAA;gBACd,CAAC;aACF,EACD,EAAE,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAC7B,CAAA;YACD,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;YACvB,EAAE,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAA;QAC5D,CAAC;QAED,OAAO,CAAC,EAAuC,EAAE,IAAyB;YACxE,4FAA4F;YAC5F,+FAA+F;YAC/F,iGAAiG;YACjG,wFAAwF;YACxF,KAAK,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;QACvC,CAAC;QAED,KAAK,CAAC,EAAuC,EAAE,IAAY,EAAE,MAAc;YACzE,KAAK,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACrC,CAAC;KACF,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"ws.js","sourceRoot":"","sources":["../src/ws.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/graphql",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "description": "Mount a GraphQL endpoint on a nifra app - a spec-compliant GraphQL-over-HTTP handler you mount at POST /graphql, graphql-ws subscriptions over nifra's native WebSocket lane, and typed nifra context piped straight into your resolvers. Uses graphql's own execute/subscribe; no server framework re-bundled.",
5
5
  "keywords": [
6
6
  "bun",
@@ -14,6 +14,9 @@
14
14
  "api"
15
15
  ],
16
16
  "type": "module",
17
+ "nifra": {
18
+ "environment": "library"
19
+ },
17
20
  "main": "./dist/index.js",
18
21
  "module": "./dist/index.js",
19
22
  "types": "./dist/index.d.ts",
@@ -43,7 +46,7 @@
43
46
  "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
44
47
  },
45
48
  "peerDependencies": {
46
- "@nifrajs/core": "^3.3.0",
49
+ "@nifrajs/core": "^4.0.0",
47
50
  "graphql": "^17.0.0",
48
51
  "graphql-ws": "^6.0.0"
49
52
  },
package/src/http.ts CHANGED
@@ -44,6 +44,14 @@ import {
44
44
  type GraphqlContextInput,
45
45
  type NifraContextLike,
46
46
  } from "./context.ts"
47
+ import {
48
+ documentLimitError,
49
+ documentMetrics,
50
+ type GraphqlLimits,
51
+ graphqlLimits,
52
+ requestLimitError,
53
+ safeFormattedError,
54
+ } from "./limits.ts"
47
55
 
48
56
  const DEFAULT_MAX_BODY_BYTES = 1_000_000
49
57
  const GRAPHQL_RESPONSE_JSON = "application/graphql-response+json; charset=utf-8"
@@ -64,6 +72,20 @@ export interface GraphqlHttpOptions<Context = unknown, Env = unknown> {
64
72
  readonly nifra?: NifraContextLike<Env>
65
73
  /** Maximum request body size in bytes. Default 1 MB. Rejected bodies answer 413. */
66
74
  readonly maxBodyBytes?: number
75
+ /** Maximum UTF-8 query size. Default 64 KiB, applied to POST and GET. */
76
+ readonly maxQueryBytes?: number
77
+ /** Maximum UTF-8 serialized variables size. Default 256 KiB. */
78
+ readonly maxVariablesBytes?: number
79
+ /** Maximum expanded field depth. Default 20. */
80
+ readonly maxDepth?: number
81
+ /** Maximum aliases in one document. Default 100. */
82
+ readonly maxAliases?: number
83
+ /** Maximum simple field complexity in one document. Default 1,000. */
84
+ readonly maxComplexity?: number
85
+ /** Maximum operation definitions in one document. Default 10. */
86
+ readonly maxOperations?: number
87
+ /** Maximum asynchronous resolver execution time. Default 10 seconds. */
88
+ readonly executionTimeoutMs?: number
67
89
  /** Prototype-pollution policy for the parsed JSON body. Default `"reject"`. */
68
90
  readonly protoPoisoning?: ProtoPoisoning
69
91
  /** Optional root value passed to the executor. */
@@ -80,6 +102,8 @@ export interface GraphqlHttpOptions<Context = unknown, Env = unknown> {
80
102
  readonly authorize?: (request: Request) => boolean | Promise<boolean>
81
103
  /** Emit the legacy always-200 `application/json` response shape instead of `application/graphql-response+json`. */
82
104
  readonly legacyJsonResponse?: boolean
105
+ /** Format request/execution errors. The default masks resolver messages and preserves request errors. */
106
+ readonly formatError?: (error: GraphQLError, phase: "request" | "execution") => unknown
83
107
  }
84
108
 
85
109
  /** A parsed GraphQL request payload (POST body or GET query string), before validation. */
@@ -95,6 +119,12 @@ function assertByteLimit(value: number): void {
95
119
  }
96
120
  }
97
121
 
122
+ function assertExecutionTimeout(value: number): void {
123
+ if (!Number.isSafeInteger(value) || value < 0) {
124
+ throw new RangeError("GraphQL executionTimeoutMs must be a non-negative safe integer")
125
+ }
126
+ }
127
+
98
128
  function corsFor(
99
129
  request: Request,
100
130
  allowedOrigins: readonly string[] | undefined,
@@ -136,9 +166,15 @@ function requestError(
136
166
  legacy: boolean,
137
167
  cors: Record<string, string>,
138
168
  errors?: readonly GraphQLError[],
169
+ formatter?: GraphqlHttpOptions["formatError"],
139
170
  ): Response {
140
171
  const payload = errors ?? [new GraphQLError(message)]
141
- return graphqlResponse({ errors: payload.map((e) => e.toJSON()) }, status, legacy, cors)
172
+ return graphqlResponse(
173
+ { errors: payload.map((e) => safeFormattedError(e, "request", formatter)) },
174
+ status,
175
+ legacy,
176
+ cors,
177
+ )
142
178
  }
143
179
 
144
180
  /** Coerce a `?variables=<json>` string (GET) into an object, or throw a GraphQLError on bad JSON. */
@@ -189,18 +225,43 @@ async function run<Context, Env>(
189
225
  isGet: boolean,
190
226
  legacy: boolean,
191
227
  cors: Record<string, string>,
228
+ limits: GraphqlLimits,
229
+ executionTimeoutMs: number,
192
230
  ): Promise<Response> {
231
+ const requestLimit = requestLimitError(params.query, params.variables, limits)
232
+ if (requestLimit !== undefined) {
233
+ return requestError(
234
+ requestLimit,
235
+ 413,
236
+ legacy,
237
+ cors,
238
+ [new GraphQLError(requestLimit)],
239
+ opts.formatError,
240
+ )
241
+ }
193
242
  let document: DocumentNode
194
243
  try {
195
244
  document = parse(new Source(params.query, "GraphQL request"))
196
245
  } catch (err) {
197
246
  const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err))
198
- return requestError("", 400, legacy, cors, [gqlErr])
247
+ return requestError("", 400, legacy, cors, [gqlErr], opts.formatError)
199
248
  }
200
249
 
201
250
  const validationErrors = validate(opts.schema, document, specifiedRules)
202
251
  if (validationErrors.length > 0) {
203
- return requestError("", 400, legacy, cors, validationErrors)
252
+ return requestError("", 400, legacy, cors, validationErrors, opts.formatError)
253
+ }
254
+
255
+ const documentLimit = documentLimitError(documentMetrics(document), limits)
256
+ if (documentLimit !== undefined) {
257
+ return requestError(
258
+ documentLimit,
259
+ 400,
260
+ legacy,
261
+ cors,
262
+ [new GraphQLError(documentLimit)],
263
+ opts.formatError,
264
+ )
204
265
  }
205
266
 
206
267
  // GET is idempotent and cacheable, so per the GraphQL-over-HTTP spec it may run queries only.
@@ -212,6 +273,7 @@ async function run<Context, Env>(
212
273
  legacy,
213
274
  { ...cors, allow: "POST" },
214
275
  [new GraphQLError("Only query operations are allowed over GET.")],
276
+ opts.formatError,
215
277
  )
216
278
  }
217
279
 
@@ -223,26 +285,74 @@ async function run<Context, Env>(
223
285
  } catch (err) {
224
286
  const gqlErr =
225
287
  err instanceof GraphQLError ? err : new GraphQLError("Failed to build request context.")
226
- return graphqlResponse({ errors: [gqlErr.toJSON()] }, 500, legacy, cors)
288
+ return graphqlResponse(
289
+ { errors: [safeFormattedError(gqlErr, "execution", opts.formatError)] },
290
+ 500,
291
+ legacy,
292
+ cors,
293
+ )
227
294
  }
228
295
 
229
296
  let result: ExecutionResult
230
297
  try {
231
- result = (await execute({
298
+ const execution = execute({
232
299
  schema: opts.schema,
233
300
  document,
234
301
  rootValue: opts.rootValue,
235
302
  contextValue,
236
303
  variableValues: params.variables ?? undefined,
237
304
  operationName: params.operationName ?? undefined,
238
- })) as ExecutionResult
305
+ })
306
+ result = (await withExecutionTimeout(execution, executionTimeoutMs)) as ExecutionResult
239
307
  } catch (err) {
240
- const gqlErr = err instanceof GraphQLError ? err : new GraphQLError("Execution failed.")
241
- return graphqlResponse({ errors: [gqlErr.toJSON()] }, 400, legacy, cors)
308
+ const gqlErr =
309
+ err instanceof GraphQLError
310
+ ? err
311
+ : err instanceof ExecutionTimeoutError
312
+ ? new GraphQLError("Execution timed out.")
313
+ : new GraphQLError("Execution failed.")
314
+ return graphqlResponse(
315
+ { errors: [safeFormattedError(gqlErr, "execution", opts.formatError)] },
316
+ err instanceof ExecutionTimeoutError ? 504 : 500,
317
+ legacy,
318
+ cors,
319
+ )
242
320
  }
243
321
 
244
322
  // Field/execution errors are a normal 200 result with both `data` and `errors`.
245
- return graphqlResponse(result, 200, legacy, cors)
323
+ return graphqlResponse(
324
+ result.errors === undefined
325
+ ? result
326
+ : {
327
+ ...result,
328
+ errors: result.errors.map((error) =>
329
+ safeFormattedError(error, "execution", opts.formatError),
330
+ ),
331
+ },
332
+ 200,
333
+ legacy,
334
+ cors,
335
+ )
336
+ }
337
+
338
+ class ExecutionTimeoutError extends Error {
339
+ constructor() {
340
+ super("GraphQL execution timed out")
341
+ this.name = "ExecutionTimeoutError"
342
+ }
343
+ }
344
+
345
+ async function withExecutionTimeout<T>(work: T | Promise<T>, timeoutMs: number): Promise<T> {
346
+ if (!(work instanceof Promise) || timeoutMs === 0) return work
347
+ let timer: ReturnType<typeof setTimeout> | undefined
348
+ const timeout = new Promise<T>((_, reject) => {
349
+ timer = setTimeout(() => reject(new ExecutionTimeoutError()), timeoutMs)
350
+ })
351
+ try {
352
+ return await Promise.race([work, timeout])
353
+ } finally {
354
+ if (timer !== undefined) clearTimeout(timer)
355
+ }
246
356
  }
247
357
 
248
358
  /**
@@ -256,6 +366,9 @@ export async function respondGraphql<Context = unknown, Env = unknown>(
256
366
  ): Promise<Response> {
257
367
  const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
258
368
  assertByteLimit(maxBytes)
369
+ const limits: GraphqlLimits = graphqlLimits(options)
370
+ const executionTimeoutMs = options.executionTimeoutMs ?? 10_000
371
+ assertExecutionTimeout(executionTimeoutMs)
259
372
  const legacy = options.legacyJsonResponse === true
260
373
 
261
374
  const cors = corsFor(request, options.allowedOrigins)
@@ -266,7 +379,14 @@ export async function respondGraphql<Context = unknown, Env = unknown>(
266
379
  if (options.authorize !== undefined) {
267
380
  const ok = await options.authorize(request)
268
381
  if (!ok) {
269
- return requestError("Unauthorized.", 401, legacy, cors, [new GraphQLError("Unauthorized.")])
382
+ return requestError(
383
+ "Unauthorized.",
384
+ 401,
385
+ legacy,
386
+ cors,
387
+ [new GraphQLError("Unauthorized.")],
388
+ options.formatError,
389
+ )
270
390
  }
271
391
  }
272
392
 
@@ -276,15 +396,20 @@ export async function respondGraphql<Context = unknown, Env = unknown>(
276
396
  params = paramsFromQuery(queryObjectOf(searchOf(request.url)) as Record<string, unknown>)
277
397
  } catch (err) {
278
398
  const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err))
279
- return requestError("", 400, legacy, cors, [gqlErr])
399
+ return requestError("", 400, legacy, cors, [gqlErr], options.formatError)
280
400
  }
281
- return run(params, request, options, true, legacy, cors)
401
+ return run(params, request, options, true, legacy, cors, limits, executionTimeoutMs)
282
402
  }
283
403
 
284
404
  if (request.method !== "POST") {
285
- return requestError("Method not allowed.", 405, legacy, { ...cors, allow: "GET, POST" }, [
286
- new GraphQLError("Only GET and POST are supported."),
287
- ])
405
+ return requestError(
406
+ "Method not allowed.",
407
+ 405,
408
+ legacy,
409
+ { ...cors, allow: "GET, POST" },
410
+ [new GraphQLError("Only GET and POST are supported.")],
411
+ options.formatError,
412
+ )
288
413
  }
289
414
 
290
415
  // Read the JSON body through core's single bounded + proto-guarded framing lane.
@@ -298,9 +423,9 @@ export async function respondGraphql<Context = unknown, Env = unknown>(
298
423
  params = paramsFromBody(parsed)
299
424
  } catch (err) {
300
425
  const gqlErr = err instanceof GraphQLError ? err : new GraphQLError(String(err))
301
- return requestError("", 400, legacy, cors, [gqlErr])
426
+ return requestError("", 400, legacy, cors, [gqlErr], options.formatError)
302
427
  }
303
- return run(params, request, options, false, legacy, cors)
428
+ return run(params, request, options, false, legacy, cors, limits, executionTimeoutMs)
304
429
  },
305
430
  (response: Response | ResponseResult) => {
306
431
  // readBodyFramed's own rejections (413 too-large, 415, proto-guard) - render + attach CORS.
@@ -311,9 +436,14 @@ export async function respondGraphql<Context = unknown, Env = unknown>(
311
436
  (err: unknown) => {
312
437
  // A body that failed to read (bad JSON / proto-guard) is a request error, not a crash.
313
438
  if (err instanceof Response) return err
314
- return requestError("Invalid request body.", 400, legacy, cors, [
315
- new GraphQLError("Request body could not be read as JSON."),
316
- ])
439
+ return requestError(
440
+ "Invalid request body.",
441
+ 400,
442
+ legacy,
443
+ cors,
444
+ [new GraphQLError("Request body could not be read as JSON.")],
445
+ options.formatError,
446
+ )
317
447
  },
318
448
  ).catch(() => plainErrorResponse(cors))
319
449
  }