Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const {
isBlobLike,
buildURL,
validateHandler,
getServerName
getServerName,
normalizedMethodRecords
} = require('./util')
const { channels } = require('./diagnostics.js')
const { headerNameLowerCasedRecord } = require('./constants')
Expand Down Expand Up @@ -51,13 +52,13 @@ class Request {
method !== 'CONNECT'
) {
throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
} else if (invalidPathRegex.exec(path) !== null) {
} else if (invalidPathRegex.test(path)) {
throw new InvalidArgumentError('invalid request path')
}

if (typeof method !== 'string') {
throw new InvalidArgumentError('method must be a string')
} else if (!isValidHTTPToken(method)) {
} else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
throw new InvalidArgumentError('invalid request method')
}

Expand Down
21 changes: 21 additions & 0 deletions lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,26 @@ function errorRequest (client, request, err) {
const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true

const normalizedMethodRecords = {
delete: 'DELETE',
DELETE: 'DELETE',
get: 'GET',
GET: 'GET',
head: 'HEAD',
HEAD: 'HEAD',
options: 'OPTIONS',
OPTIONS: 'OPTIONS',
post: 'POST',
POST: 'POST',
put: 'PUT',
PUT: 'PUT',
patch: 'patch',
PATCH: 'PATCH'
}

// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(normalizedMethodRecords, null)

module.exports = {
kEnumerableProperty,
nop,
Expand Down Expand Up @@ -600,6 +620,7 @@ module.exports = {
isValidHeaderChar,
isTokenCharCode,
parseRangeHeader,
normalizedMethodRecords,
nodeMajor,
nodeMinor,
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
Expand Down
34 changes: 8 additions & 26 deletions lib/web/fetch/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ const { FinalizationRegistry } = require('./dispatcher-weakref')()
const util = require('../../core/util')
const nodeUtil = require('node:util')
const {
isValidHTTPToken,
sameOrigin,
normalizeMethod,
makePolicyContainer,
normalizeMethodRecord
validateAndNormalizeMethod
} = require('./util')
const {
forbiddenMethodsSet,
corsSafeListedMethodsSet,
referrerPolicy,
requestRedirect,
Expand All @@ -24,7 +21,7 @@ const {
requestCache,
requestDuplex
} = require('./constants')
const { kEnumerableProperty } = util
const { kEnumerableProperty, normalizedMethodRecords } = util
const { kHeaders, kSignal, kState, kGuard, kRealm, kDispatcher } = require('./symbols')
const { webidl } = require('./webidl')
const { getGlobalOrigin } = require('./global')
Expand Down Expand Up @@ -318,30 +315,15 @@ class Request {
// 25. If init["method"] exists, then:
if (init.method !== undefined) {
// 1. Let method be init["method"].
let method = init.method
const method = init.method

const mayBeNormalized = normalizeMethodRecord[method]
// 2. If method is not a method or method is a forbidden method, then
// throw a TypeError.

if (mayBeNormalized !== undefined) {
// Note: Bypass validation DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH and these lowercase ones
request.method = mayBeNormalized
} else {
// 2. If method is not a method or method is a forbidden method, then
// throw a TypeError.
if (!isValidHTTPToken(method)) {
throw new TypeError(`'${method}' is not a valid HTTP method.`)
}

if (forbiddenMethodsSet.has(method.toUpperCase())) {
throw new TypeError(`'${method}' HTTP method is unsupported.`)
}

// 3. Normalize method.
method = normalizeMethod(method)
// 3. Normalize method.

// 4. Set request’s method to method.
request.method = method
}
// 4. Set request’s method to method.
request.method = normalizedMethodRecords[method] ?? validateAndNormalizeMethod(method)

if (!patchMethodWarning && request.method === 'patch') {
process.emitWarning('Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.', {
Expand Down
47 changes: 16 additions & 31 deletions lib/web/fetch/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet
const { getGlobalOrigin } = require('./global')
const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url')
const { performance } = require('node:perf_hooks')
const { isBlobLike, ReadableStreamFrom, isValidHTTPToken } = require('../../core/util')
const { isBlobLike, ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecords } = require('../../core/util')
const assert = require('node:assert')
const { isUint8Array } = require('node:util/types')
const { webidl } = require('./webidl')

const { forbiddenMethodsSet } = require('./constants')
let supportedHashes = []

// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable
Expand Down Expand Up @@ -783,37 +783,23 @@ function isCancelled (fetchParams) {
fetchParams.controller.state === 'terminated'
}

const normalizeMethodRecordBase = {
delete: 'DELETE',
DELETE: 'DELETE',
get: 'GET',
GET: 'GET',
head: 'HEAD',
HEAD: 'HEAD',
options: 'OPTIONS',
OPTIONS: 'OPTIONS',
post: 'POST',
POST: 'POST',
put: 'PUT',
PUT: 'PUT'
}

const normalizeMethodRecord = {
...normalizeMethodRecordBase,
patch: 'patch',
PATCH: 'PATCH'
}

// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`.
Object.setPrototypeOf(normalizeMethodRecordBase, null)
Object.setPrototypeOf(normalizeMethodRecord, null)

/**
* @see https://fetch.spec.whatwg.org/#concept-method-normalize
* @param {string} method
*/
function normalizeMethod (method) {
return normalizeMethodRecordBase[method.toLowerCase()] ?? method
function validateAndNormalizeMethod (method) {
if (!isValidHTTPToken(method)) {
throw new TypeError(`'${method}' is not a valid HTTP method.`)
}

const upperCase = method.toUpperCase()

if (forbiddenMethodsSet.has(upperCase)) {
throw new TypeError(`'${method}' HTTP method is unsupported.`)
}

// Note: must be in uppercase
return normalizedMethodRecords[upperCase] ?? method
}

// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
Expand Down Expand Up @@ -1590,7 +1576,7 @@ module.exports = {
isURLPotentiallyTrustworthy,
isValidReasonPhrase,
sameOrigin,
normalizeMethod,
validateAndNormalizeMethod,
serializeJavascriptValueToJSONString,
iteratorMixin,
createIterator,
Expand All @@ -1606,7 +1592,6 @@ module.exports = {
urlHasHttpsScheme,
urlIsHttpHttpsScheme,
readAllBytes,
normalizeMethodRecord,
simpleRangeHeaderValue,
buildContentRange,
parseMetadata,
Expand Down