Skip to content

Commit ace4223

Browse files
fix(fetch): align Request with spec (#85)
* fix: align with spec for Request method normalization * chore: fix changeset --------- Co-authored-by: Matt Brophy <[email protected]>
1 parent c1339b9 commit ace4223

File tree

3 files changed

+81
-11
lines changed

3 files changed

+81
-11
lines changed

.changeset/align-method-to-spec.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@web-std/fetch": patch
3+
---
4+
5+
Align with [spec](https://fetch.spec.whatwg.org/#methods) for `new Request()` `method` normalization
6+
7+
- Only `DELETE`, `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT` get automatically uppercased
8+
- Note that `method: "patch"` will no longer be automatically uppercased
9+
- Throw a `TypeError` for `CONNECT`, `TRACE`, and `TRACK`

packages/fetch/src/request.js

+19-11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ import {getSearch} from './utils/get-search.js';
1515

1616
const INTERNALS = Symbol('Request internals');
1717

18+
const forbiddenMethods = new Set(["CONNECT", "TRACE", "TRACK"]);
19+
const normalizedMethods = new Set(["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"]);
20+
1821
/**
1922
* Check if `obj` is an instance of Request.
2023
*
@@ -32,15 +35,15 @@ const isRequest = object => {
3235
/**
3336
* Request class
3437
* @implements {globalThis.Request}
35-
*
38+
*
3639
* @typedef {Object} RequestState
3740
* @property {string} method
3841
* @property {RequestRedirect} redirect
3942
* @property {globalThis.Headers} headers
4043
* @property {RequestCredentials} credentials
4144
* @property {URL} parsedURL
4245
* @property {AbortSignal|null} signal
43-
*
46+
*
4447
* @typedef {Object} RequestExtraOptions
4548
* @property {number} [follow]
4649
* @property {boolean} [compress]
@@ -49,15 +52,15 @@ const isRequest = object => {
4952
* @property {Agent} [agent]
5053
* @property {number} [highWaterMark]
5154
* @property {boolean} [insecureHTTPParser]
52-
*
55+
*
5356
* @typedef {((url:URL) => import('http').Agent) | import('http').Agent} Agent
54-
*
57+
*
5558
* @typedef {Object} RequestOptions
5659
* @property {string} [method]
5760
* @property {ReadableStream<Uint8Array>|null} [body]
5861
* @property {globalThis.Headers} [headers]
5962
* @property {RequestRedirect} [redirect]
60-
*
63+
*
6164
*/
6265
export default class Request extends Body {
6366
/**
@@ -80,8 +83,13 @@ export default class Request extends Body {
8083

8184

8285

86+
// Normalize method: https://fetch.spec.whatwg.org/#methods
8387
let method = init.method || settings.method || 'GET';
84-
method = method.toUpperCase();
88+
if (forbiddenMethods.has(method.toUpperCase())) {
89+
throw new TypeError(`Failed to construct 'Request': '${method}' HTTP method is unsupported.`)
90+
} else if (normalizedMethods.has(method.toUpperCase())) {
91+
method = method.toUpperCase();
92+
}
8593

8694
const inputBody = init.body != null
8795
? init.body
@@ -99,7 +107,7 @@ export default class Request extends Body {
99107
});
100108
const input = settings
101109

102-
110+
103111
const headers = /** @type {globalThis.Headers} */
104112
(new Headers(init.headers || input.headers || {}));
105113

@@ -170,11 +178,11 @@ export default class Request extends Body {
170178
get destination() {
171179
return ""
172180
}
173-
181+
174182
get integrity() {
175183
return ""
176184
}
177-
185+
178186
/** @type {RequestMode} */
179187
get mode() {
180188
return "cors"
@@ -184,7 +192,7 @@ export default class Request extends Body {
184192
get referrer() {
185193
return ""
186194
}
187-
195+
188196
/** @type {ReferrerPolicy} */
189197
get referrerPolicy() {
190198
return ""
@@ -308,7 +316,7 @@ export const getNodeRequestOptions = request => {
308316
port: parsedURL.port,
309317
hash: parsedURL.hash,
310318
search: parsedURL.search,
311-
// @ts-ignore - it does not has a query
319+
// @ts-ignore - it does not has a query
312320
query: parsedURL.query,
313321
href: parsedURL.href,
314322
method: request.method,

packages/fetch/test/request.js

+53
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,59 @@ describe('Request', () => {
8686
expect(r2.counter).to.equal(0);
8787
});
8888

89+
it('should throw a TypeError for forbidden methods', () => {
90+
// https://fetch.spec.whatwg.org/#methods
91+
const forbiddenMethods = [
92+
"CONNECT",
93+
"TRACE",
94+
"TRACK",
95+
];
96+
97+
forbiddenMethods.forEach(method => {
98+
try {
99+
new Request(base, { method: method.toLowerCase() });
100+
expect(true).to.equal(false);
101+
} catch (e) {
102+
expect(e instanceof TypeError).to.equal(true);
103+
expect(e.message).to.equal(`Failed to construct 'Request': '${method.toLowerCase()}' HTTP method is unsupported.`)
104+
}
105+
try {
106+
new Request(base, { method: method.toUpperCase() });
107+
expect(true).to.equal(false);
108+
} catch (e) {
109+
expect(e instanceof TypeError).to.equal(true);
110+
expect(e.message).to.equal(`Failed to construct 'Request': '${method.toUpperCase()}' HTTP method is unsupported.`)
111+
}
112+
});
113+
});
114+
115+
it('should normalize method', () => {
116+
// https://fetch.spec.whatwg.org/#methods
117+
const shouldUpperCaseMethods = [
118+
"DELETE",
119+
"GET",
120+
"HEAD",
121+
"OPTIONS",
122+
"POST",
123+
"PUT",
124+
];
125+
const otherMethods = ["PATCH", "CHICKEN"];
126+
127+
shouldUpperCaseMethods.forEach(method => {
128+
const r1 = new Request(base, { method: method.toLowerCase() });
129+
expect(r1.method).to.equal(method.toUpperCase());
130+
const r2 = new Request(base, { method: method.toUpperCase() });
131+
expect(r2.method).to.equal(method.toUpperCase());
132+
});
133+
134+
otherMethods.forEach(method => {
135+
const r1 = new Request(base, { method: method.toLowerCase() });
136+
expect(r1.method).to.equal(method.toLowerCase());
137+
const r2 = new Request(base, { method: method.toUpperCase() });
138+
expect(r2.method).to.equal(method.toUpperCase());
139+
});
140+
});
141+
89142
it('should override signal on derived Request instances', () => {
90143
const parentAbortController = new AbortController();
91144
const derivedAbortController = new AbortController();

0 commit comments

Comments
 (0)