Skip to content

Commit 733b3a0

Browse files
authored
feat(fetch): add Request{Init}.duplex and add WPTs (nodejs#1681)
* feat(fetch): add `Request.duplex` and add WPTs * feat: add remaining applicable tests
1 parent 0964a83 commit 733b3a0

18 files changed

+936
-64
lines changed

lib/fetch/request.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -472,15 +472,21 @@ class Request {
472472
// 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
473473
// null, then:
474474
if (inputOrInitBody != null && inputOrInitBody.source == null) {
475-
// 1. If this’s request’s mode is neither "same-origin" nor "cors",
475+
// 1. If initBody is non-null and init["duplex"] does not exist,
476+
// then throw a TypeError.
477+
if (initBody != null && init.duplex == null) {
478+
throw new TypeError('RequestInit: duplex option is required when sending a body.')
479+
}
480+
481+
// 2. If this’s request’s mode is neither "same-origin" nor "cors",
476482
// then throw a TypeError.
477483
if (request.mode !== 'same-origin' && request.mode !== 'cors') {
478484
throw new TypeError(
479485
'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
480486
)
481487
}
482488

483-
// 2. Set this’s request’s use-CORS-preflight flag.
489+
// 3. Set this’s request’s use-CORS-preflight flag.
484490
request.useCORSPreflightFlag = true
485491
}
486492

@@ -821,7 +827,17 @@ Object.defineProperties(Request.prototype, {
821827
headers: kEnumerableProperty,
822828
redirect: kEnumerableProperty,
823829
clone: kEnumerableProperty,
824-
signal: kEnumerableProperty
830+
signal: kEnumerableProperty,
831+
duplex: {
832+
...kEnumerableProperty,
833+
get () {
834+
// The duplex getter steps are to return "half".
835+
return 'half'
836+
},
837+
set () {
838+
839+
}
840+
}
825841
})
826842

827843
webidl.converters.Request = webidl.interfaceConverter(
@@ -929,6 +945,15 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
929945
{
930946
key: 'window',
931947
converter: webidl.converters.any
948+
},
949+
{
950+
key: 'duplex',
951+
converter: webidl.converters.DOMString,
952+
allowedValues: ['half'],
953+
// TODO(@KhafraDev): this behavior is incorrect, but
954+
// without it, a WPT throws with an uncaught exception,
955+
// causing the entire WPT runner to crash.
956+
defaultValue: 'half'
932957
}
933958
])
934959

test/fetch/abort.js

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const { test } = require('tap')
44
const { fetch } = require('../..')
55
const { createServer } = require('http')
66
const { once } = require('events')
7-
const { ReadableStream } = require('stream/web')
87
const { DOMException } = require('../../lib/fetch/constants')
98

109
const { AbortController: NPMAbortController } = require('abort-controller')
@@ -62,53 +61,6 @@ test('parallel fetch with the same AbortController works as expected', async (t)
6261
t.end()
6362
})
6463

