Skip to content

Commit b6ae2a8

Browse files
authored
fix(fetch): multiple data: url fixes & move tests to wpt runner (nodejs#1678)
1 parent 39a21cf commit b6ae2a8

File tree

12 files changed

+133
-110
lines changed

12 files changed

+133
-110
lines changed

lib/fetch/dataURL.js

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const assert = require('assert')
22
const { atob } = require('buffer')
3+
const { isValidHTTPToken } = require('./util')
34

45
const encoder = new TextEncoder()
56

@@ -376,9 +377,7 @@ function parseMIMEType (input) {
376377
// 1. Set parameterValue to the result of collecting
377378
// an HTTP quoted string from input, given position
378379
// and the extract-value flag.
379-
// Undici implementation note: extract-value is never
380-
// defined or mentioned anywhere.
381-
parameterValue = collectAnHTTPQuotedString(input, position/*, extractValue */)
380+
parameterValue = collectAnHTTPQuotedString(input, position, true)
382381

383382
// 2. Collect a sequence of code points that are not
384383
// U+003B (;) from input, given position.
@@ -400,7 +399,8 @@ function parseMIMEType (input) {
400399
)
401400

402401
// 2. Remove any trailing HTTP whitespace from parameterValue.
403-
parameterValue = parameterValue.trim()
402+
// Note: it says "trailing" whitespace; leading is fine.
403+
parameterValue = parameterValue.trimEnd()
404404

405405
// 3. If parameterValue is the empty string, then continue.
406406
if (parameterValue.length === 0) {
@@ -547,11 +547,56 @@ function collectAnHTTPQuotedString (input, position, extractValue) {
547547
return input.slice(positionStart, position.position)
548548
}
549549

550+
/**
551+
* @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
552+
*/
553+
function serializeAMimeType (mimeType) {
554+
assert(mimeType !== 'failure')
555+
const { type, subtype, parameters } = mimeType
556+
557+
// 1. Let serialization be the concatenation of mimeType’s
558+
// type, U+002F (/), and mimeType’s subtype.
559+
let serialization = `${type}/${subtype}`
560+
561+
// 2. For each name → value of mimeType’s parameters:
562+
for (let [name, value] of parameters.entries()) {
563+
// 1. Append U+003B (;) to serialization.
564+
serialization += ';'
565+
566+
// 2. Append name to serialization.
567+
serialization += name
568+
569+
// 3. Append U+003D (=) to serialization.
570+
serialization += '='
571+
572+
// 4. If value does not solely contain HTTP token code
573+
// points or value is the empty string, then:
574+
if (!isValidHTTPToken(value)) {
575+
// 1. Precede each occurence of U+0022 (") or
576+
// U+005C (\) in value with U+005C (\).
577+
value = value.replace(/(\\|")/g, '\\$1')
578+
579+
// 2. Prepend U+0022 (") to value.
580+
value = '"' + value
581+
582+
// 3. Append U+0022 (") to value.
583+
value += '"'
584+
}
585+
586+
// 5. Append value to serialization.
587+
serialization += value
588+
}
589+
590+
// 3. Return serialization.
591+
return serialization
592+
}
593+
550594
module.exports = {
551595
dataURLProcessor,
552596
URLSerializer,
553597
collectASequenceOfCodePoints,
554598
stringPercentDecode,
555599
parseMIMEType,
556-
collectAnHTTPQuotedString
600+
collectAnHTTPQuotedString,
601+
serializeAMimeType
557602
}

lib/fetch/index.js

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const { kHeadersList } = require('../core/symbols')
5252
const EE = require('events')
5353
const { Readable, pipeline } = require('stream')
5454
const { isErrored, isReadable } = require('../core/util')
55-
const { dataURLProcessor } = require('./dataURL')
55+
const { dataURLProcessor, serializeAMimeType } = require('./dataURL')
5656
const { TransformStream } = require('stream/web')
5757

5858
/** @type {import('buffer').resolveObjectURL} */
@@ -832,33 +832,15 @@ async function schemeFetch (fetchParams) {
832832
}
833833

834834
// 3. Let mimeType be dataURLStruct’s MIME type, serialized.
835-
const { mimeType } = dataURLStruct
836-
837-
/** @type {string} */
838-
let contentType = `${mimeType.type}/${mimeType.subtype}`
839-
const contentTypeParams = []
840-
841-
if (mimeType.parameters.size > 0) {
842-
contentType += ';'
843-
}
844-
845-
for (const [key, value] of mimeType.parameters) {
846-
if (value.length > 0) {
847-
contentTypeParams.push(`${key}=${value}`)
848-
} else {
849-
contentTypeParams.push(key)
850-
}
851-
}
852-
853-
contentType += contentTypeParams.join(',')
835+
const mimeType = serializeAMimeType(dataURLStruct.mimeType)
854836

855837
// 4. Return a response whose status message is `OK`,
856838
// header list is « (`Content-Type`, mimeType) »,
857839
// and body is dataURLStruct’s body.
858840
return makeResponse({
859841
statusText: 'OK',
860842
headersList: [
861-
['content-type', contentType]
843+
['content-type', mimeType]
862844
],
863845
body: extractBody(dataURLStruct.body)[0]
864846
})

test/fetch/data-uri.js

Lines changed: 14 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ const {
99
collectAnHTTPQuotedString
1010
} = require('../../lib/fetch/dataURL')
1111
const { fetch } = require('../..')
12-
const base64tests = require('./resources/base64.json')
13-
const dataURLtests = require('./resources/data-urls.json')
1412

1513
test('https://url.spec.whatwg.org/#concept-url-serializer', (t) => {
1614
t.test('url scheme gets appended', (t) => {
@@ -121,7 +119,7 @@ test('https://mimesniff.spec.whatwg.org/#parse-a-mime-type', (t) => {
121119
t.same(parseMIMEType('text/html;charset="shift_jis"iso-2022-jp'), {
122120
type: 'text',
123121
subtype: 'html',
124-
parameters: new Map([['charset', '"shift_jis"']])
122+
parameters: new Map([['charset', 'shift_jis']])
125123
})
126124

127125
t.same(parseMIMEType('application/javascript'), {
@@ -161,71 +159,18 @@ test('https://fetch.spec.whatwg.org/#collect-an-http-quoted-string', (t) => {
161159
t.end()
162160
})
163161

164-
// https://github.com/web-platform-tests/wpt/blob/master/fetch/data-urls/resources/base64.json
165-
// https://github.com/web-platform-tests/wpt/blob/master/fetch/data-urls/base64.any.js
166-
test('base64.any.js', async (t) => {
167-
for (const [input, output] of base64tests) {
168-
const dataURL = `data:;base64,${input}`
169-
170-
if (output === null) {
171-
await t.rejects(fetch(dataURL), TypeError)
172-
continue
173-
}
174-
175-
try {
176-
const res = await fetch(dataURL)
177-
const body = await res.arrayBuffer()
178-
179-
t.same(
180-
new Uint8Array(body),
181-
new Uint8Array(output)
182-
)
183-
} catch (e) {
184-
t.fail(`failed to fetch ${dataURL}`)
185-
}
162+
// https://github.com/nodejs/undici/issues/1574
163+
test('too long base64 url', async (t) => {
164+
const inputStr = 'a'.repeat(1 << 20)
165+
const base64 = Buffer.from(inputStr).toString('base64')
166+
const dataURIPrefix = 'data:application/octet-stream;base64,'
167+
const dataURL = dataURIPrefix + base64
168+
try {
169+
const res = await fetch(dataURL)
170+
const buf = await res.arrayBuffer()
171+
const outputStr = Buffer.from(buf).toString('ascii')
172+
t.same(outputStr, inputStr)
173+
} catch (e) {
174+
t.fail(`failed to fetch ${dataURL}`)
186175
}
187176
})
188-
189-
test('processing.any.js', async (t) => {
190-
for (const [input, expectedMimeType, expectedBody = null] of dataURLtests) {
191-
if (expectedMimeType === null) {
192-
try {
193-
await fetch(input)
194-
t.fail(`fetching "${input}" was expected to fail`)
195-
} catch (e) {
196-
t.ok(e, 'got expected error')
197-
continue
198-
}
199-
}
200-
201-
try {
202-
const res = await fetch(input)
203-
const body = await res.arrayBuffer()
204-
205-
t.same(
206-
new Uint8Array(body),
207-
new Uint8Array(expectedBody)
208-
)
209-
} catch (e) {
210-
t.fail(`failed on '${input}'`)
211-
}
212-
}
213-
214-
// https://github.com/nodejs/undici/issues/1574
215-
test('too long base64 url', async (t) => {
216-
const inputStr = 'a'.repeat(1 << 20)
217-
const base64 = Buffer.from(inputStr).toString('base64')
218-
const dataURIPrefix = 'data:application/octet-stream;base64,'
219-
const dataURL = dataURIPrefix + base64
220-
try {
221-
const res = await fetch(dataURL)
222-
const buf = await res.arrayBuffer()
223-
const outputStr = Buffer.from(buf).toString('ascii')
224-
t.same(outputStr, inputStr)
225-
} catch (e) {
226-
t.fail(`failed to fetch ${dataURL}`)
227-
}
228-
})
229-
230-
t.end()
231-
})

test/wpt/runner/runner/runner.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ export class WPTRunner extends EventEmitter {
3636
super()
3737

3838
this.#folderPath = join(testPath, folder)
39-
this.#files.push(...WPTRunner.walk(this.#folderPath, () => true))
39+
this.#files.push(...WPTRunner.walk(
40+
this.#folderPath,
41+
(file) => file.endsWith('.js')
42+
))
4043
this.#status = JSON.parse(readFileSync(join(statusPath, `${folder}.status.json`)))
4144
this.#url = url
4245

test/wpt/runner/runner/worker.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { join } from 'node:path'
12
import { runInThisContext } from 'node:vm'
23
import { parentPort, workerData } from 'node:worker_threads'
34
import {
@@ -10,7 +11,10 @@ import {
1011
Headers
1112
} from '../../../../index.js'
1213

13-
const { initScripts, meta, test, url } = workerData
14+
const { initScripts, meta, test, url, path } = workerData
15+
16+
const basePath = join(process.cwd(), 'test/wpt/tests')
17+
const urlPath = path.slice(basePath.length)
1418

1519
const globalPropertyDescriptors = {
1620
writable: true,
@@ -86,7 +90,7 @@ add_completion_callback((_, status) => {
8690
})
8791
})
8892

89-
setGlobalOrigin(url)
93+
setGlobalOrigin(new URL(urlPath, url))
9094

9195
// Inject any script the user provided before
9296
// running the tests.

test/wpt/server/server.mjs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'
66
import { createReadStream } from 'node:fs'
77
import { setTimeout as sleep } from 'node:timers/promises'
88

9-
const resources = fileURLToPath(join(import.meta.url, '../../runner/resources'))
9+
const tests = fileURLToPath(join(import.meta.url, '../../tests'))
1010

1111
// https://web-platform-tests.org/tools/wptserve/docs/stash.html
1212
class Stash extends Map {
@@ -30,13 +30,16 @@ const server = createServer(async (req, res) => {
3030
const fullUrl = new URL(req.url, `http://localhost:${server.address().port}`)
3131

3232
switch (fullUrl.pathname) {
33-
case '/resources/data.json': {
33+
case '/fetch/data-urls/resources/base64.json':
34+
case '/fetch/data-urls/resources/data-urls.json':
35+
case '/fetch/api/resources/empty.txt':
36+
case '/fetch/api/resources/data.json': {
3437
// https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/data.json
35-
return createReadStream(join(resources, 'data.json'))
38+
return createReadStream(join(tests, fullUrl.pathname))
3639
.on('end', () => res.end())
3740
.pipe(res)
3841
}
39-
case '/resources/infinite-slow-response.py': {
42+
case '/fetch/api/resources/infinite-slow-response.py': {
4043
// https://github.com/web-platform-tests/wpt/blob/master/fetch/api/resources/infinite-slow-response.py
4144
const stateKey = fullUrl.searchParams.get('stateKey') ?? ''
4245
const abortKey = fullUrl.searchParams.get('abortKey') ?? ''
@@ -66,7 +69,7 @@ const server = createServer(async (req, res) => {
6669

6770
return res.end()
6871
}
69-
case '/resources/stash-take.py': {
72+
case '/fetch/api/resources/stash-take.py': {
7073
// https://github.com/web-platform-tests/wpt/blob/6ae3f702a332e8399fab778c831db6b7dca3f1c6/fetch/api/resources/stash-take.py
7174

7275
const key = fullUrl.searchParams.get('key')
@@ -77,11 +80,6 @@ const server = createServer(async (req, res) => {
7780
res.write(JSON.stringify(took))
7881
return res.end()
7982
}
80-
case '/resources/empty.txt': {
81-
return createReadStream(join(resources, 'empty.txt'))
82-
.on('end', () => res.end())
83-
.pipe(res)
84-
}
8583
default: {
8684
res.statusCode = 200
8785
res.end('body')
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// META: global=window,worker
2+
3+
promise_test(() => fetch("resources/base64.json").then(res => res.json()).then(runBase64Tests), "Setup.");
4+
function runBase64Tests(tests) {
5+
for(let i = 0; i < tests.length; i++) {
6+
const input = tests[i][0],
7+
output = tests[i][1],
8+
dataURL = "data:;base64," + input;
9+
promise_test(t => {
10+
if(output === null) {
11+
return promise_rejects_js(t, TypeError, fetch(dataURL));
12+
}
13+
return fetch(dataURL).then(res => res.arrayBuffer()).then(body => {
14+
assert_array_equals(new Uint8Array(body), output);
15+
});
16+
}, "data: URL base64 handling: " + format_value(input));
17+
}
18+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// META: global=window,worker
2+
3+
promise_test(() => fetch("resources/data-urls.json").then(res => res.json()).then(runDataURLTests), "Setup.");
4+
function runDataURLTests(tests) {
5+
for(let i = 0; i < tests.length; i++) {
6+
const input = tests[i][0],
7+
expectedMimeType = tests[i][1],
8+
expectedBody = expectedMimeType !== null ? tests[i][2] : null;
9+
promise_test(t => {
10+
if(expectedMimeType === null) {
11+
return promise_rejects_js(t, TypeError, fetch(input));
12+
} else {
13+
return fetch(input).then(res => {
14+
return res.arrayBuffer().then(body => {
15+
assert_array_equals(new Uint8Array(body), expectedBody);
16+
assert_equals(res.headers.get("content-type"), expectedMimeType); // We could assert this earlier, but this fails often
17+
});
18+
});
19+
}
20+
}, format_value(input));
21+
}
22+
}

0 commit comments

Comments
 (0)