'use strict'
// Test tools
const assert = require('node:assert')
const { describe, it, before, beforeEach, after } = require('node:test')
const { setTimeout: delay } = require('node:timers/promises')
const zlib = require('node:zlib')
const stream = require('node:stream')
const vm = require('node:vm')
const crypto = require('node:crypto')
const {
fetch,
Headers,
Request,
FormData,
Response,
setGlobalDispatcher,
Agent
} = require('../../index.js')
const HeadersOrig = require('../../lib/web/fetch/headers.js').Headers
const ResponseOrig = require('../../lib/web/fetch/response.js').Response
const RequestOrig = require('../../lib/web/fetch/request.js').Request
const TestServer = require('./utils/server.js')
const { createServer } = require('node:http')
const { default: tspl } = require('@matteo.collina/tspl')
const {
Uint8Array: VMUint8Array
} = vm.runInNewContext('this')
describe('node-fetch', () => {
const local = new TestServer()
let base
before(async () => {
await local.start()
setGlobalDispatcher(new Agent({
connect: {
rejectUnauthorized: false
}
}))
base = `http://${local.hostname}:${local.port}/`
})
after(async () => {
return local.stop()
})
it('should return a promise', () => {
const url = `${base}hello`
const p = fetch(url)
assert.ok(p instanceof Promise)
assert.strictEqual(typeof p.then, 'function')
})
it('should expose Headers, Response and Request constructors', () => {
assert.strictEqual(Headers, HeadersOrig)
assert.strictEqual(Response, ResponseOrig)
assert.strictEqual(Request, RequestOrig)
})
it('should support proper toString output for Headers, Response and Request objects', () => {
assert.strictEqual(new Headers().toString(), '[object Headers]')
assert.strictEqual(new Response().toString(), '[object Response]')
assert.strictEqual(new Request(base).toString(), '[object Request]')
})
// TODO Should we reflect the input?
it('should reject with error if url is protocol relative', () => {
const url = '//example.com/'
return assert.rejects(fetch(url), new TypeError('Failed to parse URL from //example.com/'))
})
it('should reject with error if url is relative path', () => {
const url = '/some/path'
return assert.rejects(fetch(url), new TypeError('Failed to parse URL from /some/path'))
})
// TODO: This seems odd
it('should reject with error if protocol is unsupported', () => {
const url = 'ftp://example.com/'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should reject with error on network failure', { timeout: 5000 }, function () {
const url = 'http://localhost:50000/'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should resolve into response', () => {
const url = `${base}hello`
return fetch(url).then(res => {
assert.ok(res instanceof Response)
assert.ok(res.headers instanceof Headers)
assert.ok(res.body instanceof ReadableStream)
assert.strictEqual(res.bodyUsed, false)
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
})
})
it('Response.redirect should resolve into response', () => {
const res = Response.redirect('http://localhost')
assert.ok(res instanceof Response)
assert.ok(res.headers instanceof Headers)
assert.strictEqual(res.headers.get('location'), 'http://localhost/')
assert.strictEqual(res.status, 302)
})
it('Response.redirect /w invalid url should fail', () => {
assert.throws(() => {
Response.redirect('localhost')
})
})
it('Response.redirect /w invalid status should fail', () => {
assert.throws(() => {
Response.redirect('http://localhost', 200)
})
})
it('should accept plain text response', () => {
const url = `${base}plain`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'text')
})
})
})
it('should accept html response (like plain text)', () => {
const url = `${base}html`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/html')
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '<html></html>')
})
})
})
it('should accept json response', () => {
const url = `${base}json`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'application/json')
return res.json().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'object')
assert.deepStrictEqual(result, { name: 'value' })
})
})
})
it('should send request with custom headers', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': 'abc' }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should send request with custom headers array', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': ['abc'] }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should send request with multi-valued headers', () => {
const url = `${base}inspect`
const options = {
headers: { 'x-custom-header': ['abc', '123'] }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc,123')
})
})
it('should accept headers instance', () => {
const url = `${base}inspect`
const options = {
headers: new Headers({ 'x-custom-header': 'abc' })
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should follow redirect code 301', () => {
const url = `${base}redirect/301`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
})
})
it('should follow redirect code 302', () => {
const url = `${base}redirect/302`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 303', () => {
const url = `${base}redirect/303`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 307', () => {
const url = `${base}redirect/307`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect code 308', () => {
const url = `${base}redirect/308`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow redirect chain', () => {
const url = `${base}redirect/chain`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should follow POST request redirect code 301 with GET', () => {
const url = `${base}redirect/301`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 301 with PATCH', () => {
const url = `${base}redirect/301`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
})
it('should follow POST request redirect code 302 with GET', () => {
const url = `${base}redirect/302`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 302 with PATCH', () => {
const url = `${base}redirect/302`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
})
it('should follow redirect code 303 with GET', () => {
const url = `${base}redirect/303`
const options = {
method: 'PUT',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.body, '')
})
})
})
it('should follow PATCH request redirect code 307 with PATCH', () => {
const url = `${base}redirect/307`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
return res.json().then(result => {
assert.strictEqual(result.method, 'PATCH')
assert.strictEqual(result.body, 'a=1')
})
})
})
it('should not follow non-GET redirect if body is a readable stream', () => {
const url = `${base}redirect/307`
const options = {
method: 'PATCH',
body: stream.Readable.from('tada')
}
return assert.rejects(fetch(url, options), new TypeError('RequestInit: duplex option is required when sending a body.'))
})
it('should obey maximum redirect, reject case', () => {
const url = `${base}redirect/chain/20`
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should obey redirect chain, resolve case', () => {
const url = `${base}redirect/chain/19`
return fetch(url).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
assert.strictEqual(res.status, 200)
})
})
it('should support redirect mode, error flag', () => {
const url = `${base}redirect/301`
const options = {
redirect: 'error'
}
return assert.rejects(fetch(url, options), new TypeError('fetch failed'))
})
it('should support redirect mode, manual flag when there is no redirect', () => {
const url = `${base}hello`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 200)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should follow redirect code 301 and keep existing headers', () => {
const url = `${base}redirect/301`
const options = {
headers: new Headers({ 'x-custom-header': 'abc' })
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, `${base}inspect`)
return res.json()
}).then(res => {
assert.strictEqual(res.headers['x-custom-header'], 'abc')
})
})
it('should treat broken redirect as ordinary response (follow)', () => {
const url = `${base}redirect/no-location`
return fetch(url).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 301)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should treat broken redirect as ordinary response (manual)', () => {
const url = `${base}redirect/no-location`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.status, 301)
assert.strictEqual(res.headers.get('location'), null)
})
})
it('should throw a TypeError on an invalid redirect option', () => {
const url = `${base}redirect/301`
const options = {
redirect: 'foobar'
}
return fetch(url, options).then(() => {
assert.fail()
}, error => {
assert.ok(error instanceof TypeError)
})
})
it('should set redirected property on response when redirect', () => {
const url = `${base}redirect/301`
return fetch(url).then(res => {
assert.strictEqual(res.redirected, true)
})
})
it('should not set redirected property on response without redirect', () => {
const url = `${base}hello`
return fetch(url).then(res => {
assert.strictEqual(res.redirected, false)
})
})
it('should handle client-error response', () => {
const url = `${base}error/400`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
assert.strictEqual(res.status, 400)
assert.strictEqual(res.statusText, 'Bad Request')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'client error')
})
})
})
it('should handle server-error response', () => {
const url = `${base}error/500`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
assert.strictEqual(res.status, 500)
assert.strictEqual(res.statusText, 'Internal Server Error')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(res.bodyUsed, true)
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'server error')
})
})
})
it('should handle network-error response', () => {
const url = `${base}error/reset`
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
it('should handle network-error partial response', () => {
const url = `${base}error/premature`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
return assert.rejects(() => res.text(), new TypeError('terminated'))
})
})
it('should handle network-error in chunked response async iterator', () => {
const url = `${base}error/premature/chunked`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
const read = async body => {
const chunks = []
for await (const chunk of body) {
chunks.push(chunk)
}
return chunks
}
return assert.rejects(read(res.body), new TypeError('terminated'))
})
})
it('should handle network-error in chunked response in consumeBody', () => {
const url = `${base}error/premature/chunked`
return fetch(url).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.ok, true)
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should handle DNS-error response', () => {
const url = 'http://domain.invalid'
return assert.rejects(fetch(url), new TypeError('fetch failed'))
})
// TODO: Should we pass through the error message?
it('should reject invalid json response', () => {
const url = `${base}error/json`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'application/json')
return assert.rejects(res.json(), SyntaxError)
})
})
it('should handle response with no status text', () => {
const url = `${base}no-status-text`
return fetch(url).then(res => {
assert.strictEqual(res.statusText, '')
})
})
it('should handle no content response', () => {
const url = `${base}no-content`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
// TODO: Should we pass through the error message?
it('should reject when trying to parse no content response as json', () => {
const url = `${base}no-content`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.ok, true)
return assert.rejects(res.json(), new SyntaxError('Unexpected end of JSON input'))
})
})
it('should handle no content response with gzip encoding', () => {
const url = `${base}no-content/gzip`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should handle not modified response', () => {
const url = `${base}not-modified`
return fetch(url).then(res => {
assert.strictEqual(res.status, 304)
assert.strictEqual(res.statusText, 'Not Modified')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should handle not modified response with gzip encoding', () => {
const url = `${base}not-modified/gzip`
return fetch(url).then(res => {
assert.strictEqual(res.status, 304)
assert.strictEqual(res.statusText, 'Not Modified')
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
assert.strictEqual(res.ok, false)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should decompress gzip response', () => {
const url = `${base}gzip`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress slightly invalid gzip response', async () => {
const url = `${base}gzip-truncated`
const res = await fetch(url)
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
const result = await res.text()
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
it('should decompress deflate response', () => {
const url = `${base}deflate`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress deflate raw response from old apache server', () => {
const url = `${base}deflate-raw`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should decompress brotli response', function () {
if (typeof zlib.createBrotliDecompress !== 'function') {
this.skip()
}
const url = `${base}brotli`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should handle no content response with brotli encoding', function () {
if (typeof zlib.createBrotliDecompress !== 'function') {
this.skip()
}
const url = `${base}no-content/brotli`
return fetch(url).then(res => {
assert.strictEqual(res.status, 204)
assert.strictEqual(res.statusText, 'No Content')
assert.strictEqual(res.headers.get('content-encoding'), 'br')
assert.strictEqual(res.ok, true)
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, '')
})
})
})
it('should skip decompression if unsupported', () => {
const url = `${base}sdch`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'fake sdch string')
})
})
})
it('should skip decompression if unsupported codings', () => {
const url = `${base}multiunsupported`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'multiunsupported')
})
})
})
it('should decompress multiple coding', () => {
const url = `${base}multisupported`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(result => {
assert.strictEqual(typeof result, 'string')
assert.strictEqual(result, 'hello world')
})
})
})
it('should reject if response compression is invalid', () => {
const url = `${base}invalid-content-encoding`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should handle errors on the body stream even if it is not used', done => {
const url = `${base}invalid-content-encoding`
fetch(url)
.then(res => {
assert.strictEqual(res.status, 200)
})
.catch(() => { })
.then(new Promise((resolve) => {
// Wait a few ms to see if an uncaught error occurs
setTimeout(() => {
resolve()
}, 20)
}))
})
it('should collect handled errors on the body stream to reject if the body is used later', () => {
const url = `${base}invalid-content-encoding`
return fetch(url).then(delay(20)).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return assert.rejects(res.text(), new TypeError('terminated'))
})
})
it('should not overwrite existing accept-encoding header when auto decompression is true', () => {
const url = `${base}inspect`
const options = {
compress: true,
headers: {
'Accept-Encoding': 'gzip'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers['accept-encoding'], 'gzip')
})
})
describe('AbortController', () => {
let controller
beforeEach(() => {
controller = new AbortController()
})
it('should support request cancellation with signal', () => {
const fetches = [
fetch(
`${base}timeout`,
{
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: JSON.stringify({ hello: 'world' })
}
}
)
]
controller.abort()
return Promise.all(fetches.map(async fetched => {
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
}))
})
it('should support multiple request cancellation with signal', () => {
const fetches = [
fetch(`${base}timeout`, { signal: controller.signal }),
fetch(
`${base}timeout`,
{
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
body: JSON.stringify({ hello: 'world' })
}
}
)
]
controller.abort()
return Promise.all(fetches.map(async fetched => {
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
}))
})
it('should reject immediately if signal has already been aborted', async () => {
const url = `${base}timeout`
const options = {
signal: controller.signal
}
controller.abort()
const fetched = fetch(url, options)
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should allow redirects to be aborted', async () => {
const request = new Request(`${base}redirect/slow`, {
signal: controller.signal
})
setTimeout(() => {
controller.abort()
}, 20)
try {
await fetch(request)
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should allow redirected response body to be aborted', async () => {
const request = new Request(`${base}redirect/slow-stream`, {
signal: controller.signal
})
const fetched = fetch(request).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
const result = res.text()
controller.abort()
return result
})
try {
await fetched
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should reject response body with AbortError when aborted before stream has been read completely', async () => {
const response = await fetch(
`${base}slow`,
{ signal: controller.signal }
)
const promise = response.text()
controller.abort()
try {
await promise
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', async () => {
const response = await fetch(
`${base}slow`,
{ signal: controller.signal }
)
controller.abort()
const promise = response.text()
try {
await promise
assert.fail('should have thrown')
} catch (error) {
assert.ok(error instanceof Error)
assert.strictEqual(error.name, 'AbortError')
}
})
})
it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
return Promise.all([
assert.rejects(fetch(`${base}inspect`, { signal: {} }), new TypeError('RequestInit: Expected signal ("{}") to be an instance of AbortSignal.')),
assert.rejects(fetch(`${base}inspect`, { signal: '' }), new TypeError('RequestInit: Expected signal ("""") to be an instance of AbortSignal.')),
assert.rejects(fetch(`${base}inspect`, { signal: Object.create(null) }), new TypeError('RequestInit: Expected signal ("[Object: null prototype] {}") to be an instance of AbortSignal.'))
])
})
it('should gracefully handle a null signal', () => {
return fetch(`${base}hello`, { signal: null }).then(res => {
return assert.strictEqual(res.ok, true)
})
})
it('should allow setting User-Agent', () => {
const url = `${base}inspect`
const options = {
headers: {
'user-agent': 'faked'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers['user-agent'], 'faked')
})
})
it('should set default Accept header', () => {
const url = `${base}inspect`
fetch(url).then(res => res.json()).then(res => {
assert.strictEqual(res.headers.accept, '*/*')
})
})
it('should allow setting Accept header', () => {
const url = `${base}inspect`
const options = {
headers: {
accept: 'application/json'
}
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.headers.accept, 'application/json')
})
})
it('should allow POST request', () => {
const url = `${base}inspect`
const options = {
method: 'POST'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '0')
})
})
it('should allow POST request with string body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with buffer body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: Buffer.from('a=1', 'utf-8')
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with ArrayBuffer body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n').buffer
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBuffer body from a VM context', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n')
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (BigUint64Array) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: new BigUint64Array(encoder.encode('0123456789abcdef').buffer)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, '0123456789abcdef')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '16')
})
})
it('should allow POST request with ArrayBufferView (DataView) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: new DataView(encoder.encode('Hello, world!\n').buffer)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new VMUint8Array(Buffer.from('Hello, world!\n'))
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'Hello, world!\n')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '14')
})
})
it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => {
const encoder = new TextEncoder()
const url = `${base}inspect`
const options = {
method: 'POST',
body: encoder.encode('Hello, world!\n').subarray(7, 13)
}
return fetch(url, options).then(res => res.json()).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'world!')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '6')
})
})
it('should allow POST request with blob body without type', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new Blob(['a=1'])
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
// assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with blob body with type', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: new Blob(['a=1'], {
type: 'text/plain;charset=UTF-8'
})
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=utf-8')
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow POST request with readable stream as body', () => {
const url = `${base}inspect`
const options = {
method: 'POST',
body: stream.Readable.from('a=1'),
duplex: 'half'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], 'chunked')
assert.strictEqual(res.headers['content-type'], undefined)
assert.strictEqual(res.headers['content-length'], undefined)
})
})
it('should allow POST request with object body', () => {
const url = `${base}inspect`
// Note that fetch simply calls tostring on an object
const options = {
method: 'POST',
body: { a: 1 }
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.body, '[object Object]')
assert.strictEqual(res.headers['content-type'], 'text/plain;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '15')
})
})
it('should allow POST request with form-data as body', () => {
const form = new FormData()
form.append('a', '1')
const url = `${base}multipart`
const options = {
method: 'POST',
body: form
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.ok(res.headers['content-type'].startsWith('multipart/form-data; boundary='))
assert.strictEqual(res.body, 'a=1')
})
})
it('constructing a Response with URLSearchParams as body should have a Content-Type', () => {
const parameters = new URLSearchParams()
const res = new Response(parameters)
res.headers.get('Content-Type')
assert.strictEqual(res.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8')
})
it('constructing a Request with URLSearchParams as body should have a Content-Type', () => {
const parameters = new URLSearchParams()
const request = new Request(base, { method: 'POST', body: parameters })
assert.strictEqual(request.headers.get('Content-Type'), 'application/x-www-form-urlencoded;charset=UTF-8')
})
it('Reading a body with URLSearchParams should echo back the result', () => {
const parameters = new URLSearchParams()
parameters.append('a', '1')
return new Response(parameters).text().then(text => {
assert.strictEqual(text, 'a=1')
})
})
// Body should be cloned...
it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => {
const parameters = new URLSearchParams()
const request = new Request(`${base}inspect`, { method: 'POST', body: parameters })
parameters.append('a', '1')
return request.text().then(text => {
assert.strictEqual(text, '')
})
})
it('should allow POST request with URLSearchParams as body', () => {
const parameters = new URLSearchParams()
parameters.append('a', '1')
const url = `${base}inspect`
const options = {
method: 'POST',
body: parameters
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
assert.strictEqual(res.body, 'a=1')
})
})
it('should still recognize URLSearchParams when extended', () => {
class CustomSearchParameters extends URLSearchParams { }
const parameters = new CustomSearchParameters()
parameters.append('a', '1')
const url = `${base}inspect`
const options = {
method: 'POST',
body: parameters
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'POST')
assert.strictEqual(res.headers['content-type'], 'application/x-www-form-urlencoded;charset=UTF-8')
assert.strictEqual(res.headers['content-length'], '3')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow PUT request', () => {
const url = `${base}inspect`
const options = {
method: 'PUT',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'PUT')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow DELETE request', () => {
const url = `${base}inspect`
const options = {
method: 'DELETE'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'DELETE')
})
})
it('should allow DELETE request with string body', () => {
const url = `${base}inspect`
const options = {
method: 'DELETE',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'DELETE')
assert.strictEqual(res.body, 'a=1')
assert.strictEqual(res.headers['transfer-encoding'], undefined)
assert.strictEqual(res.headers['content-length'], '3')
})
})
it('should allow PATCH request', () => {
const url = `${base}inspect`
const options = {
method: 'PATCH',
body: 'a=1'
}
return fetch(url, options).then(res => {
return res.json()
}).then(res => {
assert.strictEqual(res.method, 'PATCH')
assert.strictEqual(res.body, 'a=1')
})
})
it('should allow HEAD request', () => {
const url = `${base}hello`
const options = {
method: 'HEAD'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
// assert.ok(res.body instanceof stream.Transform)
return res.text()
}).then(text => {
assert.strictEqual(text, '')
})
})
it('should allow HEAD request with content-encoding header', () => {
const url = `${base}error/404`
const options = {
method: 'HEAD'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 404)
assert.strictEqual(res.headers.get('content-encoding'), 'gzip')
return res.text()
}).then(text => {
assert.strictEqual(text, '')
})
})
it('should allow OPTIONS request', () => {
const url = `${base}options`
const options = {
method: 'OPTIONS'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 200)
assert.strictEqual(res.statusText, 'OK')
assert.strictEqual(res.headers.get('allow'), 'GET, HEAD, OPTIONS')
// assert.ok(res.body instanceof stream.Transform)
})
})
it('should reject decoding body twice', () => {
const url = `${base}plain`
return fetch(url).then(res => {
assert.strictEqual(res.headers.get('content-type'), 'text/plain')
return res.text().then(() => {
assert.strictEqual(res.bodyUsed, true)
return assert.rejects(res.text(), new TypeError('Body is unusable: Body has already been read'))
})
})
})
it('should allow cloning a json response and log it as text response', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return Promise.all([res.json(), r1.text()]).then(results => {
assert.deepStrictEqual(results[0], { name: 'value' })
assert.strictEqual(results[1], '{"name":"value"}')
})
})
})
it('should allow cloning a json response, and then log it as text response', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return res.json().then(result => {
assert.deepStrictEqual(result, { name: 'value' })
return r1.text().then(result => {
assert.strictEqual(result, '{"name":"value"}')
})
})
})
})
it('should allow cloning a json response, first log as text response, then return json object', () => {
const url = `${base}json`
return fetch(url).then(res => {
const r1 = res.clone()
return r1.text().then(result => {
assert.strictEqual(result, '{"name":"value"}')
return res.json().then(result => {
assert.deepStrictEqual(result, { name: 'value' })
})
})
})
})
it('should not allow cloning a response after its been used', () => {
const url = `${base}hello`
return fetch(url).then(res =>
res.text().then(() => {
assert.throws(() => {
res.clone()
}, new TypeError('Response.clone: Body has already been consumed.'))
})
)
})
/* global expect */
// TODO: fix test.
it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
// Observed behavior of TCP packets splitting:
// - response body size <= 65438 → single packet sent
// - response body size > 65438 → multiple packets sent
// Max TCP packet size is 64kB (http://stackoverflow.com/a/2614188/5763764),
// but first packet probably transfers more than the response body.
const firstPacketMaxSize = 65438
const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
})
return expect(
fetch(url).then(res => res.clone().buffer())
).to.timeout
})
// TODO: fix test.
it.skip('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 10
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize))
})
return expect(
fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
).to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', { timeout: 300 }, async function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 16 * 1024 // = defaultHighWaterMark
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
})
return expect(
fetch(url).then(res => res.clone().buffer())
).not.to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', { timeout: 300 }, function () {
const url = local.mockState(res => {
const firstPacketMaxSize = 65438
const secondPacketSize = 10
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1))
})
return expect(
fetch(url, { highWaterMark: 10 }).then(res => res.clone().buffer())
).not.to.timeout
})
// TODO: fix test.
it.skip('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', { timeout: 300 }, function () {
const url = local.mockState(res => {
res.end(crypto.randomBytes((2 * 512 * 1024) - 1))
})
return expect(
fetch(url, { highWaterMark: 512 * 1024 }).then(res => res.clone().buffer())
).not.to.timeout
})
it('should allow get all responses of a header', () => {
const url = `${base}cookie`
return fetch(url).then(res => {
const expected = 'a=1, b=1'
assert.strictEqual(res.headers.get('set-cookie'), expected)
assert.strictEqual(res.headers.get('Set-Cookie'), expected)
})
})
it('should support fetch with Request instance', () => {
const url = `${base}hello`
const request = new Request(url)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support fetch with Node.js URL object', () => {
const url = `${base}hello`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support fetch with WHATWG URL object', () => {
const url = `${base}hello`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('if params are given, do not modify anything', () => {
const url = `${base}question?a=1`
const urlObject = new URL(url)
const request = new Request(urlObject)
return fetch(request).then(res => {
assert.strictEqual(res.url, url)
assert.strictEqual(res.ok, true)
assert.strictEqual(res.status, 200)
})
})
it('should support reading blob as text', () => {
return new Response('hello')
.blob()
.then(blob => blob.text())
.then(body => {
assert.strictEqual(body, 'hello')
})
})
it('should support reading blob as arrayBuffer', () => {
return new Response('hello')
.blob()
.then(blob => blob.arrayBuffer())
.then(ab => {
const string = String.fromCharCode.apply(null, new Uint8Array(ab))
assert.strictEqual(string, 'hello')
})
})
it('should support blob round-trip', () => {
const url = `${base}hello`
let length
let type
return fetch(url).then(res => res.blob()).then(async blob => {
const url = `${base}inspect`
length = blob.size
type = blob.type
return fetch(url, {
method: 'POST',
body: blob
})
}).then(res => res.json()).then(({ body, headers }) => {
assert.strictEqual(body, 'world')
assert.strictEqual(headers['content-type'], type)
assert.strictEqual(headers['content-length'], String(length))
})
})
it('should support overwrite Request instance', () => {
const url = `${base}inspect`
const request = new Request(url, {
method: 'POST',
headers: {
a: '1'
}
})
return fetch(request, {
method: 'GET',
headers: {
a: '2'
}
}).then(res => {
return res.json()
}).then(body => {
assert.strictEqual(body.method, 'GET')
assert.strictEqual(body.headers.a, '2')
})
})
it('should support http request', { timeout: 5000 }, async function (t) {
t = tspl(t, { plan: 2 })
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
res.end()
})
after(() => server.close())
server.listen(0, () => {
const url = `http://localhost:${server.address().port}`
const options = {
method: 'HEAD'
}
fetch(url, options).then(res => {
t.strictEqual(res.status, 200)
t.strictEqual(res.ok, true)
})
})
await t.completed
})
it('should encode URLs as UTF-8', async () => {
const url = `${base}möbius`
const res = await fetch(url)
assert.strictEqual(res.url, `${base}m%C3%B6bius`)
})
it('should allow manual redirect handling', { timeout: 5000 }, function () {
const url = `${base}redirect/302`
const options = {
redirect: 'manual'
}
return fetch(url, options).then(res => {
assert.strictEqual(res.status, 302)
assert.strictEqual(res.url, url)
assert.strictEqual(res.type, 'basic')
assert.strictEqual(res.headers.get('Location'), '/inspect')
assert.strictEqual(res.ok, false)
})
})
})