Skip to content

Commit 1bbaad9

Browse files
authored
fix: align with spec for Request method normalization (#30)
1 parent 06cd4f7 commit 1bbaad9

File tree

3 files changed

+82
-12
lines changed

3 files changed

+82
-12
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@remix-run/web-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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {getSearch} from './utils/get-search.js';
1616

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

19+
const forbiddenMethods = new Set(["CONNECT", "TRACE", "TRACK"]);
20+
const normalizedMethods = new Set(["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"]);
21+
1922
/**
2023
* Check if `obj` is an instance of Request.
2124
*
@@ -33,15 +36,15 @@ const isRequest = object => {
3336
/**
3437
* Request class
3538
* @implements {globalThis.Request}
36-
*
39+
*
3740
* @typedef {Object} RequestState
3841
* @property {string} method
3942
* @property {RequestRedirect} redirect
4043
* @property {globalThis.Headers} headers
4144
* @property {RequestCredentials} credentials
4245
* @property {URL} parsedURL
4346
* @property {AbortSignal|null} signal
44-
*
47+
*
4548
* @typedef {Object} RequestExtraOptions
4649
* @property {number} [follow]
4750
* @property {boolean} [compress]
@@ -50,15 +53,15 @@ const isRequest = object => {
5053
* @property {Agent} [agent]
5154
* @property {number} [highWaterMark]
5255
* @property {boolean} [insecureHTTPParser]
53-
*
56+
*
5457
* @typedef {((url:URL) => import('http').Agent | import('https').Agent) | import('http').Agent | import('https').Agent} Agent
55-
*
58+
*
5659
* @typedef {Object} RequestOptions
5760
* @property {string} [method]
5861
* @property {ReadableStream<Uint8Array>|null} [body]
5962
* @property {globalThis.Headers} [headers]
6063
* @property {RequestRedirect} [redirect]
61-
*
64+
*
6265
*/
6366
export default class Request extends Body {
6467
/**
@@ -81,8 +84,13 @@ export default class Request extends Body {
8184

8285

8386

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

8795
const inputBody = init.body != null
8896
? init.body
@@ -100,7 +108,7 @@ export default class Request extends Body {
100108
});
101109
const input = settings
102110

103-
111+
104112
const headers = /** @type {globalThis.Headers} */
105113
(new Headers(init.headers || input.headers || {}));
106114

@@ -121,7 +129,7 @@ export default class Request extends Body {
121129
if (signal != null && !isAbortSignal(signal)) {
122130
throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget');
123131
}
124-
132+
125133
if (!signal) {
126134
let AbortControllerConstructor = typeof AbortController != "undefined"
127135
? AbortController
@@ -180,11 +188,11 @@ export default class Request extends Body {
180188
get destination() {
181189
return ""
182190
}
183-
191+
184192
get integrity() {
185193
return ""
186194
}
187-
195+
188196
/** @type {RequestMode} */
189197
get mode() {
190198
return "cors"
@@ -194,7 +202,7 @@ export default class Request extends Body {
194202
get referrer() {
195203
return ""
196204
}
197-
205+
198206
/** @type {ReferrerPolicy} */
199207
get referrerPolicy() {
200208
return ""
@@ -318,7 +326,7 @@ export const getNodeRequestOptions = request => {
318326
port: parsedURL.port,
319327
hash: parsedURL.hash,
320328
search: parsedURL.search,
321-
// @ts-ignore - it does not has a query
329+
// @ts-ignore - it does not has a query
322330
query: parsedURL.query,
323331
href: parsedURL.href,
324332
method: request.method,

packages/fetch/test/request.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,59 @@ describe('Request', () => {
8888
expect(r2.counter).to.equal(0);
8989
});
9090

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

0 commit comments

Comments
 (0)