65-
// https://github.com/web-platform-tests/wpt/blob/fd8aeb1bb2eb33bc43f8a5bbc682b0cff6075dfe/fetch/api/abort/general.any.js#L474-L507
66-
test('Readable stream synchronously cancels with AbortError if aborted before reading', async (t) => {
67-
const server = createServer((req, res) => {
68-
res.write('')
69-
res.end()
70-
}).listen(0)
71-
72-
t.teardown(server.close.bind(server))
73-
await once(server, 'listening')
74-
75-
const controller = new AbortController()
76-
const signal = controller.signal
77-
controller.abort()
78-
79-
let cancelReason
80-
81-
const body = new ReadableStream({
82-
pull (controller) {
83-
controller.enqueue(new Uint8Array([42]))
84-
},
85-
cancel (reason) {
86-
cancelReason = reason
87-
}
88-
})
89-
90-
const fetchPromise = fetch(`http://localhost:${server.address().port}`, {
91-
body,
92-
signal,
93-
method: 'POST',
94-
headers: {
95-
'Content-Type': 'text/plain'
96-
}
97-
})
98-
99-
t.ok(cancelReason, 'Cancel called sync')
100-
t.equal(cancelReason.constructor, DOMException)
101-
t.equal(cancelReason.name, 'AbortError')
102-
103-
await t.rejects(fetchPromise, { name: 'AbortError' })
104-
105-
const fetchErr = await fetchPromise.catch(e => e)
106-
107-
t.equal(cancelReason, fetchErr, 'Fetch rejects with same error instance')
108-
109-
t.end()
110-
})
111-
11264
test('Allow the usage of custom implementation of AbortController', async (t) => {
11365
const body = {
11466
fixes: 1605

test/types/fetch.test-d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { URL } from 'url'
22
import { Blob } from 'buffer'
33
import { ReadableStream } from 'stream/web'
4-
import { expectType, expectError } from 'tsd'
4+
import { expectType, expectError, expectAssignable, expectNotAssignable } from 'tsd'
55
import {
66
Agent,
77
BodyInit,
@@ -10,7 +10,6 @@ import {
1010
Headers,
1111
HeadersInit,
1212
SpecIterableIterator,
13-
SpecIterator,
1413
Request,
1514
RequestCache,
1615
RequestCredentials,
@@ -166,3 +165,7 @@ expectType<Promise<FormData>>(response.formData())
166165
expectType<Promise<unknown>>(response.json())
167166
expectType<Promise<string>>(response.text())
168167
expectType<Response>(response.clone())
168+
169+
expectType<Request>(new Request('https://example.com', { body: 'Hello, world', duplex: 'half' }))
170+
expectAssignable<RequestInit>({ duplex: 'half' })
171+
expectNotAssignable<RequestInit>({ duplex: 'not valid' })

test/wpt/runner/runner/runner.mjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventEmitter, once } from 'node:events'
22
import { readdirSync, readFileSync, statSync } from 'node:fs'
3-
import { isAbsolute, join, resolve } from 'node:path'
3+
import { basename, isAbsolute, join, resolve } from 'node:path'
44
import { fileURLToPath } from 'node:url'
55
import { Worker } from 'node:worker_threads'
66
import { parseMeta } from './util.mjs'
@@ -93,7 +93,7 @@ export class WPTRunner extends EventEmitter {
9393

9494
worker.on('message', (message) => {
9595
if (message.type === 'result') {
96-
this.handleIndividualTestCompletion(message)
96+
this.handleIndividualTestCompletion(message, basename(test))
9797
} else if (message.type === 'completion') {
9898
this.handleTestCompletion(worker)
9999
}
@@ -114,14 +114,16 @@ export class WPTRunner extends EventEmitter {
114114
/**
115115
* Called after a test has succeeded or failed.
116116
*/
117-
handleIndividualTestCompletion (message) {
117+
handleIndividualTestCompletion (message, fileName) {
118+
const { fail } = this.#status[fileName] ?? {}
119+
118120
if (message.type === 'result') {
119121
this.#stats.completed += 1
120122

121123
if (message.result.status === 1) {
122124
this.#stats.failed += 1
123125

124-
if (this.#status.fail.includes(message.result.name)) {
126+
if (fail && fail.includes(message.result.name)) {
125127
this.#stats.expectedFailures += 1
126128
} else {
127129
process.exitCode = 1

test/wpt/runner/runner/util.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ export function parseMeta (fileContents) {
3838
}
3939

4040
switch (groups.type) {
41+
case 'title':
4142
case 'timeout': {
42-
meta.timeout = groups.match
43+
meta[groups.type] = groups.match
4344
break
4445
}
4546
case 'global': {

test/wpt/runner/runner/worker.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { join } from 'node:path'
22
import { runInThisContext } from 'node:vm'
33
import { parentPort, workerData } from 'node:worker_threads'
4+
import { readFileSync } from 'node:fs'
45
import {
56
setGlobalOrigin,
67
Response,
@@ -65,7 +66,8 @@ runInThisContext(`
6566
globalThis.location = new URL('${url}')
6667
`)
6768

68-
await import('../resources/testharness.cjs')
69+
const harness = readFileSync(join(basePath, '../runner/resources/testharness.cjs'), 'utf-8')
70+
runInThisContext(harness)
6971

7072
// add_*_callback comes from testharness
7173
// stolen from node's wpt test runner

test/wpt/status/fetch.status.json

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
{
2-
"fail": [
3-
"Stream errors once aborted. Underlying connection closed.",
4-
"Underlying connection is closed when aborting after receiving response - no-cors",
5-
"Already aborted signal rejects immediately"
6-
]
2+
"request-init-stream.any.js": {
3+
"fail": [
4+
"It is error to omit .duplex when the body is a ReadableStream."
5+
]
6+
},
7+
"general.any.js": {
8+
"fail": [
9+
"Stream errors once aborted. Underlying connection closed.",
10+
"Underlying connection is closed when aborting after receiving response - no-cors",
11+
"Already aborted signal rejects immediately"
12+
]
13+
},
14+
"request-disturbed.any.js": {
15+
"fail": [
16+
"Input request used for creating new request became disturbed even if body is not used"
17+
]
18+
}
719
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// META: global=window,worker
2+
3+
// https://fetch.spec.whatwg.org/#forbidden-method
4+
for (const method of [
5+
'CONNECT', 'TRACE', 'TRACK',
6+
'connect', 'trace', 'track'
7+
]) {
8+
test(function() {
9+
assert_throws_js(TypeError,
10+
function() { new Request('./', {method: method}); }
11+
);
12+
}, 'Request() with a forbidden method ' + method + ' must throw.');
13+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// META: global=window,worker
2+
3+
// list of bad ports according to
4+
// https://fetch.spec.whatwg.org/#port-blocking
5+
var BLOCKED_PORTS_LIST = [
6+
1, // tcpmux
7+
7, // echo
8+
9, // discard
9+
11, // systat
10+
13, // daytime
11+
15, // netstat
12+
17, // qotd
13+
19, // chargen
14+
20, // ftp-data
15+
21, // ftp
16+
22, // ssh
17+
23, // telnet
18+
25, // smtp
19+
37, // time
20+
42, // name
21+
43, // nicname
22+
53, // domain
23+
69, // tftp
24+
77, // priv-rjs
25+
79, // finger
26+
87, // ttylink
27+
95, // supdup
28+
101, // hostriame
29+
102, // iso-tsap
30+
103, // gppitnp
31+
104, // acr-nema
32+
109, // pop2
33+
110, // pop3
34+
111, // sunrpc
35+
113, // auth
36+
115, // sftp
37+
117, // uucp-path
38+
119, // nntp
39+
123, // ntp
40+
135, // loc-srv / epmap
41+
137, // netbios-ns
42+
139, // netbios-ssn
43+
143, // imap2
44+
161, // snmp
45+
179, // bgp
46+
389, // ldap
47+
427, // afp (alternate)
48+
465, // smtp (alternate)
49+
512, // print / exec
50+
513, // login
51+
514, // shell
52+
515, // printer
53+
526, // tempo
54+
530, // courier
55+
531, // chat
56+
532, // netnews
57+
540, // uucp
58+
548, // afp
59+
554, // rtsp
60+
556, // remotefs
61+
563, // nntp+ssl
62+
587, // smtp (outgoing)
63+
601, // syslog-conn
64+
636, // ldap+ssl
65+
989, // ftps-data
66+
990, // ftps
67+
993, // ldap+ssl
68+
995, // pop3+ssl
69+
1719, // h323gatestat
70+
1720, // h323hostcall
71+
1723, // pptp
72+
2049, // nfs
73+
3659, // apple-sasl
74+
4045, // lockd
75+
5060, // sip
76+
5061, // sips
77+
6000, // x11
78+
6566, // sane-port
79+
6665, // irc (alternate)
80+
6666, // irc (alternate)
81+
6667, // irc (default)
82+
6668, // irc (alternate)
83+
6669, // irc (alternate)
84+
6697, // irc+tls
85+
10080, // amanda
86+
];
87+
88+
BLOCKED_PORTS_LIST.map(function(a){
89+
promise_test(function(t){
90+
return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a))
91+
}, 'Request on bad port ' + a + ' should throw TypeError.');
92+
});

0 commit comments

Comments
 (0)