/* eslint no-unused-expressions: "off" */ /* globals AbortController */ // Test tools const zlib = require('zlib') const stream = require('stream') const vm = require('vm') const chai = require('chai') const crypto = require('crypto') const chaiPromised = require('chai-as-promised') const chaiIterator = require('chai-iterator') const chaiString = require('chai-string') const delay = require('delay') const { Blob } = require('buffer') const { fetch, Headers, Request, FormData, Response, setGlobalDispatcher, Agent } = require('../../index.js') const HeadersOrig = require('../../lib/fetch/headers.js').Headers const RequestOrig = require('../../lib/fetch/request.js').Request const ResponseOrig = require('../../lib/fetch/response.js').Response const TestServer = require('./utils/server.js') const chaiTimeout = require('./utils/chai-timeout.js') const { ReadableStream } = require('stream/web') function isNodeLowerThan (version) { return !~process.version.localeCompare(version, undefined, { numeric: true }) } const { Uint8Array: VMUint8Array } = vm.runInNewContext('this') chai.use(chaiPromised) chai.use(chaiIterator) chai.use(chaiString) chai.use(chaiTimeout) const { expect } = chai 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) expect(p).to.be.an.instanceof(Promise) expect(p).to.have.property('then') }) it('should expose Headers, Response and Request constructors', () => { expect(Headers).to.equal(HeadersOrig) expect(Response).to.equal(ResponseOrig) expect(Request).to.equal(RequestOrig) }) it('should support proper toString output for Headers, Response and Request objects', () => { expect(new Headers().toString()).to.equal('[object Headers]') expect(new Response().toString()).to.equal('[object Response]') expect(new Request(base).toString()).to.equal('[object Request]') }) it('should reject with error if url is protocol relative', () => { const url = '//example.com/' return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) }) it('should reject with error if url is relative path', () => { const url = '/some/path' return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) }) it('should reject with error if protocol is unsupported', () => { const url = 'ftp://example.com/' return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) }) it('should reject with error on network failure', function () { this.timeout(5000) const url = 'http://localhost:50000/' return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(TypeError) }) it('should resolve into response', () => { const url = `${base}hello` return fetch(url).then(res => { expect(res).to.be.an.instanceof(Response) expect(res.headers).to.be.an.instanceof(Headers) expect(res.body).to.be.an.instanceof(ReadableStream) expect(res.bodyUsed).to.be.false expect(res.url).to.equal(url) expect(res.ok).to.be.true expect(res.status).to.equal(200) expect(res.statusText).to.equal('OK') }) }) it('Response.redirect should resolve into response', () => { const res = Response.redirect('http://localhost') expect(res).to.be.an.instanceof(Response) expect(res.headers).to.be.an.instanceof(Headers) expect(res.headers.get('location')).to.equal('http://localhost/') expect(res.status).to.equal(302) }) it('Response.redirect /w invalid url should fail', () => { expect(() => { Response.redirect('localhost') }).to.throw() }) it('Response.redirect /w invalid status should fail', () => { expect(() => { Response.redirect('http://localhost', 200) }).to.throw() }) it('should accept plain text response', () => { const url = `${base}plain` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(res.bodyUsed).to.be.true expect(result).to.be.a('string') expect(result).to.equal('text') }) }) }) it('should accept html response (like plain text)', () => { const url = `${base}html` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/html') return res.text().then(result => { expect(res.bodyUsed).to.be.true expect(result).to.be.a('string') expect(result).to.equal('') }) }) }) it('should accept json response', () => { const url = `${base}json` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json') return res.json().then(result => { expect(res.bodyUsed).to.be.true expect(result).to.be.an('object') expect(result).to.deep.equal({ 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 => { expect(res.headers['x-custom-header']).to.equal('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 => { expect(res.headers['x-custom-header']).to.equal('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 => { expect(res.headers['x-custom-header']).to.equal('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 => { expect(res.headers['x-custom-header']).to.equal('abc') }) }) it('should follow redirect code 301', () => { const url = `${base}redirect/301` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) expect(res.ok).to.be.true }) }) it('should follow redirect code 302', () => { const url = `${base}redirect/302` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) }) }) it('should follow redirect code 303', () => { const url = `${base}redirect/303` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) }) }) it('should follow redirect code 307', () => { const url = `${base}redirect/307` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) }) }) it('should follow redirect code 308', () => { const url = `${base}redirect/308` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) }) }) it('should follow redirect chain', () => { const url = `${base}redirect/chain` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(result => { expect(result.method).to.equal('GET') expect(result.body).to.equal('') }) }) }) 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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(res => { expect(res.method).to.equal('PATCH') expect(res.body).to.equal('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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(result => { expect(result.method).to.equal('GET') expect(result.body).to.equal('') }) }) }) 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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(res => { expect(res.method).to.equal('PATCH') expect(res.body).to.equal('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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(result => { expect(result.method).to.equal('GET') expect(result.body).to.equal('') }) }) }) 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 => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) return res.json().then(result => { expect(result.method).to.equal('PATCH') expect(result.body).to.equal('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 expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(TypeError) }) it('should obey maximum redirect, reject case', () => { const url = `${base}redirect/chain/20` return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(TypeError) }) it('should obey redirect chain, resolve case', () => { const url = `${base}redirect/chain/19` return fetch(url).then(res => { expect(res.url).to.equal(`${base}inspect`) expect(res.status).to.equal(200) }) }) it('should support redirect mode, error flag', () => { const url = `${base}redirect/301` const options = { redirect: 'error' } return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(TypeError) }) 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 => { expect(res.url).to.equal(url) expect(res.status).to.equal(200) expect(res.headers.get('location')).to.be.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 => { expect(res.url).to.equal(`${base}inspect`) return res.json() }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc') }) }) it('should treat broken redirect as ordinary response (follow)', () => { const url = `${base}redirect/no-location` return fetch(url).then(res => { expect(res.url).to.equal(url) expect(res.status).to.equal(301) expect(res.headers.get('location')).to.be.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 => { expect(res.url).to.equal(url) expect(res.status).to.equal(301) expect(res.headers.get('location')).to.be.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(() => { expect.fail() }, error => { expect(error).to.be.an.instanceOf(TypeError) }) }) it('should set redirected property on response when redirect', () => { const url = `${base}redirect/301` return fetch(url).then(res => { expect(res.redirected).to.be.true }) }) it('should not set redirected property on response without redirect', () => { const url = `${base}hello` return fetch(url).then(res => { expect(res.redirected).to.be.false }) }) it('should handle client-error response', () => { const url = `${base}error/400` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') expect(res.status).to.equal(400) expect(res.statusText).to.equal('Bad Request') expect(res.ok).to.be.false return res.text().then(result => { expect(res.bodyUsed).to.be.true expect(result).to.be.a('string') expect(result).to.equal('client error') }) }) }) it('should handle server-error response', () => { const url = `${base}error/500` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') expect(res.status).to.equal(500) expect(res.statusText).to.equal('Internal Server Error') expect(res.ok).to.be.false return res.text().then(result => { expect(res.bodyUsed).to.be.true expect(result).to.be.a('string') expect(result).to.equal('server error') }) }) }) it('should handle network-error response', () => { const url = `${base}error/reset` return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) }) it('should handle network-error partial response', () => { const url = `${base}error/premature` return fetch(url).then(res => { expect(res.status).to.equal(200) expect(res.ok).to.be.true return expect(res.text()).to.eventually.be.rejectedWith(Error) }) }) it('should handle network-error in chunked response async iterator', () => { const url = `${base}error/premature/chunked` return fetch(url).then(res => { expect(res.status).to.equal(200) expect(res.ok).to.be.true const read = async body => { const chunks = [] for await (const chunk of body) { chunks.push(chunk) } return chunks } return expect(read(res.body)) .to.eventually.be.rejectedWith(Error) }) }) it('should handle network-error in chunked response in consumeBody', () => { const url = `${base}error/premature/chunked` return fetch(url).then(res => { expect(res.status).to.equal(200) expect(res.ok).to.be.true return expect(res.text()).to.eventually.be.rejectedWith(Error) }) }) it('should handle DNS-error response', () => { const url = 'http://domain.invalid' return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError) }) it('should reject invalid json response', () => { const url = `${base}error/json` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('application/json') return expect(res.json()).to.eventually.be.rejectedWith(Error) }) }) it('should handle response with no status text', () => { const url = `${base}no-status-text` return fetch(url).then(res => { expect(res.statusText).to.equal('') }) }) it('should handle no content response', () => { const url = `${base}no-content` return fetch(url).then(res => { expect(res.status).to.equal(204) expect(res.statusText).to.equal('No Content') expect(res.ok).to.be.true return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.be.empty }) }) }) it('should reject when trying to parse no content response as json', () => { const url = `${base}no-content` return fetch(url).then(res => { expect(res.status).to.equal(204) expect(res.statusText).to.equal('No Content') expect(res.ok).to.be.true return expect(res.json()).to.eventually.be.rejectedWith(Error) }) }) it('should handle no content response with gzip encoding', () => { const url = `${base}no-content/gzip` return fetch(url).then(res => { expect(res.status).to.equal(204) expect(res.statusText).to.equal('No Content') expect(res.headers.get('content-encoding')).to.equal('gzip') expect(res.ok).to.be.true return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.be.empty }) }) }) it('should handle not modified response', () => { const url = `${base}not-modified` return fetch(url).then(res => { expect(res.status).to.equal(304) expect(res.statusText).to.equal('Not Modified') expect(res.ok).to.be.false return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.be.empty }) }) }) it('should handle not modified response with gzip encoding', () => { const url = `${base}not-modified/gzip` return fetch(url).then(res => { expect(res.status).to.equal(304) expect(res.statusText).to.equal('Not Modified') expect(res.headers.get('content-encoding')).to.equal('gzip') expect(res.ok).to.be.false return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.be.empty }) }) }) it('should decompress gzip response', () => { const url = `${base}gzip` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('hello world') }) }) }) it('should decompress slightly invalid gzip response', async () => { const url = `${base}gzip-truncated` const res = await fetch(url) expect(res.headers.get('content-type')).to.equal('text/plain') const result = await res.text() expect(result).to.be.a('string') expect(result).to.equal('hello world') }) it('should decompress deflate response', () => { const url = `${base}deflate` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('hello world') }) }) }) xit('should decompress deflate raw response from old apache server', () => { const url = `${base}deflate-raw` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('hello world') }) }) }) it('should decompress brotli response', function () { if (typeof zlib.createBrotliDecompress !== 'function') { this.skip() } const url = `${base}brotli` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('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 => { expect(res.status).to.equal(204) expect(res.statusText).to.equal('No Content') expect(res.headers.get('content-encoding')).to.equal('br') expect(res.ok).to.be.true return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.be.empty }) }) }) it('should skip decompression if unsupported', () => { const url = `${base}sdch` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('fake sdch string') }) }) }) it('should skip decompression if unsupported codings', () => { const url = `${base}multiunsupported` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('multiunsupported') }) }) }) it('should decompress multiple coding', () => { const url = `${base}multisupported` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(result => { expect(result).to.be.a('string') expect(result).to.equal('hello world') }) }) }) it('should reject if response compression is invalid', () => { const url = `${base}invalid-content-encoding` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return expect(res.text()).to.eventually.be.rejected }) }) 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 => { expect(res.status).to.equal(200) }) .catch(() => {}) .then(() => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { done() }, 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 => { expect(res.headers.get('content-type')).to.equal('text/plain') return expect(res.text()).to.eventually.be.rejected }) }) 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 => { expect(res.headers['accept-encoding']).to.equal('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(fetched => expect(fetched) .to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.have.property('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(fetched => expect(fetched) .to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError') )) }) it('should reject immediately if signal has already been aborted', () => { const url = `${base}timeout` const options = { signal: controller.signal } controller.abort() const fetched = fetch(url, options) return expect(fetched).to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError') }) it('should allow redirects to be aborted', () => { const request = new Request(`${base}redirect/slow`, { signal: controller.signal }) setTimeout(() => { controller.abort() }, 20) return expect(fetch(request)).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError') }) it('should allow redirected response body to be aborted', () => { const request = new Request(`${base}redirect/slow-stream`, { signal: controller.signal }) return expect(fetch(request).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') const result = res.text() controller.abort() return result })).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError') }) it('should reject response body with AbortError when aborted before stream has been read completely', () => { return expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled .then(res => { const promise = res.text() controller.abort() return expect(promise) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') }) }) it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => { return expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled .then(res => { controller.abort() return expect(res.text()) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') }) }) }) it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => { return Promise.all([ expect(fetch(`${base}inspect`, { signal: {} })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError), expect(fetch(`${base}inspect`, { signal: '' })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError), expect(fetch(`${base}inspect`, { signal: Object.create(null) })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) ]) }) it('should gracefully handle a null signal', () => { return fetch(`${base}hello`, { signal: null }).then(res => { return expect(res.ok).to.be.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 => { expect(res.headers['user-agent']).to.equal('faked') }) }) it('should set default Accept header', () => { const url = `${base}inspect` fetch(url).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('*/*') }) }) 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 => { expect(res.headers.accept).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('Hello, world!\n') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('Hello, world!\n') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('Hello, world!\n') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('0123456789abcdef') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('Hello, world!\n') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('Hello, world!\n') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('world!') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.be.undefined // expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8') expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.equal('chunked') expect(res.headers['content-type']).to.be.undefined expect(res.headers['content-length']).to.be.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 => { expect(res.method).to.equal('POST') expect(res.body).to.equal('[object Object]') expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8') expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary=') expect(res.body).to.equal('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') expect(res.headers.get('Content-Type')).to.equal('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 }) expect(request.headers.get('Content-Type')).to.equal('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 => { expect(text).to.equal('a=1') }) }) // Body should been 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 => { expect(text).to.equal('') }) }) 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 => { expect(res.method).to.equal('POST') expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') expect(res.headers['content-length']).to.equal('3') expect(res.body).to.equal('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 => { expect(res.method).to.equal('POST') expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8') expect(res.headers['content-length']).to.equal('3') expect(res.body).to.equal('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 => { expect(res.method).to.equal('PUT') expect(res.body).to.equal('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 => { expect(res.method).to.equal('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 => { expect(res.method).to.equal('DELETE') expect(res.body).to.equal('a=1') expect(res.headers['transfer-encoding']).to.be.undefined expect(res.headers['content-length']).to.equal('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 => { expect(res.method).to.equal('PATCH') expect(res.body).to.equal('a=1') }) }) it('should allow HEAD request', () => { const url = `${base}hello` const options = { method: 'HEAD' } return fetch(url, options).then(res => { expect(res.status).to.equal(200) expect(res.statusText).to.equal('OK') expect(res.headers.get('content-type')).to.equal('text/plain') // expect(res.body).to.be.an.instanceof(stream.Transform) return res.text() }).then(text => { expect(text).to.equal('') }) }) it('should allow HEAD request with content-encoding header', () => { const url = `${base}error/404` const options = { method: 'HEAD' } return fetch(url, options).then(res => { expect(res.status).to.equal(404) expect(res.headers.get('content-encoding')).to.equal('gzip') return res.text() }).then(text => { expect(text).to.equal('') }) }) it('should allow OPTIONS request', () => { const url = `${base}options` const options = { method: 'OPTIONS' } return fetch(url, options).then(res => { expect(res.status).to.equal(200) expect(res.statusText).to.equal('OK') expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS') // expect(res.body).to.be.an.instanceof(stream.Transform) }) }) it('should reject decoding body twice', () => { const url = `${base}plain` return fetch(url).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain') return res.text().then(() => { expect(res.bodyUsed).to.be.true return expect(res.text()).to.eventually.be.rejectedWith(Error) }) }) }) 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 => { expect(results[0]).to.deep.equal({ name: 'value' }) expect(results[1]).to.equal('{"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 => { expect(result).to.deep.equal({ name: 'value' }) return r1.text().then(result => { expect(result).to.equal('{"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 => { expect(result).to.equal('{"name":"value"}') return res.json().then(result => { expect(result).to.deep.equal({ 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(() => { expect(() => { res.clone() }).to.throw(Error) }) ) }) xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { this.timeout(300) 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 }) xit('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { this.timeout(300) 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 }) xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip() } this.timeout(300) 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 }) xit('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip() } this.timeout(300) 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 }) xit('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip() } this.timeout(300) 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 }) xit('should allow get all responses of a header', () => { // TODO: fix test. const url = `${base}cookie` return fetch(url).then(res => { const expected = 'a=1, b=1' expect(res.headers.get('set-cookie')).to.equal(expected) expect(res.headers.get('Set-Cookie')).to.equal(expected) }) }) it('should support fetch with Request instance', () => { const url = `${base}hello` const request = new Request(url) return fetch(request).then(res => { expect(res.url).to.equal(url) expect(res.ok).to.be.true expect(res.status).to.equal(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 => { expect(res.url).to.equal(url) expect(res.ok).to.be.true expect(res.status).to.equal(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 => { expect(res.url).to.equal(url) expect(res.ok).to.be.true expect(res.status).to.equal(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 => { expect(res.url).to.equal(url) expect(res.ok).to.be.true expect(res.status).to.equal(200) }) }) it('should support reading blob as text', () => { return new Response('hello') .blob() .then(blob => blob.text()) .then(body => { expect(body).to.equal('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)) expect(string).to.equal('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 }) => { expect(body).to.equal('world') expect(headers['content-type']).to.equal(type) expect(headers['content-length']).to.equal(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 => { expect(body.method).to.equal('GET') expect(body.headers.a).to.equal('2') }) }) it('should support http request', function () { this.timeout(5000) const url = 'https://github.com/' const options = { method: 'HEAD' } return fetch(url, options).then(res => { expect(res.status).to.equal(200) expect(res.ok).to.be.true }) }) it('should encode URLs as UTF-8', async () => { const url = `${base}möbius` const res = await fetch(url) expect(res.url).to.equal(`${base}m%C3%B6bius`) }) it('should allow manual redirect handling', function () { this.timeout(5000) const url = `${base}redirect/302` const options = { redirect: 'manual' } return fetch(url, options).then(res => { expect(res.status).to.equal(302) expect(res.url).to.equal(url) expect(res.type).to.equal('basic') expect(res.headers.get('Location')).to.equal('/inspect') expect(res.ok).to.be.false }) }) })