Skip to content

Commit 342aafb

Browse files
authoredMay 8, 2024··
Add no-invalid-fetch-options rule (#2338)
1 parent 45bd444 commit 342aafb

6 files changed

+526
-0
lines changed
 
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Disallow invalid options in `fetch()` and `new Request()`
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
<!-- end auto-generated rule header -->
6+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
7+
8+
[`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) throws a `TypeError` when the method is `GET` or `HEAD` and a body is provided.
9+
10+
## Fail
11+
12+
```js
13+
const response = await fetch('/', {body: 'foo=bar'});
14+
```
15+
16+
```js
17+
const request = new Request('/', {body: 'foo=bar'});
18+
```
19+
20+
```js
21+
const response = await fetch('/', {method: 'GET', body: 'foo=bar'});
22+
```
23+
24+
```js
25+
const request = new Request('/', {method: 'GET', body: 'foo=bar'});
26+
```
27+
28+
## Pass
29+
30+
```js
31+
const response = await fetch('/', {method: 'HEAD'});
32+
```
33+
34+
```js
35+
const request = new Request('/', {method: 'HEAD'});
36+
```
37+
38+
```js
39+
const response = await fetch('/', {method: 'POST', body: 'foo=bar'});
40+
```
41+
42+
```js
43+
const request = new Request('/', {method: 'POST', body: 'foo=bar'});
44+
```

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
138138
| [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. || 🔧 | 💡 |
139139
| [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. || 🔧 | |
140140
| [no-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. || 🔧 | |
141+
| [no-invalid-fetch-options](docs/rules/no-invalid-fetch-options.md) | Disallow invalid options in `fetch()` and `new Request()`. || | |
141142
| [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. || | |
142143
| [no-keyword-prefix](docs/rules/no-keyword-prefix.md) | Disallow identifiers starting with `new` or `class`. | | | |
143144
| [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. || 🔧 | |

‎rules/no-invalid-fetch-options.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
const {getStaticValue} = require('@eslint-community/eslint-utils');
3+
const {
4+
isCallExpression,
5+
isNewExpression,
6+
isUndefined,
7+
isNullLiteral,
8+
} = require('./ast/index.js');
9+
10+
const MESSAGE_ID_ERROR = 'no-invalid-fetch-options';
11+
const messages = {
12+
[MESSAGE_ID_ERROR]: '"body" is not allowed when method is "{{method}}".',
13+
};
14+
15+
const isObjectPropertyWithName = (node, name) =>
16+
node.type === 'Property'
17+
&& !node.computed
18+
&& node.key.type === 'Identifier'
19+
&& node.key.name === name;
20+
21+
function checkFetchOptions(context, node) {
22+
if (node.type !== 'ObjectExpression') {
23+
return;
24+
}
25+
26+
const {properties} = node;
27+
28+
const bodyProperty = properties.findLast(property => isObjectPropertyWithName(property, 'body'));
29+
30+
if (!bodyProperty) {
31+
return;
32+
}
33+
34+
const bodyValue = bodyProperty.value;
35+
if (isUndefined(bodyValue) || isNullLiteral(bodyValue)) {
36+
return;
37+
}
38+
39+
const methodProperty = properties.findLast(property => isObjectPropertyWithName(property, 'method'));
40+
// If `method` is omitted but there is an `SpreadElement`, we just ignore the case
41+
if (!methodProperty) {
42+
if (properties.some(node => node.type === 'SpreadElement')) {
43+
return;
44+
}
45+
46+
return {
47+
node: bodyProperty.key,
48+
messageId: MESSAGE_ID_ERROR,
49+
data: {method: 'GET'},
50+
};
51+
}
52+
53+
const methodValue = methodProperty.value;
54+
55+
const scope = context.sourceCode.getScope(methodValue);
56+
let method = getStaticValue(methodValue, scope)?.value;
57+
58+
if (typeof method !== 'string') {
59+
return;
60+
}
61+
62+
method = method.toUpperCase();
63+
if (method !== 'GET' && method !== 'HEAD') {
64+
return;
65+
}
66+
67+
return {
68+
node: bodyProperty.key,
69+
messageId: MESSAGE_ID_ERROR,
70+
data: {method},
71+
};
72+
}
73+
74+
/** @param {import('eslint').Rule.RuleContext} context */
75+
const create = context => {
76+
context.on('CallExpression', callExpression => {
77+
if (!isCallExpression(callExpression, {
78+
name: 'fetch',
79+
minimumArguments: 2,
80+
optional: false,
81+
})) {
82+
return;
83+
}
84+
85+
return checkFetchOptions(context, callExpression.arguments[1]);
86+
});
87+
88+
context.on('NewExpression', newExpression => {
89+
if (!isNewExpression(newExpression, {
90+
name: 'Request',
91+
minimumArguments: 2,
92+
})) {
93+
return;
94+
}
95+
96+
return checkFetchOptions(context, newExpression.arguments[1]);
97+
});
98+
};
99+
100+
/** @type {import('eslint').Rule.RuleModule} */
101+
module.exports = {
102+
create,
103+
meta: {
104+
type: 'problem',
105+
docs: {
106+
description: 'Disallow invalid options in `fetch()` and `new Request()`.',
107+
recommended: true,
108+
},
109+
messages,
110+
},
111+
};

‎test/no-invalid-fetch-options.mjs

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
valid: [
8+
'fetch(url, {method: "POST", body})',
9+
'new Request(url, {method: "POST", body})',
10+
'fetch(url, {})',
11+
'new Request(url, {})',
12+
'fetch(url)',
13+
'new Request(url)',
14+
'fetch(url, {method: "UNKNOWN", body})',
15+
'new Request(url, {method: "UNKNOWN", body})',
16+
'fetch(url, {body: undefined})',
17+
'new Request(url, {body: undefined})',
18+
'fetch(url, {body: null})',
19+
'new Request(url, {body: null})',
20+
'fetch(url, {...options, body})',
21+
'new Request(url, {...options, body})',
22+
'new fetch(url, {body})',
23+
'Request(url, {body})',
24+
'not_fetch(url, {body})',
25+
'new not_Request(url, {body})',
26+
'fetch({body}, url)',
27+
'new Request({body}, url)',
28+
'fetch(url, {[body]: "foo=bar"})',
29+
'new Request(url, {[body]: "foo=bar"})',
30+
outdent`
31+
fetch(url, {
32+
body: 'foo=bar',
33+
body: undefined,
34+
});
35+
`,
36+
outdent`
37+
new Request(url, {
38+
body: 'foo=bar',
39+
body: undefined,
40+
});
41+
`,
42+
outdent`
43+
fetch(url, {
44+
method: 'HEAD',
45+
body: 'foo=bar',
46+
method: 'post',
47+
});
48+
`,
49+
outdent`
50+
new Request(url, {
51+
method: 'HEAD',
52+
body: 'foo=bar',
53+
method: 'post',
54+
});
55+
`,
56+
],
57+
invalid: [
58+
'fetch(url, {body})',
59+
'new Request(url, {body})',
60+
'fetch(url, {method: "GET", body})',
61+
'new Request(url, {method: "GET", body})',
62+
'fetch(url, {method: "HEAD", body})',
63+
'new Request(url, {method: "HEAD", body})',
64+
'fetch(url, {method: "head", body})',
65+
'new Request(url, {method: "head", body})',
66+
'const method = "head"; new Request(url, {method, body: "foo=bar"})',
67+
'const method = "head"; fetch(url, {method, body: "foo=bar"})',
68+
'fetch(url, {body}, extraArgument)',
69+
'new Request(url, {body}, extraArgument)',
70+
outdent`
71+
fetch(url, {
72+
body: undefined,
73+
body: 'foo=bar',
74+
});
75+
`,
76+
outdent`
77+
new Request(url, {
78+
body: undefined,
79+
body: 'foo=bar',
80+
});
81+
`,
82+
outdent`
83+
fetch(url, {
84+
method: 'post',
85+
body: 'foo=bar',
86+
method: 'HEAD',
87+
});
88+
`,
89+
outdent`
90+
new Request(url, {
91+
method: 'post',
92+
body: 'foo=bar',
93+
method: 'HEAD',
94+
});
95+
`,
96+
],
97+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Snapshot report for `test/no-invalid-fetch-options.mjs`
2+
3+
The actual snapshot is saved in `no-invalid-fetch-options.mjs.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## invalid(1): fetch(url, {body})
8+
9+
> Input
10+
11+
`␊
12+
1 | fetch(url, {body})␊
13+
`
14+
15+
> Error 1/1
16+
17+
`␊
18+
> 1 | fetch(url, {body})␊
19+
| ^^^^ "body" is not allowed when method is "GET".␊
20+
`
21+
22+
## invalid(2): new Request(url, {body})
23+
24+
> Input
25+
26+
`␊
27+
1 | new Request(url, {body})␊
28+
`
29+
30+
> Error 1/1
31+
32+
`␊
33+
> 1 | new Request(url, {body})␊
34+
| ^^^^ "body" is not allowed when method is "GET".␊
35+
`
36+
37+
## invalid(3): fetch(url, {method: "GET", body})
38+
39+
> Input
40+
41+
`␊
42+
1 | fetch(url, {method: "GET", body})␊
43+
`
44+
45+
> Error 1/1
46+
47+
`␊
48+
> 1 | fetch(url, {method: "GET", body})␊
49+
| ^^^^ "body" is not allowed when method is "GET".␊
50+
`
51+
52+
## invalid(4): new Request(url, {method: "GET", body})
53+
54+
> Input
55+
56+
`␊
57+
1 | new Request(url, {method: "GET", body})␊
58+
`
59+
60+
> Error 1/1
61+
62+
`␊
63+
> 1 | new Request(url, {method: "GET", body})␊
64+
| ^^^^ "body" is not allowed when method is "GET".␊
65+
`
66+
67+
## invalid(5): fetch(url, {method: "HEAD", body})
68+
69+
> Input
70+
71+
`␊
72+
1 | fetch(url, {method: "HEAD", body})␊
73+
`
74+
75+
> Error 1/1
76+
77+
`␊
78+
> 1 | fetch(url, {method: "HEAD", body})␊
79+
| ^^^^ "body" is not allowed when method is "HEAD".␊
80+
`
81+
82+
## invalid(6): new Request(url, {method: "HEAD", body})
83+
84+
> Input
85+
86+
`␊
87+
1 | new Request(url, {method: "HEAD", body})␊
88+
`
89+
90+
> Error 1/1
91+
92+
`␊
93+
> 1 | new Request(url, {method: "HEAD", body})␊
94+
| ^^^^ "body" is not allowed when method is "HEAD".␊
95+
`
96+
97+
## invalid(7): fetch(url, {method: "head", body})
98+
99+
> Input
100+
101+
`␊
102+
1 | fetch(url, {method: "head", body})␊
103+
`
104+
105+
> Error 1/1
106+
107+
`␊
108+
> 1 | fetch(url, {method: "head", body})␊
109+
| ^^^^ "body" is not allowed when method is "HEAD".␊
110+
`
111+
112+
## invalid(8): new Request(url, {method: "head", body})
113+
114+
> Input
115+
116+
`␊
117+
1 | new Request(url, {method: "head", body})␊
118+
`
119+
120+
> Error 1/1
121+
122+
`␊
123+
> 1 | new Request(url, {method: "head", body})␊
124+
| ^^^^ "body" is not allowed when method is "HEAD".␊
125+
`
126+
127+
## invalid(9): const method = "head"; new Request(url, {method, body: "foo=bar"})
128+
129+
> Input
130+
131+
`␊
132+
1 | const method = "head"; new Request(url, {method, body: "foo=bar"})␊
133+
`
134+
135+
> Error 1/1
136+
137+
`␊
138+
> 1 | const method = "head"; new Request(url, {method, body: "foo=bar"})␊
139+
| ^^^^ "body" is not allowed when method is "HEAD".␊
140+
`
141+
142+
## invalid(10): const method = "head"; fetch(url, {method, body: "foo=bar"})
143+
144+
> Input
145+
146+
`␊
147+
1 | const method = "head"; fetch(url, {method, body: "foo=bar"})␊
148+
`
149+
150+
> Error 1/1
151+
152+
`␊
153+
> 1 | const method = "head"; fetch(url, {method, body: "foo=bar"})␊
154+
| ^^^^ "body" is not allowed when method is "HEAD".␊
155+
`
156+
157+
## invalid(11): fetch(url, {body}, extraArgument)
158+
159+
> Input
160+
161+
`␊
162+
1 | fetch(url, {body}, extraArgument)␊
163+
`
164+
165+
> Error 1/1
166+
167+
`␊
168+
> 1 | fetch(url, {body}, extraArgument)␊
169+
| ^^^^ "body" is not allowed when method is "GET".␊
170+
`
171+
172+
## invalid(12): new Request(url, {body}, extraArgument)
173+
174+
> Input
175+
176+
`␊
177+
1 | new Request(url, {body}, extraArgument)␊
178+
`
179+
180+
> Error 1/1
181+
182+
`␊
183+
> 1 | new Request(url, {body}, extraArgument)␊
184+
| ^^^^ "body" is not allowed when method is "GET".␊
185+
`
186+
187+
## invalid(13): fetch(url, { body: undefined, body: 'foo=bar', });
188+
189+
> Input
190+
191+
`␊
192+
1 | fetch(url, {␊
193+
2 | body: undefined,␊
194+
3 | body: 'foo=bar',␊
195+
4 | });␊
196+
`
197+
198+
> Error 1/1
199+
200+
`␊
201+
1 | fetch(url, {␊
202+
2 | body: undefined,␊
203+
> 3 | body: 'foo=bar',␊
204+
| ^^^^ "body" is not allowed when method is "GET".␊
205+
4 | });␊
206+
`
207+
208+
## invalid(14): new Request(url, { body: undefined, body: 'foo=bar', });
209+
210+
> Input
211+
212+
`␊
213+
1 | new Request(url, {␊
214+
2 | body: undefined,␊
215+
3 | body: 'foo=bar',␊
216+
4 | });␊
217+
`
218+
219+
> Error 1/1
220+
221+
`␊
222+
1 | new Request(url, {␊
223+
2 | body: undefined,␊
224+
> 3 | body: 'foo=bar',␊
225+
| ^^^^ "body" is not allowed when method is "GET".␊
226+
4 | });␊
227+
`
228+
229+
## invalid(15): fetch(url, { method: 'post', body: 'foo=bar', method: 'HEAD', });
230+
231+
> Input
232+
233+
`␊
234+
1 | fetch(url, {␊
235+
2 | method: 'post',␊
236+
3 | body: 'foo=bar',␊
237+
4 | method: 'HEAD',␊
238+
5 | });␊
239+
`
240+
241+
> Error 1/1
242+
243+
`␊
244+
1 | fetch(url, {␊
245+
2 | method: 'post',␊
246+
> 3 | body: 'foo=bar',␊
247+
| ^^^^ "body" is not allowed when method is "HEAD".␊
248+
4 | method: 'HEAD',␊
249+
5 | });␊
250+
`
251+
252+
## invalid(16): new Request(url, { method: 'post', body: 'foo=bar', method: 'HEAD', });
253+
254+
> Input
255+
256+
`␊
257+
1 | new Request(url, {␊
258+
2 | method: 'post',␊
259+
3 | body: 'foo=bar',␊
260+
4 | method: 'HEAD',␊
261+
5 | });␊
262+
`
263+
264+
> Error 1/1
265+
266+
`␊
267+
1 | new Request(url, {␊
268+
2 | method: 'post',␊
269+
> 3 | body: 'foo=bar',␊
270+
| ^^^^ "body" is not allowed when method is "HEAD".␊
271+
4 | method: 'HEAD',␊
272+
5 | });␊
273+
`
841 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)
Please sign in to comment.