Skip to content

Commit 8050ec0

Browse files
authored
Use sets and reusable TextEncoder/TextDecoder instances (#2368)
* Use sets and reusable TextEncoder/TextDecoder instances * Do not reuse streaming decoder
1 parent 9197790 commit 8050ec0

7 files changed

Lines changed: 51 additions & 30 deletions

File tree

lib/fetch/body.js

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ let ReadableStream = globalThis.ReadableStream
2626

2727
/** @type {globalThis['File']} */
2828
const File = NativeFile ?? UndiciFile
29+
const textEncoder = new TextEncoder()
30+
const textDecoder = new TextDecoder()
2931

3032
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
3133
function extractBody (object, keepalive = false) {
@@ -49,7 +51,7 @@ function extractBody (object, keepalive = false) {
4951
stream = new ReadableStream({
5052
async pull (controller) {
5153
controller.enqueue(
52-
typeof source === 'string' ? new TextEncoder().encode(source) : source
54+
typeof source === 'string' ? textEncoder.encode(source) : source
5355
)
5456
queueMicrotask(() => readableStreamClose(controller))
5557
},
@@ -119,21 +121,20 @@ function extractBody (object, keepalive = false) {
119121
// - That the content-length is calculated in advance.
120122
// - And that all parts are pre-encoded and ready to be sent.
121123

122-
const enc = new TextEncoder()
123124
const blobParts = []
124125
const rn = new Uint8Array([13, 10]) // '\r\n'
125126
length = 0
126127
let hasUnknownSizeValue = false
127128

128129
for (const [name, value] of object) {
129130
if (typeof value === 'string') {
130-
const chunk = enc.encode(prefix +
131+
const chunk = textEncoder.encode(prefix +
131132
`; name="${escape(normalizeLinefeeds(name))}"` +
132133
`\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
133134
blobParts.push(chunk)
134135
length += chunk.byteLength
135136
} else {
136-
const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
137+
const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` +
137138
(value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' +
138139
`Content-Type: ${
139140
value.type || 'application/octet-stream'
@@ -147,7 +148,7 @@ function extractBody (object, keepalive = false) {
147148
}
148149
}
149150

150-
const chunk = enc.encode(`--${boundary}--`)
151+
const chunk = textEncoder.encode(`--${boundary}--`)
151152
blobParts.push(chunk)
152153
length += chunk.byteLength
153154
if (hasUnknownSizeValue) {
@@ -443,14 +444,16 @@ function bodyMixinMethods (instance) {
443444
let text = ''
444445
// application/x-www-form-urlencoded parser will keep the BOM.
445446
// https://url.spec.whatwg.org/#concept-urlencoded-parser
446-
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
447+
// Note that streaming decoder is stateful and cannot be reused
448+
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
449+
447450
for await (const chunk of consumeBody(this[kState].body)) {
448451
if (!isUint8Array(chunk)) {
449452
throw new TypeError('Expected Uint8Array chunk')
450453
}
451-
text += textDecoder.decode(chunk, { stream: true })
454+
text += streamingDecoder.decode(chunk, { stream: true })
452455
}
453-
text += textDecoder.decode()
456+
text += streamingDecoder.decode()
454457
entries = new URLSearchParams(text)
455458
} catch (err) {
456459
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
@@ -565,7 +568,7 @@ function utf8DecodeBytes (buffer) {
565568

566569
// 3. Process a queue with an instance of UTF-8’s
567570
// decoder, ioQueue, output, and "replacement".
568-
const output = new TextDecoder().decode(buffer)
571+
const output = textDecoder.decode(buffer)
569572

570573
// 4. Return output.
571574
return output

lib/fetch/constants.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
const { MessageChannel, receiveMessageOnPort } = require('worker_threads')
44

55
const corsSafeListedMethods = ['GET', 'HEAD', 'POST']
6+
const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
67

78
const nullBodyStatus = [101, 204, 205, 304]
89

910
const redirectStatus = [301, 302, 303, 307, 308]
11+
const redirectStatusSet = new Set(redirectStatus)
1012

1113
// https://fetch.spec.whatwg.org/#block-bad-port
1214
const badPorts = [
@@ -18,6 +20,8 @@ const badPorts = [
1820
'10080'
1921
]
2022

23+
const badPortsSet = new Set(badPorts)
24+
2125
// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
2226
const referrerPolicy = [
2327
'',
@@ -30,10 +34,12 @@ const referrerPolicy = [
3034
'strict-origin-when-cross-origin',
3135
'unsafe-url'
3236
]
37+
const referrerPolicySet = new Set(referrerPolicy)
3338

3439
const requestRedirect = ['follow', 'manual', 'error']
3540

3641
const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']
42+
const safeMethodsSet = new Set(safeMethods)
3743

3844
const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']
3945

@@ -68,6 +74,7 @@ const requestDuplex = [
6874

6975
// http://fetch.spec.whatwg.org/#forbidden-method
7076
const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']
77+
const forbiddenMethodsSet = new Set(forbiddenMethods)
7178

7279
const subresource = [
7380
'audio',
@@ -83,6 +90,7 @@ const subresource = [
8390
'xslt',
8491
''
8592
]
93+
const subresourceSet = new Set(subresource)
8694

8795
/** @type {globalThis['DOMException']} */
8896
const DOMException = globalThis.DOMException ?? (() => {
@@ -132,5 +140,12 @@ module.exports = {
132140
nullBodyStatus,
133141
safeMethods,
134142
badPorts,
135-
requestDuplex
143+
requestDuplex,
144+
subresourceSet,
145+
badPortsSet,
146+
redirectStatusSet,
147+
corsSafeListedMethodsSet,
148+
safeMethodsSet,
149+
forbiddenMethodsSet,
150+
referrerPolicySet
136151
}

lib/fetch/file.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { isBlobLike } = require('./util')
77
const { webidl } = require('./webidl')
88
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
99
const { kEnumerableProperty } = require('../core/util')
10+
const encoder = new TextEncoder()
1011

1112
class File extends Blob {
1213
constructor (fileBits, fileName, options = {}) {
@@ -280,7 +281,7 @@ function processBlobParts (parts, options) {
280281
}
281282

282283
// 3. Append the result of UTF-8 encoding s to bytes.
283-
bytes.push(new TextEncoder().encode(s))
284+
bytes.push(encoder.encode(s))
284285
} else if (
285286
types.isAnyArrayBuffer(element) ||
286287
types.isTypedArray(element)

lib/fetch/index.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
4646
const assert = require('assert')
4747
const { safelyExtractBody } = require('./body')
4848
const {
49-
redirectStatus,
49+
redirectStatusSet,
5050
nullBodyStatus,
51-
safeMethods,
51+
safeMethodsSet,
5252
requestBodyHeader,
53-
subresource,
53+
subresourceSet,
5454
DOMException
5555
} = require('./constants')
5656
const { kHeadersList } = require('../core/symbols')
@@ -62,6 +62,7 @@ const { TransformStream } = require('stream/web')
6262
const { getGlobalDispatcher } = require('../global')
6363
const { webidl } = require('./webidl')
6464
const { STATUS_CODES } = require('http')
65+
const GET_OR_HEAD = ['GET', 'HEAD']
6566

6667
/** @type {import('buffer').resolveObjectURL} */
6768
let resolveObjectURL
@@ -509,7 +510,7 @@ function fetching ({
509510
}
510511

511512
// 15. If request is a subresource request, then:
512-
if (subresource.includes(request.destination)) {
513+
if (subresourceSet.has(request.destination)) {
513514
// TODO
514515
}
515516

@@ -1063,7 +1064,7 @@ async function httpFetch (fetchParams) {
10631064
}
10641065

10651066
// 8. If actualResponse’s status is a redirect status, then:
1066-
if (redirectStatus.includes(actualResponse.status)) {
1067+
if (redirectStatusSet.has(actualResponse.status)) {
10671068
// 1. If actualResponse’s status is not 303, request’s body is not null,
10681069
// and the connection uses HTTP/2, then user agents may, and are even
10691070
// encouraged to, transmit an RST_STREAM frame.
@@ -1181,7 +1182,7 @@ function httpRedirectFetch (fetchParams, response) {
11811182
if (
11821183
([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
11831184
(actualResponse.status === 303 &&
1184-
!['GET', 'HEAD'].includes(request.method))
1185+
!GET_OR_HEAD.includes(request.method))
11851186
) {
11861187
// then:
11871188
// 1. Set request’s method to `GET` and request’s body to null.
@@ -1465,7 +1466,7 @@ async function httpNetworkOrCacheFetch (
14651466
// responses in httpCache, as per the "Invalidation" chapter of HTTP
14661467
// Caching, and set storedResponse to null. [HTTP-CACHING]
14671468
if (
1468-
!safeMethods.includes(httpRequest.method) &&
1469+
!safeMethodsSet.has(httpRequest.method) &&
14691470
forwardResponse.status >= 200 &&
14701471
forwardResponse.status <= 399
14711472
) {
@@ -2025,7 +2026,7 @@ async function httpNetworkFetch (
20252026

20262027
const willFollow = request.redirect === 'follow' &&
20272028
location &&
2028-
redirectStatus.includes(status)
2029+
redirectStatusSet.has(status)
20292030

20302031
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
20312032
if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {

lib/fetch/request.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ const {
1313
makePolicyContainer
1414
} = require('./util')
1515
const {
16-
forbiddenMethods,
17-
corsSafeListedMethods,
16+
forbiddenMethodsSet,
17+
corsSafeListedMethodsSet,
1818
referrerPolicy,
1919
requestRedirect,
2020
requestMode,
@@ -319,7 +319,7 @@ class Request {
319319
throw TypeError(`'${init.method}' is not a valid HTTP method.`)
320320
}
321321

322-
if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) {
322+
if (forbiddenMethodsSet.has(method.toUpperCase())) {
323323
throw TypeError(`'${init.method}' HTTP method is unsupported.`)
324324
}
325325

@@ -404,7 +404,7 @@ class Request {
404404
if (mode === 'no-cors') {
405405
// 1. If this’s request’s method is not a CORS-safelisted method,
406406
// then throw a TypeError.
407-
if (!corsSafeListedMethods.includes(request.method)) {
407+
if (!corsSafeListedMethodsSet.has(request.method)) {
408408
throw new TypeError(
409409
`'${request.method} is unsupported in no-cors mode.`
410410
)

lib/fetch/response.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const {
1414
isomorphicEncode
1515
} = require('./util')
1616
const {
17-
redirectStatus,
17+
redirectStatusSet,
1818
nullBodyStatus,
1919
DOMException
2020
} = require('./constants')
@@ -28,6 +28,7 @@ const assert = require('assert')
2828
const { types } = require('util')
2929

3030
const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream
31+
const textEncoder = new TextEncoder('utf-8')
3132

3233
// https://fetch.spec.whatwg.org/#response-class
3334
class Response {
@@ -57,7 +58,7 @@ class Response {
5758
}
5859

5960
// 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
60-
const bytes = new TextEncoder('utf-8').encode(
61+
const bytes = textEncoder.encode(
6162
serializeJavascriptValueToJSONString(data)
6263
)
6364

@@ -102,7 +103,7 @@ class Response {
102103
}
103104

104105
// 3. If status is not a redirect status, then throw a RangeError.
105-
if (!redirectStatus.includes(status)) {
106+
if (!redirectStatusSet.has(status)) {
106107
throw new RangeError('Invalid status code ' + status)
107108
}
108109

lib/fetch/util.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants')
3+
const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require('./constants')
44
const { getGlobalOrigin } = require('./global')
55
const { performance } = require('perf_hooks')
66
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
@@ -29,7 +29,7 @@ function responseURL (response) {
2929
// https://fetch.spec.whatwg.org/#concept-response-location-url
3030
function responseLocationURL (response, requestFragment) {
3131
// 1. If response’s status is not a redirect status, then return null.
32-
if (!redirectStatus.includes(response.status)) {
32+
if (!redirectStatusSet.has(response.status)) {
3333
return null
3434
}
3535

@@ -64,7 +64,7 @@ function requestBadPort (request) {
6464

6565
// 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
6666
// then return blocked.
67-
if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) {
67+
if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
6868
return 'blocked'
6969
}
7070

@@ -206,7 +206,7 @@ function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
206206
// The left-most policy is the fallback.
207207
for (let i = policyHeader.length; i !== 0; i--) {
208208
const token = policyHeader[i - 1].trim()
209-
if (referrerPolicyTokens.includes(token)) {
209+
if (referrerPolicyTokens.has(token)) {
210210
policy = token
211211
break
212212
}

0 commit comments

Comments
 (0)