Skip to content

Commit 9f3f945

Browse files
authored
feat: allow extra params in body (#3333)
* feat: allow extra params in body * Apply feedback https://github.com/dotansimha/graphql-yoga/pull/3333\#discussion_r1658423243 * Docs * Update request-customization.mdx * Prettier
1 parent 66bef8a commit 9f3f945

File tree

6 files changed

+301
-6
lines changed

6 files changed

+301
-6
lines changed

.changeset/eighty-books-wonder.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'graphql-yoga': minor
3+
---
4+
5+
By default, Yoga does not allow extra parameters in the request body other than `query`, `operationName`, `extensions`, and `variables`, then throws 400 HTTP Error.
6+
This change adds a new option called `extraParamNames` to allow extra parameters in the request body.
7+
8+
```ts
9+
import { createYoga } from 'graphql-yoga';
10+
11+
const yoga = createYoga({
12+
/* other options */
13+
extraParamNames: ['extraParam1', 'extraParam2'],
14+
});
15+
16+
const res = await yoga.fetch('/graphql', {
17+
method: 'POST',
18+
headers: {
19+
'Content-Type': 'application/json',
20+
},
21+
body: JSON.stringify({
22+
query: 'query { __typename }',
23+
extraParam1: 'value1',
24+
extraParam2: 'value2',
25+
}),
26+
});
27+
28+
console.assert(res.status === 200);
29+
```

packages/graphql-yoga/__tests__/requests.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,47 @@ describe('requests', () => {
402402
expect(body.errors?.[0].message).toBe('Unexpected parameter "test" in the request body.');
403403
});
404404

405+
it('does not error if there is a specified invalid parameter in the request body', async () => {
406+
const yoga = createYoga({
407+
schema,
408+
logging: false,
409+
extraParamNames: ['test'],
410+
});
411+
const response = await yoga.fetch(`http://yoga/graphql`, {
412+
method: 'POST',
413+
headers: {
414+
'content-type': 'application/graphql+json',
415+
},
416+
body: JSON.stringify({ query: '{ ping }', test: 'a' }),
417+
});
418+
419+
expect(response.status).toBe(200);
420+
const body = await response.json();
421+
expect(body.errors).toBeUndefined();
422+
expect(body.data.ping).toBe('pong');
423+
});
424+
425+
it('throws when there is an invalid parameter in the request body other than the specified invalid parameters', async () => {
426+
const yoga = createYoga({
427+
schema,
428+
logging: false,
429+
extraParamNames: ['test'],
430+
});
431+
const response = await yoga.fetch(`http://yoga/graphql`, {
432+
method: 'POST',
433+
headers: {
434+
accept: 'application/graphql-response+json',
435+
'content-type': 'application/graphql+json',
436+
},
437+
body: JSON.stringify({ query: '{ ping }', test2: 'a' }),
438+
});
439+
440+
expect(response.status).toBe(400);
441+
const body = await response.json();
442+
expect(body.data).toBeUndefined();
443+
expect(body.errors?.[0].message).toBe('Unexpected parameter "test2" in the request body.');
444+
});
445+
405446
it('should use supported accept header when multiple are provided', async () => {
406447
const response = await yoga.fetch('http://yoga/test-graphql', {
407448
method: 'POST',

packages/graphql-yoga/src/plugins/request-validation/use-check-graphql-query-params.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import type { Plugin } from '../types.js';
44

55
const expectedParameters = new Set(['query', 'variables', 'operationName', 'extensions']);
66

7-
export function assertInvalidParams(params: unknown): asserts params is GraphQLParams {
7+
export function assertInvalidParams(
8+
params: unknown,
9+
extraParamNames?: string[],
10+
): asserts params is GraphQLParams {
811
if (params == null || typeof params !== 'object') {
912
throw createGraphQLError('Invalid "params" in the request body', {
1013
extensions: {
@@ -20,6 +23,9 @@ export function assertInvalidParams(params: unknown): asserts params is GraphQLP
2023
continue;
2124
}
2225
if (!expectedParameters.has(paramKey)) {
26+
if (extraParamNames?.includes(paramKey)) {
27+
continue;
28+
}
2329
throw createGraphQLError(`Unexpected parameter "${paramKey}" in the request body.`, {
2430
extensions: {
2531
http: {
@@ -31,7 +37,10 @@ export function assertInvalidParams(params: unknown): asserts params is GraphQLP
3137
}
3238
}
3339

34-
export function checkGraphQLQueryParams(params: unknown): GraphQLParams {
40+
export function checkGraphQLQueryParams(
41+
params: unknown,
42+
extraParamNames?: string[],
43+
): GraphQLParams {
3544
if (!isObject(params)) {
3645
throw createGraphQLError(
3746
`Expected params to be an object but given ${extendedTypeof(params)}.`,
@@ -48,7 +57,7 @@ export function checkGraphQLQueryParams(params: unknown): GraphQLParams {
4857
);
4958
}
5059

51-
assertInvalidParams(params);
60+
assertInvalidParams(params, extraParamNames);
5261

5362
if (params.query == null) {
5463
throw createGraphQLError('Must provide query string.', {
@@ -124,10 +133,10 @@ export function isValidGraphQLParams(params: unknown): params is GraphQLParams {
124133
}
125134
}
126135

127-
export function useCheckGraphQLQueryParams(): Plugin {
136+
export function useCheckGraphQLQueryParams(extraParamNames?: string[]): Plugin {
128137
return {
129138
onParams({ params }) {
130-
checkGraphQLQueryParams(params);
139+
checkGraphQLQueryParams(params, extraParamNames);
131140
},
132141
};
133142
}

packages/graphql-yoga/src/server.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ export type YogaServerOptions<TServerContext, TUserContext> = {
163163
* @default false
164164
*/
165165
batching?: BatchingOptions | undefined;
166+
167+
/**
168+
* By default, GraphQL Yoga does not allow parameters in the request body except `query`, `variables`, `extensions`, and `operationName`.
169+
*
170+
* This option allows you to specify additional parameters that are allowed in the request body.
171+
*
172+
* @default []
173+
*
174+
* @example ['doc_id', 'id']
175+
*/
176+
extraParamNames?: string[] | undefined;
166177
};
167178

168179
export type BatchingOptions =
@@ -359,7 +370,7 @@ export class YogaServer<
359370
// @ts-expect-error Add plugins has context but this hook doesn't care
360371
addPlugin(useLimitBatching(batchingLimit));
361372
// @ts-expect-error Add plugins has context but this hook doesn't care
362-
addPlugin(useCheckGraphQLQueryParams());
373+
addPlugin(useCheckGraphQLQueryParams(options?.extraParamNames));
363374
const showLandingPage = !!(options?.landingPage ?? true);
364375
addPlugin(
365376
// @ts-expect-error Add plugins has context but this hook doesn't care

website/src/pages/docs/features/_meta.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export default {
2424
testing: 'Testing',
2525
jwt: 'JWT',
2626
'landing-page': 'Landing Page',
27+
'request-customization': 'Request Customization',
2728
};
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
---
2+
description: You can customize the request handling in GraphQL Yoga
3+
---
4+
5+
# Request Customization
6+
7+
For each type of request, GraphQL Yoga uses a specific parser to parse the incoming request and
8+
extract the GraphQL operation from it. Then it applies some validation logic to the extracted
9+
object, then it passes it to the GraphQL execution engine. Yoga mostly follows GraphQL-over-HTTP
10+
standards for this validation and parsing logic.
11+
[See the GraphQL-over-HTTP compliance test results of GraphQL Yoga](https://github.com/graphql/graphql-http/blob/main/implementations/graphql-yoga/README.md)
12+
13+
The parsers are expected to return `GraphQLParams` object that contains the following properties:
14+
15+
- `query`: The GraphQL operation as a string.
16+
- `variables`: The variables for the operation.
17+
- `operationName`: The name of the operation.
18+
- `extensions`: The extensions for the operation.
19+
20+
It doesn't matter how the request is sent, Yoga engine expects the request to parsed in the form of
21+
a `GraphQLParams` object.
22+
23+
## Request Parser
24+
25+
Request parsers are responsible for extracting the GraphQL operation and the other relevant
26+
information from the incoming request. Each parser is responsible for a specific type of request
27+
based on the content type and the HTTP method.
28+
29+
### GraphQL-over-HTTP Spec
30+
31+
These parsers are implemented following the
32+
[GraphQL-over-HTTP spec](https://graphql.github.io/graphql-over-http/).
33+
34+
#### `GET` Parser
35+
36+
This request parser extracts the GraphQL operation from the query string by following
37+
GraphQL-over-HTTP spec.
38+
39+
[See the implementation](https://github.com/dotansimha/graphql-yoga/blob/main/packages/graphql-yoga/src/plugins/request-parser/get.ts)
40+
[See the relevant part in GraphQL-over-HTTP spec](https://graphql.github.io/graphql-over-http/draft/#sec-GET)
41+
42+
##### Example Request
43+
44+
```ts
45+
fetch('/graphql?query=query { __typename }')
46+
```
47+
48+
#### `POST` JSON Parser
49+
50+
This request parser extracts the GraphQL operation from the JSON body of the POST request by
51+
following GraphQL-over-HTTP spec.
52+
53+
[See the implementation](https://github.com/dotansimha/graphql-yoga/blob/main/packages/graphql-yoga/src/plugins/request-parser/post-json.ts).
54+
55+
##### Example Request
56+
57+
```ts
58+
fetch('/graphql', {
59+
method: 'POST',
60+
headers: {
61+
'Content-Type': 'application/json'
62+
},
63+
body: JSON.stringify({
64+
query: 'query { __typename }'
65+
})
66+
})
67+
```
68+
69+
### GraphQL Multipart Request Spec
70+
71+
The parser for multipart requests is implemented following the
72+
[GraphQL Multipart Request Spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
73+
74+
#### `POST` Multipart Parser
75+
76+
This request parser extracts the GraphQL operation from the multipart request by following the
77+
GraphQL Multipart Request Spec. It handles the HTTP request by parsing it as `multipart/form-data`
78+
and extracting the GraphQL operation from it.
79+
80+
[See the implementation](https://github.com/dotansimha/graphql-yoga/blob/main/packages/graphql-yoga/src/plugins/request-parser/post-multipart.ts).
81+
82+
##### Example Request
83+
84+
```ts
85+
const formData = new FormData()
86+
formData.append('operations', JSON.stringify({ query: 'query { __typename }' }))
87+
fetch('/graphql', {
88+
method: 'POST',
89+
headers: {
90+
'Content-Type': 'multipart/form-data'
91+
},
92+
body: formData
93+
})
94+
```
95+
96+
### Extra Parsers
97+
98+
These parsers are not part of any spec but are de-facto standards in the GraphQL community.
99+
100+
### `POST` GraphQL String Parser
101+
102+
This request parser extracts the GraphQL operation from the body of the POST request as a string.
103+
104+
[See the implementation](https://github.com/dotansimha/graphql-yoga/blob/main/packages/graphql-yoga/src/plugins/request-parser/post-graphql-string.ts).
105+
106+
##### Example Request
107+
108+
```ts
109+
fetch('/graphql', {
110+
method: 'POST',
111+
headers: {
112+
'Content-Type': 'application/graphql'
113+
},
114+
body: 'query { __typename }'
115+
})
116+
```
117+
118+
### `POST` FormUrlEncoded Parser
119+
120+
This request parser extracts the GraphQL operation from the form data as url encoded in the POST
121+
body. The context is similar to the `GET` parser but the data is sent in the body of the request.
122+
123+
[See the implementation](https://github.com/dotansimha/graphql-yoga/blob/main/packages/graphql-yoga/src/plugins/request-parser/post-form-url-encoded.ts)
124+
125+
##### Example Request
126+
127+
```ts
128+
fetch('/graphql', {
129+
method: 'POST',
130+
headers: {
131+
'Content-Type': 'application/x-www-form-urlencoded'
132+
},
133+
body: 'query=query { __typename }'
134+
})
135+
```
136+
137+
### Write your own parser
138+
139+
If you want to handle a specific type of request body that is not part of the list above, you can
140+
write your own parser. We use `onRequestParse` to hook into the request parsing process and add your
141+
own logic.
142+
143+
Request parsers are functions that take the request object and return a promise that resolves to a
144+
`GraphQLParams` object.
145+
146+
```ts
147+
import { GraphQLParams, Plugin } from 'graphql-yoga'
148+
149+
const useMyParser: Plugin = () => {
150+
return {
151+
onRequestParse({ request, url, setRequestParser }) {
152+
const contentType = request.headers.get('Content-Type')
153+
if (contentType === 'application/my-content-type') {
154+
setRequestParser(async function myParser() {
155+
const body = await request.text()
156+
const params: GraphQLParams = getParamsFromMyParser(body)
157+
return params
158+
})
159+
}
160+
}
161+
}
162+
}
163+
```
164+
165+
## Request Validation
166+
167+
Request validation is the process of validating the incoming request to ensure that it follows the
168+
GraphQL-over-HTTP spec. Besides the required validation rules, Yoga applies some extra rules to
169+
ensure the security and the performance of the server such as disallowing extra paramters in the
170+
request body. However, you can customize this kind of behaviors on your own risk.
171+
172+
### Extra Parameters in `GraphQLParams`
173+
174+
Yoga doesn't allow extra parameters in the request body other than `query`, `operationName`,
175+
`extensions`, and `variables`. And it returns a 400 HTTP error if it finds any extra parameters. But
176+
you can customize this behavior by passing an array of extra parameter names to the
177+
`extraParamNames` option.
178+
179+
```ts
180+
import { createYoga } from 'graphql-yoga'
181+
182+
const yoga = createYoga({
183+
/* other options */
184+
extraParamNames: ['extraParam1', 'extraParam2']
185+
})
186+
```
187+
188+
Then you can send extra parameters in the request body.
189+
190+
```ts
191+
const res = await yoga.fetch('/graphql', {
192+
method: 'POST',
193+
headers: {
194+
'Content-Type': 'application/json'
195+
},
196+
body: JSON.stringify({
197+
query: 'query { __typename }',
198+
extraParam1: 'value1',
199+
extraParam2: 'value2'
200+
})
201+
})
202+
203+
console.assert(res.status === 200)
204+
```

0 commit comments

Comments
 (0)