Skip to content

Commit de12940

Browse files
committed
port node-fetch tests + fixes
1 parent 22efd70 commit de12940

19 files changed

Lines changed: 3918 additions & 86 deletions

index.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,25 @@ function makeDispatcher (fn) {
8484
module.exports.setGlobalDispatcher = setGlobalDispatcher
8585
module.exports.getGlobalDispatcher = getGlobalDispatcher
8686

87-
module.exports.fetch = makeDispatcher(api.fetch)
87+
if (api.fetch) {
88+
const _fetch = makeDispatcher(api.fetch)
89+
module.exports.fetch = async function fetch (...args) {
90+
try {
91+
return await _fetch(...args)
92+
} catch (err) {
93+
// TODO (fix): This is a little weird. Spec compliant?
94+
if (err.code === 'ERR_INVALID_URL') {
95+
const er = new TypeError('Invalid URL')
96+
er.cause = err
97+
throw er
98+
}
99+
throw err
100+
}
101+
}
102+
module.exports.Headers = api.fetch.Headers
103+
module.exports.Response = api.fetch.Response
104+
}
105+
88106
module.exports.request = makeDispatcher(api.request)
89107
module.exports.stream = makeDispatcher(api.stream)
90108
module.exports.pipeline = makeDispatcher(api.pipeline)

lib/api/api-fetch/body.js

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

33
const util = require('../../core/util')
4-
const { Readable } = require('stream')
4+
const { finished } = require('stream')
5+
const { AbortError } = require('../../core/errors')
56

6-
let TransformStream
7+
let ReadableStream
8+
let CountQueuingStrategy
79

810
// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
911
function extractBody (body) {
@@ -24,6 +26,10 @@ function extractBody (body) {
2426
source: body
2527
}, 'text/plain;charset=UTF-8']
2628
} else if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
29+
if (body instanceof DataView) {
30+
// TODO: Blob doesn't seem to work with DataView?
31+
body = body.buffer
32+
}
2733
return [{
2834
source: body
2935
}, null]
@@ -39,20 +45,12 @@ function extractBody (body) {
3945

4046
let stream
4147
if (util.isStream(body)) {
42-
stream = Readable.toWeb(body)
48+
stream = toWeb(body)
4349
} else {
4450
if (body.locked) {
4551
throw new TypeError('locked')
4652
}
47-
48-
if (!TransformStream) {
49-
TransformStream = require('stream/web').TransformStream
50-
}
51-
52-
// https://streams.spec.whatwg.org/#readablestream-create-a-proxy
53-
const identityTransform = new TransformStream()
54-
body.pipeThrough(identityTransform)
55-
stream = identityTransform
53+
stream = body
5654
}
5755

5856
return [{
@@ -63,4 +61,74 @@ function extractBody (body) {
6361
}
6462
}
6563

64+
function toWeb (streamReadable) {
65+
if (!ReadableStream) {
66+
ReadableStream = require('stream/web').ReadableStream
67+
}
68+
if (!CountQueuingStrategy) {
69+
CountQueuingStrategy = require('stream/web').CountQueuingStrategy
70+
}
71+
72+
if (util.isDestroyed(streamReadable)) {
73+
const readable = new ReadableStream()
74+
readable.cancel()
75+
return readable
76+
}
77+
78+
const objectMode = streamReadable.readableObjectMode
79+
const highWaterMark = streamReadable.readableHighWaterMark
80+
// When not running in objectMode explicitly, we just fall
81+
// back to a minimal strategy that just specifies the highWaterMark
82+
// and no size algorithm. Using a ByteLengthQueuingStrategy here
83+
// is unnecessary.
84+
const strategy = objectMode
85+
? new CountQueuingStrategy({ highWaterMark })
86+
: { highWaterMark }
87+
88+
let controller
89+
90+
function onData (chunk) {
91+
// Copy the Buffer to detach it from the pool.
92+
if (Buffer.isBuffer(chunk) && !objectMode) {
93+
chunk = new Uint8Array(chunk)
94+
}
95+
controller.enqueue(chunk)
96+
if (controller.desiredSize <= 0) {
97+
streamReadable.pause()
98+
}
99+
}
100+
101+
streamReadable.pause()
102+
103+
finished(streamReadable, (err) => {
104+
if (err && err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
105+
const er = new AbortError()
106+
er.cause = er
107+
err = er
108+
}
109+
110+
if (err) {
111+
controller.error(err)
112+
} else {
113+
controller.close()
114+
}
115+
})
116+
117+
streamReadable.on('data', onData)
118+
119+
return new ReadableStream({
120+
start (c) {
121+
controller = c
122+
},
123+
124+
pull () {
125+
streamReadable.resume()
126+
},
127+
128+
cancel (reason) {
129+
util.destroy(streamReadable, reason)
130+
}
131+
}, strategy)
132+
}
133+
66134
module.exports = { extractBody }

lib/api/api-fetch/headers.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
const { types } = require('util')
66
const { validateHeaderName, validateHeaderValue } = require('http')
77
const { kHeadersList } = require('../../core/symbols')
8-
const { InvalidHTTPTokenError, HTTPInvalidHeaderValueError, InvalidArgumentError, InvalidThisError } = require('../../core/errors')
8+
const {
9+
InvalidHTTPTokenError,
10+
HTTPInvalidHeaderValueError,
11+
InvalidThisError
12+
} = require('../../core/errors')
913

1014
function binarySearch (arr, val) {
1115
let low = 0
@@ -49,21 +53,21 @@ function isHeaders (object) {
4953
function fill (headers, object) {
5054
if (isHeaders(object)) {
5155
// Object is instance of Headers
52-
headers[kHeadersList] = Array.splice(object[kHeadersList])
56+
headers[kHeadersList] = [...object[kHeadersList]]
5357
} else if (Array.isArray(object)) {
5458
// Support both 1D and 2D arrays of header entries
5559
if (Array.isArray(object[0])) {
5660
// Array of arrays
5761
for (let i = 0; i < object.length; i++) {
5862
if (object[i].length !== 2) {
59-
throw new InvalidArgumentError(`The argument 'init' is not of length 2. Received ${object[i]}`)
63+
throw new TypeError(`The argument 'init' is not of length 2. Received ${object[i]}`)
6064
}
6165
headers.append(object[i][0], object[i][1])
6266
}
6367
} else if (typeof object[0] === 'string' || Buffer.isBuffer(object[0])) {
6468
// Flat array of strings or Buffers
6569
if (object.length % 2 !== 0) {
66-
throw new InvalidArgumentError(`The argument 'init' is not even in length. Received ${object}`)
70+
throw new TypeError(`The argument 'init' is not even in length. Received ${object}`)
6771
}
6872
for (let i = 0; i < object.length; i += 2) {
6973
headers.append(
@@ -73,7 +77,7 @@ function fill (headers, object) {
7377
}
7478
} else {
7579
// All other array based entries
76-
throw new InvalidArgumentError(`The argument 'init' is not a valid array entry. Received ${object}`)
80+
throw new TypeError(`The argument 'init' is not a valid array entry. Received ${object}`)
7781
}
7882
} else if (!types.isBoxedPrimitive(object)) {
7983
// Object of key/value entries
@@ -94,12 +98,20 @@ class Headers {
9498
constructor (init = {}) {
9599
// validateObject allowArray = true
96100
if (!Array.isArray(init) && typeof init !== 'object') {
97-
throw new InvalidArgumentError('The argument \'init\' must be one of type Object or Array')
101+
throw new TypeError('The argument \'init\' must be one of type Object or Array')
98102
}
99103
this[kHeadersList] = []
100104
fill(this, init)
101105
}
102106

107+
get [Symbol.toStringTag] () {
108+
return this.constructor.name
109+
}
110+
111+
toString () {
112+
return Object.prototype.toString.call(this)
113+
}
114+
103115
append (...args) {
104116
if (!isHeaders(this)) {
105117
throw new InvalidThisError('Header')
@@ -233,8 +245,31 @@ class Headers {
233245
callback.call(thisArg, this[kHeadersList][index + 1], this[kHeadersList][index], this)
234246
}
235247
}
248+
249+
[Symbol.for('nodejs.util.inspect.custom')] () {
250+
return Object.fromEntries(this.entries())
251+
}
236252
}
237253

254+
// Re-shaping object for Web IDL tests.
255+
Object.defineProperties(
256+
Headers.prototype,
257+
[
258+
'append',
259+
'delete',
260+
'entries',
261+
'forEach',
262+
'get',
263+
'has',
264+
'keys',
265+
'set',
266+
'values'
267+
].reduce((result, property) => {
268+
result[property] = { enumerable: true }
269+
return result
270+
}, {})
271+
)
272+
238273
Headers.prototype[Symbol.iterator] = Headers.prototype.entries
239274

240275
module.exports = Headers

lib/api/api-fetch/index.js

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22

33
'use strict'
44

5-
const Headers = require('./headers')
65
const { kHeadersList } = require('../../core/symbols')
7-
const { METHODS } = require('http')
6+
const Headers = require('./headers')
87
const Response = require('./response')
8+
const { METHODS, STATUS_CODES } = require('http')
9+
910
const {
1011
InvalidArgumentError,
1112
NotSupportedError,
1213
RequestAbortedError
1314
} = require('../../core/errors')
14-
const { addSignal, removeSignal } = require('../abort-signal')
1515
const { extractBody } = require('./body')
16+
const { kUrlList } = require('./symbols')
17+
const { addSignal, removeSignal } = require('../abort-signal')
1618

19+
let TransformStream
1720
let ReadableStream
1821

1922
class FetchHandler {
@@ -69,46 +72,42 @@ class FetchHandler {
6972
let response
7073
if (headers.has('location')) {
7174
if (this.redirect === 'manual') {
72-
response = new Response({
75+
response = new Response(null, {
7376
type: 'opaqueredirect',
77+
status: 0,
7478
url: this.url
7579
})
7680
} else {
77-
response = new Response({
78-
type: 'error',
79-
url: this.url
80-
})
81+
response = Response.error()
8182
}
8283
} else {
8384
const self = this
8485
if (!ReadableStream) {
8586
ReadableStream = require('stream/web').ReadableStream
8687
}
87-
response = new Response({
88-
type: 'default',
89-
url: this.url,
90-
body: new ReadableStream({
91-
async start (controller) {
92-
self.controller = controller
93-
},
94-
async pull () {
95-
resume()
96-
},
97-
async cancel (reason) {
98-
let err
99-
if (reason instanceof Error) {
100-
err = reason
101-
} else if (typeof reason === 'string') {
102-
err = new Error(reason)
103-
} else {
104-
err = new RequestAbortedError()
105-
}
106-
abort(err)
88+
response = new Response(new ReadableStream({
89+
async start (controller) {
90+
self.controller = controller
91+
},
92+
async pull () {
93+
resume()
94+
},
95+
async cancel (reason) {
96+
let err
97+
if (reason instanceof Error) {
98+
err = reason
99+
} else if (typeof reason === 'string') {
100+
err = new Error(reason)
101+
} else {
102+
err = new RequestAbortedError()
107103
}
108-
}, { highWaterMark: 16384 }),
109-
statusCode,
104+
abort(err)
105+
}
106+
}, { highWaterMark: 16384 }), {
107+
status: statusCode,
108+
statusText: STATUS_CODES[statusCode],
110109
headers,
111-
context
110+
[kUrlList]: [this.url, ...((context && context.history) || [])]
112111
})
113112
}
114113

@@ -182,7 +181,11 @@ async function fetch (opts) {
182181
}
183182

184183
if (opts.redirect != null) {
185-
// TODO: Validate
184+
if (
185+
typeof opts.redirect !== 'string' ||
186+
!/^(follow|manual|error)/.test(opts.redirect)) {
187+
throw new TypeError(`Redirect option '${opts.redirect}' is not a valid value of RequestRedirect`)
188+
}
186189
} else {
187190
opts.redirect = 'follow'
188191
}
@@ -214,6 +217,17 @@ async function fetch (opts) {
214217

215218
const [body, contentType] = extractBody(opts.body)
216219

220+
if (body.stream) {
221+
if (!TransformStream) {
222+
TransformStream = require('stream/web').TransformStream
223+
}
224+
225+
// https://streams.spec.whatwg.org/#readablestream-create-a-proxy
226+
const identityTransform = new TransformStream()
227+
body.pipeThrough(identityTransform)
228+
body.stream = identityTransform
229+
}
230+
217231
if (contentType && !headers.has('content-type')) {
218232
headers.set('content-type', contentType)
219233
}

0 commit comments

Comments
 (0)