UNPKG

request-monitor

Version:
1,663 lines (1,503 loc) 78.4 kB
// test tools import chai from 'chai'; import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; import chaiString from 'chai-string'; import then from 'promise'; import resumer from 'resumer'; import FormData from 'form-data'; import stringToArrayBuffer from 'string-to-arraybuffer'; import URLSearchParams_Polyfill from 'url-search-params'; import { URL } from 'whatwg-url'; import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller'; import AbortController2 from 'abort-controller'; const { spawn } = require('child_process'); const http = require('http'); const fs = require('fs'); const path = require('path'); const stream = require('stream'); const { parse: parseURL, URLSearchParams } = require('url'); const { lookup } = require('dns'); const vm = require('vm'); const { ArrayBuffer: VMArrayBuffer, Uint8Array: VMUint8Array } = vm.runInNewContext('this'); let convert; try { convert = require('encoding').convert; } catch(e) { } chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); const expect = chai.expect; import TestServer from './server'; // test subjects import originFetch, { FetchError, Headers, Request, Response } from '../__lib/node-fetch'; import FetchErrorOrig from '../__lib/node-fetch/fetch-error.js'; import HeadersOrig from '../__lib/node-fetch/headers.js'; import RequestOrig from '../__lib/node-fetch/request.js'; import ResponseOrig from '../__lib/node-fetch/response.js'; import Body from '../__lib/node-fetch/body.js'; import Blob from '../__lib/node-fetch/blob.js'; const supportToString = ({ [Symbol.toStringTag]: 'z' }).toString() === '[object z]'; const supportStreamDestroy = 'destroy' in stream.Readable.prototype; const local = new TestServer(); const base = `http://${local.hostname}:${local.port}/`; const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; global.window = { fetch: originFetch, XMLHttpRequest: XMLHttpRequest } global.XMLHttpRequest = XMLHttpRequest; const monitor = require('../../src') monitor(()=>{}) const fetch = global.window.fetch; if(originFetch === fetch){ throw new Error('error') } before(done => { local.start(done); }); after(done => { local.stop(done); process.exit(0) }); describe('node-fetch', () => { it('should return a promise', function() { const url = `${base}hello`; const p = fetch(url); expect(p).to.be.an.instanceof(fetch.Promise); expect(p).to.have.property('then'); }); // it('should allow custom promise', function() { // const url = `${base}hello`; // const old = fetch.Promise; // fetch.Promise = then; // expect(fetch(url)).to.be.an.instanceof(then); // expect(fetch(url)).to.not.be.an.instanceof(old); // fetch.Promise = old; // }); it('should throw error when no promise implementation are found', function() { const url = `${base}hello`; const old = fetch.Promise; fetch.Promise = undefined; expect(() => { fetch(url) }).to.throw(Error); fetch.Promise = old; }); it('should expose Headers, Response and Request constructors', function() { expect(FetchError).to.equal(FetchErrorOrig); expect(Headers).to.equal(HeadersOrig); expect(Response).to.equal(ResponseOrig); expect(Request).to.equal(RequestOrig); }); (supportToString ? it : it.skip)('should support proper toString output for Headers, Response and Request objects', function() { 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', function() { const url = '//example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); it('should reject with error if url is relative path', function() { const url = '/some/path'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only absolute URLs are supported'); }); it('should reject with error if protocol is unsupported', function() { const url = 'ftp://example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported'); }); it('should reject with error on network failure', function() { const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' }); }); it('should resolve into response', function() { 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(stream.Transform); 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('should accept plain text response', function() { 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)', function() { 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('<html></html>'); }); }); }); it('should accept json response', function() { 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', function() { const url = `${base}inspect`; const opts = { headers: { 'x-custom-header': 'abc' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept headers instance', function() { const url = `${base}inspect`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['x-custom-header']).to.equal('abc'); }); }); it('should accept custom host header', function() { const url = `${base}inspect`; const opts = { headers: { host: 'example.com' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['host']).to.equal('example.com'); }); }); it('should accept custom HoSt header', function() { const url = `${base}inspect`; const opts = { headers: { HoSt: 'example.com' } }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.headers['host']).to.equal('example.com'); }); }); it('should follow redirect code 301', function() { 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', function() { 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', function() { 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', function() { 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', function() { 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', function() { 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', function() { const url = `${base}redirect/301`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/301`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/302`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/302`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/303`; const opts = { method: 'PUT', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/307`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}redirect/307`; const opts = { method: 'PATCH', body: resumer().queue('a=1').end() }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'unsupported-redirect'); }); it('should obey maximum redirect, reject case', function() { const url = `${base}redirect/chain`; const opts = { follow: 1 } return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should obey redirect chain, resolve case', function() { const url = `${base}redirect/chain`; const opts = { follow: 2 } return fetch(url, opts).then(res => { expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); }); }); it('should allow not following redirect', function() { const url = `${base}redirect/301`; const opts = { follow: 0 } return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should support redirect mode, manual flag', function() { const url = `${base}redirect/301`; const opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal(`${base}inspect`); }); }); it('should support redirect mode, error flag', function() { const url = `${base}redirect/301`; const opts = { redirect: 'error' }; return expect(fetch(url, opts)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'no-redirect'); }); it('should support redirect mode, manual flag when there is no redirect', function() { const url = `${base}hello`; const opts = { redirect: 'manual' }; return fetch(url, opts).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', function() { const url = `${base}redirect/301`; const opts = { headers: new Headers({ 'x-custom-header': 'abc' }) }; return fetch(url, opts).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)', function() { 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)', function() { const url = `${base}redirect/no-location`; const opts = { redirect: 'manual' }; return fetch(url, opts).then(res => { expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; }); }); it('should ignore invalid headers', function() { const url = `${base}invalid-header`; return fetch(url).then(res => { expect(res.headers.get('Invalid-Header')).to.be.null; expect(res.headers.get('Invalid-Header-Value')).to.be.null; expect(res.headers.get('Set-Cookie')).to.be.null; expect(Array.from(res.headers.keys()).length).to.equal(4); expect(res.headers.has('Connection')).to.be.true; expect(res.headers.has('Content-Type')).to.be.true; expect(res.headers.has('Date')).to.be.true; expect(res.headers.has('Transfer-Encoding')).to.be.true; }); }); it('should handle client-error response', function() { 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', function() { 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', function() { const url = `${base}error/reset`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ECONNRESET'); }); it('should handle DNS-error response', function() { const url = 'http://domain.invalid'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ENOTFOUND'); }); it('should reject invalid json response', function() { 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.rejected }); }); it('should handle no content response', function() { 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', function() { 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.rejected; }); }); it('should handle no content response with gzip encoding', function() { 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', function() { 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', function() { 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', function() { 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', function() { const url = `${base}gzip-truncated`; 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 deflate response', function() { 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'); }); }); }); it('should decompress deflate raw response from old apache server', function() { 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 skip decompression if unsupported', function() { 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 reject if response compression is invalid', function() { 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 .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); }); it('should handle errors on the body stream even if it is not used', function(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(); }, 50); }); }); it('should collect handled errors on the body stream to reject if the body is used later', function() { function delay(value) { return new Promise((resolve) => { setTimeout(() => { resolve(value) }, 100); }); } const url = `${base}invalid-content-encoding`; return fetch(url).then(delay).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); }); it('should allow disabling auto decompression', function() { const url = `${base}gzip`; const opts = { compress: false }; return fetch(url, opts).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.not.equal('hello world'); }); }); }); it('should not overwrite existing accept-encoding header when auto decompression is true', function() { const url = `${base}inspect`; const opts = { compress: true, headers: { 'Accept-Encoding': 'gzip' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers['accept-encoding']).to.equal('gzip'); }); }); it('should allow custom timeout', function() { this.timeout(500); const url = `${base}timeout`; const opts = { timeout: 100 }; return expect(fetch(url, opts)).to.eventually.be.rejected }); it('should allow custom timeout on response body', function() { this.timeout(500); const url = `${base}slow`; const opts = { timeout: 100 }; return fetch(url, opts).then(res => { expect(res.ok).to.be.true; return expect(res.text()).to.eventually.be.rejected }); }); it('should clear internal timeout on fetch response', function (done) { this.timeout(2000); spawn('node', ['-e', `require('./')('${base}hello', { timeout: 10000 })`]) .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch redirect', function (done) { this.timeout(2000); spawn('node', ['-e', `require('./')('${base}redirect/301', { timeout: 10000 })`]) .on('exit', () => { done(); }); }); it('should clear internal timeout on fetch error', function (done) { this.timeout(2000); spawn('node', ['-e', `require('./')('${base}error/reset', { timeout: 10000 })`]) .on('exit', () => { done(); }); }); it('should support request cancellation with signal', function () { this.timeout(500); const controller = new AbortController(); const controller2 = new AbortController2(); const fetches = [ fetch(`${base}timeout`, { signal: controller.signal }), fetch(`${base}timeout`, { signal: controller2.signal }), fetch( `${base}timeout`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', body: JSON.stringify({ hello: 'world' }) } } ) ]; setTimeout(() => { controller.abort(); controller2.abort(); }, 100); return Promise.all(fetches.map(fetched => expect(fetched) .to.eventually.be.rejected .and.be.an.instanceOf(Error) )); }); it('should reject immediately if signal has already been aborted', function () { const url = `${base}timeout`; const controller = new AbortController(); const opts = { signal: controller.signal }; controller.abort(); const fetched = fetch(url, opts); return expect(fetched).to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError', }); }); it('should clear internal timeout when request is cancelled with an AbortSignal', function(done) { this.timeout(2000); const script = ` var AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController; var controller = new AbortController(); require('./')( '${base}timeout', { signal: controller.signal, timeout: 10000 } ); setTimeout(function () { controller.abort(); }, 100); ` spawn('node', ['-e', script]) .on('exit', () => { done(); }); }); it('should remove internal AbortSignal event listener after request is aborted', function () { const controller = new AbortController(); const { signal } = controller; const promise = fetch( `${base}timeout`, { signal } ); const result = expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') .then(() => { expect(signal.listeners.abort.length).to.equal(0); }); controller.abort(); return result; }); it('should allow redirects to be aborted', function() { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow`, { signal: abortController.signal }); setTimeout(() => { abortController.abort(); }, 50); 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', function() { const abortController = new AbortController(); const request = new Request(`${base}redirect/slow-stream`, { signal: abortController.signal }); return expect(fetch(request).then(res => { expect(res.headers.get('content-type')).to.equal('text/plain'); const result = res.text(); abortController.abort(); return result; })).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError'); }); it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortController(); const { signal } = controller; const fetchHtml = fetch(`${base}html`, { signal }) .then(res => res.text()); const fetchResponseError = fetch(`${base}error/reset`, { signal }); const fetchRedirect = fetch(`${base}redirect/301`, { signal }).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal('<html></html>'), expect(fetchResponseError).to.be.eventually.rejected, expect(fetchRedirect).to.eventually.be.fulfilled, ]).then(() => { expect(signal.listeners.abort.length).to.equal(0) }); }); it('should reject response body with AbortError when aborted before stream has been read completely', () => { const controller = new AbortController(); 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', () => { const controller = new AbortController(); 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 emit error event to response body with an AbortError when aborted before underlying stream is closed', (done) => { const controller = new AbortController(); expect(fetch( `${base}slow`, { signal: controller.signal } )) .to.eventually.be.fulfilled .then((res) => { res.body.on('error', (err) => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); done(); }); controller.abort(); }); }); (supportStreamDestroy ? it : it.skip)('should cancel request body of type Stream with AbortError when aborted', () => { const controller = new AbortController(); const body = new stream.Readable({ objectMode: true }); body._read = () => {}; const promise = fetch( `${base}slow`, { signal: controller.signal, body, method: 'POST' } ); const result = Promise.all([ new Promise((resolve, reject) => { body.on('error', (error) => { try { expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError') resolve(); } catch (err) { reject(err); } }); }), expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') ]); controller.abort(); return result; }); (supportStreamDestroy ? it.skip : it)('should immediately reject when attempting to cancel streamed Requests in node < 8', () => { const controller = new AbortController(); const body = new stream.Readable({ objectMode: true }); body._read = () => {}; const promise = fetch( `${base}slow`, { signal: controller.signal, body, method: 'POST' } ); return expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('message').includes('not supported'); }); it('should throw a TypeError if a signal is not of type AbortSignal', () => { return Promise.all([ expect(fetch(`${base}inspect`, { signal: {} })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), expect(fetch(`${base}inspect`, { signal: '' })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), expect(fetch(`${base}inspect`, { signal: Object.create(null) })) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), ]); }); it('should set default User-Agent', function () { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.startWith('node-fetch/'); }); }); it('should allow setting User-Agent', function () { const url = `${base}inspect`; const opts = { headers: { 'user-agent': 'faked' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.equal('faked'); }); }); it('should set default Accept header', function () { const url = `${base}inspect`; fetch(url).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('*/*'); }); }); it('should allow setting Accept header', function () { const url = `${base}inspect`; const opts = { headers: { 'accept': 'application/json' } }; return fetch(url, opts).then(res => res.json()).then(res => { expect(res.headers.accept).to.equal('application/json'); }); }); it('should allow POST request', function() { const url = `${base}inspect`; const opts = { method: 'POST' }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: Buffer.from('a=1', 'utf-8') }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: stringToArrayBuffer('Hello, world!\n') }; return fetch(url, opts).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', function() { // TODO: Node.js v4 doesn't support ArrayBuffer from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed try { Buffer.from(new VMArrayBuffer()); } catch (err) { this.skip(); } const url = `${base}inspect`; const opts = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n')) }; return fetch(url, opts).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 (DataView) body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new DataView(stringToArrayBuffer('Hello, world!\n')) }; return fetch(url, opts).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', function() { // TODO: Node.js v4 doesn't support ArrayBufferView from other contexts, so we skip this test, drop this check once Node.js v4 support is not needed try { Buffer.from(new VMArrayBuffer()); } catch (err) { this.skip(); } const url = `${base}inspect`; const opts = { method: 'POST', body: new VMUint8Array(Buffer.from('Hello, world!\n')) }; return fetch(url, opts).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'); }); }); // TODO: Node.js v4 doesn't support necessary Buffer API, so we skip this test, drop this check once Node.js v4 support is not needed (Buffer.from.length === 3 ? it : it.skip)('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Uint8Array(stringToArrayBuffer('Hello, world!\n'), 7, 6) }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Blob(['a=1']) }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'POST', body: new Blob(['a=1'], { type: 'text/plain;charset=UTF-8' }) }; return fetch(url, opts).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', function() { let body = resumer().queue('a=1').end(); body = body.pipe(new stream.PassThrough()); const url = `${base}inspect`; const opts = { method: 'POST', body }; return fetch(url, opts).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 form-data as body', function() { const form = new FormData(); form.append('a','1'); const url = `${base}multipart`; const opts = { method: 'POST', body: form }; return fetch(url, opts).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.headers['content-length']).to.be.a('string'); expect(res.body).to.equal('a=1'); }); }); it('should allow POST request with form-data using stream as body', function() { const form = new FormData(); form.append('my_field', fs.createReadStream(path.join(__dirname, 'dummy.txt'))); const url = `${base}multipart`; const opts = { method: 'POST', body: form }; return fetch(url, opts).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.headers['content-length']).to.be.undefined; expect(res.body).to.contain('my_field='); }); }); it('should allow POST request with form-data as body and custom headers', function() { const form = new FormData(); form.append('a','1'); const headers = form.getHeaders(); headers['b'] = '2'; const url = `${base}multipart`; const opts = { method: 'POST', body: form, headers }; return fetch(url, opts).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.headers['content-length']).to.be.a('string'); expect(res.headers.b).to.equal('2'); expect(res.body).to.equal('a=1'); }); }); it('should allow POST request with object body', function() { const url = `${base}inspect`; // note that fetch simply calls tostring on an object const opts = { method: 'POST', body: { a: 1 } }; return fetch(url, opts).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'); }); }); const itUSP = typeof URLSearchParams === 'function' ? it : it.skip; itUSP('constructing a Response with URLSearchParams as body should have a Content-Type', function() { const params = new URLSearchParams(); const res = new Response(params); res.headers.get('Content-Type'); expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); itUSP('constructing a Request with URLSearchParams as body should have a Content-Type', function() { const params = new URLSearchParams(); const req = new Request(base, { method: 'POST', body: params }); expect(req.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); itUSP('Reading a body with URLSearchParams should echo back the result', function() { const params = new URLSearchParams(); params.append('a','1'); return new Response(params).text().then(text => { expect(text).to.equal('a=1'); }); }); // Body should been cloned... itUSP('constructing a Request/Response with URLSearchParams and mutating it should not affected body', function() { const params = new URLSearchParams(); const req = new Request(`${base}inspect`, { method: 'POST', body: params }) params.append('a','1') return req.text().then(text => { expect(text).to.equal(''); }); }); itUSP('should allow POST request with URLSearchParams as body', function() { const params = new URLSearchParams(); params.append('a','1'); const url = `${base}inspect`; const opts = { method: 'POST', body: params, }; return fetch(url, opts).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'); }); }); // itUSP('should still recognize URLSearchParams when extended', function() { // class CustomSearchParams extends URLSearchParams {} // const params = new CustomSearchParams(); // params.append('a','1'); // const url = `${base}inspect`; // const opts = { // method: 'POST', // body: params, // }; // return fetch(url, opts).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'); // }); // }); /* for 100% code coverage, checks for duck-typing-only detection * where both constructor.name and brand tests fail */ it('should still recognize URLSearchParams when extended from polyfill', function() { class CustomPolyfilledSearchParams extends URLSearchParams_Polyfill {} const params = new CustomPolyfilledSearchParams(); params.append('a','1'); const url = `${base}inspect`; const opts = { method: 'POST', body: params, }; return fetch(url, opts).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 overwrite Content-Length if possible', function() { const url = `${base}inspect`; // note that fetch simply calls tostring on an object const opts = { method: 'POST', headers: { 'Content-Length': '1000' }, body: 'a=1' }; return fetch(url, opts).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 PUT request', function() { const url = `${base}inspect`; const opts = { method: 'PUT', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'DELETE' }; return fetch(url, opts).then(res => { return res.json(); }).then(res => { expect(res.method).to.equal('DELETE'); }); }); it('should allow DELETE request with string body', function() { const url = `${base}inspect`; const opts = { method: 'DELETE', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}inspect`; const opts = { method: 'PATCH', body: 'a=1' }; return fetch(url, opts).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', function() { const url = `${base}hello`; const opts = { method: 'HEAD' }; return fetch(url, opts).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', function() { const url = `${base}error/404`; const opts = { method: 'HEAD' }; return fetch(url, opts).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', function() { const url = `${base}options`; const opts = { method: 'OPTIONS' }; return fetch(url, opts).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', function() { 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; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); }); }); it('should support maximum response size, multiple chunk', function() { const url = `${base}size/chunk`; const opts = { size: 5 }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); }); it('should support maximum response size, single chunk', function() { const url = `${base}size/long`; const opts = { size: 5 }; return fetch(url, opts).then(res => { expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); }); it('should allow piping response body as stream', function() { const url = `${base}hello`; return fetch(url).then(res => { expect(res.body).to.be.an.instanceof(stream.Transform); return streamToPromise(res.body, chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }); }); }); it('should allow cloning a response, and use both as stream', function() { const url = `${base}hello`; return fetch(url).then(res => { const r1 = res.clone(); expect(res.body).to.be.an.instanceof(stream.Transform); expect(r1.body).to.be.an.instanceof(stream.Transform); const dataHandler = chunk => { if (chunk === null) { return; } expect(chunk.toString()).to.equal('world'); }; return Promise.all([ streamToPromise(res.body, dataHandler), streamToPromise(r1.body, dataHandler) ]); }); }); it('should allow cloning a json response and log it as text response', function(