request-monitor
Version:
1,663 lines (1,503 loc) • 78.4 kB
JavaScript
// 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(