Skip to content

Commit 342aafb

Browse files
authored
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+
});

0 commit comments

Comments
 (0)