2286 lines
66 KiB
JavaScript
2286 lines
66 KiB
JavaScript
// Test tools
|
|
import zlib from 'zlib';
|
|
import crypto from 'crypto';
|
|
import http from 'http';
|
|
import fs from 'fs';
|
|
import stream from 'stream';
|
|
import path from 'path';
|
|
import {lookup} from 'dns';
|
|
import vm from 'vm';
|
|
import {TextEncoder} from 'util';
|
|
import chai from 'chai';
|
|
import chaiPromised from 'chai-as-promised';
|
|
import chaiIterator from 'chai-iterator';
|
|
import chaiString from 'chai-string';
|
|
import FormData from 'form-data';
|
|
import FormDataNode from 'formdata-node';
|
|
import delay from 'delay';
|
|
import AbortControllerMysticatea from 'abort-controller';
|
|
import abortControllerPolyfill from 'abortcontroller-polyfill/dist/abortcontroller.js';
|
|
const AbortControllerPolyfill = abortControllerPolyfill.AbortController;
|
|
|
|
// Test subjects
|
|
import Blob from 'fetch-blob';
|
|
|
|
import fetch, {
|
|
FetchError,
|
|
Headers,
|
|
Request,
|
|
Response
|
|
} from '../src/index.js';
|
|
import {FetchError as FetchErrorOrig} from '../src/errors/fetch-error.js';
|
|
import HeadersOrig, {fromRawHeaders} from '../src/headers.js';
|
|
import RequestOrig from '../src/request.js';
|
|
import ResponseOrig from '../src/response.js';
|
|
import Body, {getTotalBytes, extractContentType} from '../src/body.js';
|
|
import TestServer from './utils/server.js';
|
|
|
|
const {
|
|
Uint8Array: VMUint8Array
|
|
} = vm.runInNewContext('this');
|
|
|
|
import chaiTimeout from './utils/chai-timeout.js';
|
|
|
|
chai.use(chaiPromised);
|
|
chai.use(chaiIterator);
|
|
chai.use(chaiString);
|
|
chai.use(chaiTimeout);
|
|
const {expect} = chai;
|
|
|
|
function streamToPromise(stream, dataHandler) {
|
|
return new Promise((resolve, reject) => {
|
|
stream.on('data', (...args) => {
|
|
Promise.resolve()
|
|
.then(() => dataHandler(...args))
|
|
.catch(reject);
|
|
});
|
|
stream.on('end', resolve);
|
|
stream.on('error', reject);
|
|
});
|
|
}
|
|
|
|
describe('node-fetch', () => {
|
|
const local = new TestServer();
|
|
let base;
|
|
|
|
before(async () => {
|
|
await local.start();
|
|
base = `http://${local.hostname}:${local.port}/`;
|
|
});
|
|
|
|
after(async () => {
|
|
return local.stop();
|
|
});
|
|
|
|
it('should return a promise', () => {
|
|
const url = `${base}hello`;
|
|
const p = fetch(url);
|
|
expect(p).to.be.an.instanceof(Promise);
|
|
expect(p).to.have.property('then');
|
|
});
|
|
|
|
it('should expose Headers, Response and Request constructors', () => {
|
|
expect(FetchError).to.equal(FetchErrorOrig);
|
|
expect(Headers).to.equal(HeadersOrig);
|
|
expect(Response).to.equal(ResponseOrig);
|
|
expect(Request).to.equal(RequestOrig);
|
|
});
|
|
|
|
it('should support proper toString output for Headers, Response and Request objects', () => {
|
|
expect(new Headers().toString()).to.equal('[object Headers]');
|
|
expect(new Response().toString()).to.equal('[object Response]');
|
|
expect(new Request(base).toString()).to.equal('[object Request]');
|
|
});
|
|
|
|
it('should reject with error if url is protocol relative', () => {
|
|
const url = '//example.com/';
|
|
return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/);
|
|
});
|
|
|
|
it('should reject with error if url is relative path', () => {
|
|
const url = '/some/path';
|
|
return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/);
|
|
});
|
|
|
|
it('should reject with error if protocol is unsupported', () => {
|
|
const url = 'ftp://example.com/';
|
|
return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /URL scheme "ftp" is not supported/);
|
|
});
|
|
|
|
it('should reject with error on network failure', function () {
|
|
this.timeout(5000);
|
|
const url = 'http://localhost:50000/';
|
|
return expect(fetch(url)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'});
|
|
});
|
|
|
|
it('error should contain system error if one occurred', () => {
|
|
const err = new FetchError('a message', 'system', new Error('an error'));
|
|
return expect(err).to.have.property('erroredSysCall');
|
|
});
|
|
|
|
it('error should not contain system error if none occurred', () => {
|
|
const err = new FetchError('a message', 'a type');
|
|
return expect(err).to.not.have.property('erroredSysCall');
|
|
});
|
|
|
|
it('system error is extracted from failed requests', function () {
|
|
this.timeout(5000);
|
|
const url = 'http://localhost:50000/';
|
|
return expect(fetch(url)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('erroredSysCall');
|
|
});
|
|
|
|
it('should resolve into response', () => {
|
|
const url = `${base}hello`;
|
|
return fetch(url).then(res => {
|
|
expect(res).to.be.an.instanceof(Response);
|
|
expect(res.headers).to.be.an.instanceof(Headers);
|
|
expect(res.body).to.be.an.instanceof(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('Response.redirect should resolve into response', () => {
|
|
const res = Response.redirect('http://localhost');
|
|
expect(res).to.be.an.instanceof(Response);
|
|
expect(res.headers).to.be.an.instanceof(Headers);
|
|
expect(res.headers.get('location')).to.equal('http://localhost/');
|
|
expect(res.status).to.equal(302);
|
|
});
|
|
|
|
it('Response.redirect /w invalid url should fail', () => {
|
|
expect(() => {
|
|
Response.redirect('localhost');
|
|
}).to.throw();
|
|
});
|
|
|
|
it('Response.redirect /w invalid status should fail', () => {
|
|
expect(() => {
|
|
Response.redirect('http://localhost', 200);
|
|
}).to.throw();
|
|
});
|
|
|
|
it('should accept plain text response', () => {
|
|
const url = `${base}plain`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('text');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should accept html response (like plain text)', () => {
|
|
const url = `${base}html`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/html');
|
|
return res.text().then(result => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('<html></html>');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should accept json response', () => {
|
|
const url = `${base}json`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('application/json');
|
|
return res.json().then(result => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
expect(result).to.be.an('object');
|
|
expect(result).to.deep.equal({name: 'value'});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should send request with custom headers', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: {'x-custom-header': 'abc'}
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers['x-custom-header']).to.equal('abc');
|
|
});
|
|
});
|
|
|
|
it('should accept headers instance', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: new Headers({'x-custom-header': 'abc'})
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers['x-custom-header']).to.equal('abc');
|
|
});
|
|
});
|
|
|
|
it('should accept custom host header', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: {
|
|
host: 'example.com'
|
|
}
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers.host).to.equal('example.com');
|
|
});
|
|
});
|
|
|
|
it('should accept custom HoSt header', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: {
|
|
HoSt: 'example.com'
|
|
}
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers.host).to.equal('example.com');
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 301', () => {
|
|
const url = `${base}redirect/301`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 302', () => {
|
|
const url = `${base}redirect/302`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 303', () => {
|
|
const url = `${base}redirect/303`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 307', () => {
|
|
const url = `${base}redirect/307`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 308', () => {
|
|
const url = `${base}redirect/308`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should follow redirect chain', () => {
|
|
const url = `${base}redirect/chain`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should follow POST request redirect code 301 with GET', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(result => {
|
|
expect(result.method).to.equal('GET');
|
|
expect(result.body).to.equal('');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should follow PATCH request redirect code 301 with PATCH', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
method: 'PATCH',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(res => {
|
|
expect(res.method).to.equal('PATCH');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should follow POST request redirect code 302 with GET', () => {
|
|
const url = `${base}redirect/302`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(result => {
|
|
expect(result.method).to.equal('GET');
|
|
expect(result.body).to.equal('');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should follow PATCH request redirect code 302 with PATCH', () => {
|
|
const url = `${base}redirect/302`;
|
|
const options = {
|
|
method: 'PATCH',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(res => {
|
|
expect(res.method).to.equal('PATCH');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 303 with GET', () => {
|
|
const url = `${base}redirect/303`;
|
|
const options = {
|
|
method: 'PUT',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(result => {
|
|
expect(result.method).to.equal('GET');
|
|
expect(result.body).to.equal('');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should follow PATCH request redirect code 307 with PATCH', () => {
|
|
const url = `${base}redirect/307`;
|
|
const options = {
|
|
method: 'PATCH',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
return res.json().then(result => {
|
|
expect(result.method).to.equal('PATCH');
|
|
expect(result.body).to.equal('a=1');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not follow non-GET redirect if body is a readable stream', () => {
|
|
const url = `${base}redirect/307`;
|
|
const options = {
|
|
method: 'PATCH',
|
|
body: stream.Readable.from('tada')
|
|
};
|
|
return expect(fetch(url, options)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('type', 'unsupported-redirect');
|
|
});
|
|
|
|
it('should obey maximum redirect, reject case', () => {
|
|
const url = `${base}redirect/chain`;
|
|
const options = {
|
|
follow: 1
|
|
};
|
|
return expect(fetch(url, options)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('type', 'max-redirect');
|
|
});
|
|
|
|
it('should obey redirect chain, resolve case', () => {
|
|
const url = `${base}redirect/chain`;
|
|
const options = {
|
|
follow: 2
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should allow not following redirect', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
follow: 0
|
|
};
|
|
return expect(fetch(url, options)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('type', 'max-redirect');
|
|
});
|
|
|
|
it('should support redirect mode, manual flag', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
redirect: 'manual'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.status).to.equal(301);
|
|
expect(res.headers.get('location')).to.equal(`${base}inspect`);
|
|
});
|
|
});
|
|
|
|
it('should support redirect mode, manual flag, broken Location header', () => {
|
|
const url = `${base}redirect/bad-location`;
|
|
const options = {
|
|
redirect: 'manual'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.status).to.equal(301);
|
|
expect(res.headers.get('location')).to.equal(`${base}redirect/%C3%A2%C2%98%C2%83`);
|
|
});
|
|
});
|
|
|
|
it('should support redirect mode, error flag', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
redirect: 'error'
|
|
};
|
|
return expect(fetch(url, options)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('type', 'no-redirect');
|
|
});
|
|
|
|
it('should support redirect mode, manual flag when there is no redirect', () => {
|
|
const url = `${base}hello`;
|
|
const options = {
|
|
redirect: 'manual'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.status).to.equal(200);
|
|
expect(res.headers.get('location')).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should follow redirect code 301 and keep existing headers', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
headers: new Headers({'x-custom-header': 'abc'})
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(`${base}inspect`);
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers['x-custom-header']).to.equal('abc');
|
|
});
|
|
});
|
|
|
|
it('should treat broken redirect as ordinary response (follow)', () => {
|
|
const url = `${base}redirect/no-location`;
|
|
return fetch(url).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.status).to.equal(301);
|
|
expect(res.headers.get('location')).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should treat broken redirect as ordinary response (manual)', () => {
|
|
const url = `${base}redirect/no-location`;
|
|
const options = {
|
|
redirect: 'manual'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.status).to.equal(301);
|
|
expect(res.headers.get('location')).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should throw a TypeError on an invalid redirect option', () => {
|
|
const url = `${base}redirect/301`;
|
|
const options = {
|
|
redirect: 'foobar'
|
|
};
|
|
return fetch(url, options).then(() => {
|
|
expect.fail();
|
|
}, error => {
|
|
expect(error).to.be.an.instanceOf(TypeError);
|
|
expect(error.message).to.equal('Redirect option \'foobar\' is not a valid value of RequestRedirect');
|
|
});
|
|
});
|
|
|
|
it('should set redirected property on response when redirect', () => {
|
|
const url = `${base}redirect/301`;
|
|
return fetch(url).then(res => {
|
|
expect(res.redirected).to.be.true;
|
|
});
|
|
});
|
|
|
|
it('should not set redirected property on response without redirect', () => {
|
|
const url = `${base}hello`;
|
|
return fetch(url).then(res => {
|
|
expect(res.redirected).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should ignore invalid headers', () => {
|
|
const headers = fromRawHeaders([
|
|
'Invalid-Header ',
|
|
'abc\r\n',
|
|
'Invalid-Header-Value',
|
|
'\u0007k\r\n',
|
|
'Cookie',
|
|
'\u0007k\r\n',
|
|
'Cookie',
|
|
'\u0007kk\r\n'
|
|
]);
|
|
expect(headers).to.be.instanceOf(Headers);
|
|
expect(headers.raw()).to.deep.equal({});
|
|
});
|
|
|
|
it('should handle client-error response', () => {
|
|
const url = `${base}error/400`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
expect(res.status).to.equal(400);
|
|
expect(res.statusText).to.equal('Bad Request');
|
|
expect(res.ok).to.be.false;
|
|
return res.text().then(result => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('client error');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle server-error response', () => {
|
|
const url = `${base}error/500`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
expect(res.status).to.equal(500);
|
|
expect(res.statusText).to.equal('Internal Server Error');
|
|
expect(res.ok).to.be.false;
|
|
return res.text().then(result => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('server error');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle network-error response', () => {
|
|
const url = `${base}error/reset`;
|
|
return expect(fetch(url)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('code', 'ECONNRESET');
|
|
});
|
|
|
|
it('should handle network-error partial response', () => {
|
|
const url = `${base}error/premature`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
return expect(res.text()).to.eventually.be.rejectedWith(Error)
|
|
.and.have.property('message').matches(/Premature close|The operation was aborted/);
|
|
});
|
|
});
|
|
|
|
it('should handle network-error in chunked response', () => {
|
|
const url = `${base}error/premature/chunked`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
|
|
return expect(new Promise((resolve, reject) => {
|
|
res.body.on('error', reject);
|
|
res.body.on('close', resolve);
|
|
})).to.eventually.be.rejectedWith(Error, 'Premature close')
|
|
.and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
|
|
});
|
|
});
|
|
|
|
it('should handle network-error in chunked response async iterator', () => {
|
|
const url = `${base}error/premature/chunked`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
|
|
const read = async body => {
|
|
const chunks = [];
|
|
|
|
if (process.version < 'v14') {
|
|
// In Node.js 12, some errors don't come out in the async iterator; we have to pick
|
|
// them up from the event-emitter and then throw them after the async iterator
|
|
let error;
|
|
body.on('error', err => {
|
|
error = err;
|
|
});
|
|
|
|
for await (const chunk of body) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
body.on('close', () => resolve(chunks));
|
|
});
|
|
}
|
|
|
|
for await (const chunk of body) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
return chunks;
|
|
};
|
|
|
|
return expect(read(res.body))
|
|
.to.eventually.be.rejectedWith(Error, 'Premature close')
|
|
.and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE');
|
|
});
|
|
});
|
|
|
|
it('should handle network-error in chunked response in consumeBody', () => {
|
|
const url = `${base}error/premature/chunked`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
|
|
return expect(res.text())
|
|
.to.eventually.be.rejectedWith(Error, 'Premature close');
|
|
});
|
|
});
|
|
|
|
it('should handle DNS-error response', () => {
|
|
const url = 'http://domain.invalid';
|
|
return expect(fetch(url)).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('code').that.matches(/ENOTFOUND|EAI_AGAIN/);
|
|
});
|
|
|
|
it('should reject invalid json response', () => {
|
|
const url = `${base}error/json`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('application/json');
|
|
return expect(res.json()).to.eventually.be.rejectedWith(Error);
|
|
});
|
|
});
|
|
|
|
it('should handle response with no status text', () => {
|
|
const url = `${base}no-status-text`;
|
|
return fetch(url).then(res => {
|
|
expect(res.statusText).to.equal('');
|
|
});
|
|
});
|
|
|
|
it('should handle no content response', () => {
|
|
const url = `${base}no-content`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(204);
|
|
expect(res.statusText).to.equal('No Content');
|
|
expect(res.ok).to.be.true;
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.be.empty;
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should reject when trying to parse no content response as json', () => {
|
|
const url = `${base}no-content`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(204);
|
|
expect(res.statusText).to.equal('No Content');
|
|
expect(res.ok).to.be.true;
|
|
return expect(res.json()).to.eventually.be.rejectedWith(Error);
|
|
});
|
|
});
|
|
|
|
it('should handle no content response with gzip encoding', () => {
|
|
const url = `${base}no-content/gzip`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(204);
|
|
expect(res.statusText).to.equal('No Content');
|
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
|
expect(res.ok).to.be.true;
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.be.empty;
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle not modified response', () => {
|
|
const url = `${base}not-modified`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(304);
|
|
expect(res.statusText).to.equal('Not Modified');
|
|
expect(res.ok).to.be.false;
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.be.empty;
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle not modified response with gzip encoding', () => {
|
|
const url = `${base}not-modified/gzip`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(304);
|
|
expect(res.statusText).to.equal('Not Modified');
|
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
|
expect(res.ok).to.be.false;
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.be.empty;
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should decompress gzip response', () => {
|
|
const url = `${base}gzip`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('hello world');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should decompress slightly invalid gzip response', () => {
|
|
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 make capitalised Content-Encoding lowercase', () => {
|
|
const url = `${base}gzip-capital`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('hello world');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should decompress deflate response', () => {
|
|
const url = `${base}deflate`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('hello world');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should decompress deflate raw response from old apache server', () => {
|
|
const url = `${base}deflate-raw`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('hello world');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should decompress brotli response', function () {
|
|
if (typeof zlib.createBrotliDecompress !== 'function') {
|
|
this.skip();
|
|
}
|
|
|
|
const url = `${base}brotli`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('hello world');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should handle no content response with brotli encoding', function () {
|
|
if (typeof zlib.createBrotliDecompress !== 'function') {
|
|
this.skip();
|
|
}
|
|
|
|
const url = `${base}no-content/brotli`;
|
|
return fetch(url).then(res => {
|
|
expect(res.status).to.equal(204);
|
|
expect(res.statusText).to.equal('No Content');
|
|
expect(res.headers.get('content-encoding')).to.equal('br');
|
|
expect(res.ok).to.be.true;
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.be.empty;
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should skip decompression if unsupported', () => {
|
|
const url = `${base}sdch`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(result => {
|
|
expect(result).to.be.a('string');
|
|
expect(result).to.equal('fake sdch string');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should reject if response compression is invalid', () => {
|
|
const url = `${base}invalid-content-encoding`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return expect(res.text()).to.eventually.be.rejected
|
|
.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', done => {
|
|
const url = `${base}invalid-content-encoding`;
|
|
fetch(url)
|
|
.then(res => {
|
|
expect(res.status).to.equal(200);
|
|
})
|
|
.catch(() => { })
|
|
.then(() => {
|
|
// Wait a few ms to see if a uncaught error occurs
|
|
setTimeout(() => {
|
|
done();
|
|
}, 20);
|
|
});
|
|
});
|
|
|
|
it('should collect handled errors on the body stream to reject if the body is used later', () => {
|
|
const url = `${base}invalid-content-encoding`;
|
|
return fetch(url).then(delay(20)).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return expect(res.text()).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.have.property('code', 'Z_DATA_ERROR');
|
|
});
|
|
});
|
|
|
|
it('should allow disabling auto decompression', () => {
|
|
const url = `${base}gzip`;
|
|
const options = {
|
|
compress: false
|
|
};
|
|
return fetch(url, options).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', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
compress: true,
|
|
headers: {
|
|
'Accept-Encoding': 'gzip'
|
|
}
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.headers['accept-encoding']).to.equal('gzip');
|
|
});
|
|
});
|
|
|
|
const testAbortController = (name, buildAbortController, moreTests = null) => {
|
|
describe(`AbortController (${name})`, () => {
|
|
let controller;
|
|
|
|
beforeEach(() => {
|
|
controller = buildAbortController();
|
|
});
|
|
|
|
it('should support request cancellation with signal', () => {
|
|
const fetches = [
|
|
fetch(
|
|
`${base}timeout`,
|
|
{
|
|
method: 'POST',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
body: JSON.stringify({hello: 'world'})
|
|
}
|
|
}
|
|
)
|
|
];
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, 100);
|
|
|
|
return Promise.all(fetches.map(fetched => expect(fetched)
|
|
.to.eventually.be.rejected
|
|
.and.be.an.instanceOf(Error)
|
|
.and.include({
|
|
type: 'aborted',
|
|
name: 'AbortError'
|
|
})
|
|
));
|
|
});
|
|
|
|
it('should support multiple request cancellation with signal', () => {
|
|
const fetches = [
|
|
fetch(`${base}timeout`, {signal: controller.signal}),
|
|
fetch(
|
|
`${base}timeout`,
|
|
{
|
|
method: 'POST',
|
|
signal: controller.signal,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
body: JSON.stringify({hello: 'world'})
|
|
}
|
|
}
|
|
)
|
|
];
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, 100);
|
|
|
|
return Promise.all(fetches.map(fetched => expect(fetched)
|
|
.to.eventually.be.rejected
|
|
.and.be.an.instanceOf(Error)
|
|
.and.include({
|
|
type: 'aborted',
|
|
name: 'AbortError'
|
|
})
|
|
));
|
|
});
|
|
|
|
it('should reject immediately if signal has already been aborted', () => {
|
|
const url = `${base}timeout`;
|
|
const options = {
|
|
signal: controller.signal
|
|
};
|
|
controller.abort();
|
|
const fetched = fetch(url, options);
|
|
return expect(fetched).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(Error)
|
|
.and.include({
|
|
type: 'aborted',
|
|
name: 'AbortError'
|
|
});
|
|
});
|
|
|
|
it('should allow redirects to be aborted', () => {
|
|
const request = new Request(`${base}redirect/slow`, {
|
|
signal: controller.signal
|
|
});
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, 20);
|
|
return expect(fetch(request)).to.be.eventually.rejected
|
|
.and.be.an.instanceOf(Error)
|
|
.and.have.property('name', 'AbortError');
|
|
});
|
|
|
|
it('should allow redirected response body to be aborted', () => {
|
|
const request = new Request(`${base}redirect/slow-stream`, {
|
|
signal: controller.signal
|
|
});
|
|
return expect(fetch(request).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
const result = res.text();
|
|
controller.abort();
|
|
return result;
|
|
})).to.be.eventually.rejected
|
|
.and.be.an.instanceOf(Error)
|
|
.and.have.property('name', 'AbortError');
|
|
});
|
|
|
|
it('should reject response body with AbortError when aborted before stream has been read completely', () => {
|
|
return expect(fetch(
|
|
`${base}slow`,
|
|
{signal: controller.signal}
|
|
))
|
|
.to.eventually.be.fulfilled
|
|
.then(res => {
|
|
const promise = res.text();
|
|
controller.abort();
|
|
return expect(promise)
|
|
.to.eventually.be.rejected
|
|
.and.be.an.instanceof(Error)
|
|
.and.have.property('name', 'AbortError');
|
|
});
|
|
});
|
|
|
|
it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => {
|
|
return expect(fetch(
|
|
`${base}slow`,
|
|
{signal: controller.signal}
|
|
))
|
|
.to.eventually.be.fulfilled
|
|
.then(res => {
|
|
controller.abort();
|
|
return expect(res.text())
|
|
.to.eventually.be.rejected
|
|
.and.be.an.instanceof(Error)
|
|
.and.have.property('name', 'AbortError');
|
|
});
|
|
});
|
|
|
|
it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', done => {
|
|
expect(fetch(
|
|
`${base}slow`,
|
|
{signal: controller.signal}
|
|
))
|
|
.to.eventually.be.fulfilled
|
|
.then(res => {
|
|
res.body.once('error', err => {
|
|
expect(err)
|
|
.to.be.an.instanceof(Error)
|
|
.and.have.property('name', 'AbortError');
|
|
done();
|
|
});
|
|
controller.abort();
|
|
});
|
|
});
|
|
|
|
it('should cancel request body of type Stream with AbortError when aborted', () => {
|
|
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 (error_) {
|
|
reject(error_);
|
|
}
|
|
});
|
|
}),
|
|
expect(promise).to.eventually.be.rejected
|
|
.and.be.an.instanceof(Error)
|
|
.and.have.property('name', 'AbortError')
|
|
]);
|
|
|
|
controller.abort();
|
|
|
|
return result;
|
|
});
|
|
|
|
if (moreTests) {
|
|
moreTests();
|
|
}
|
|
});
|
|
};
|
|
|
|
testAbortController('polyfill',
|
|
() => new AbortControllerPolyfill(),
|
|
() => {
|
|
it('should remove internal AbortSignal event listener after request is aborted', () => {
|
|
const controller = new AbortControllerPolyfill();
|
|
const {signal} = controller;
|
|
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, 20);
|
|
|
|
return expect(fetch(`${base}timeout`, {signal}))
|
|
.to.eventually.be.rejected
|
|
.and.be.an.instanceof(Error)
|
|
.and.have.property('name', 'AbortError')
|
|
.then(() => {
|
|
return expect(signal.listeners.abort.length).to.equal(0);
|
|
});
|
|
});
|
|
|
|
it('should remove internal AbortSignal event listener after request and response complete without aborting', () => {
|
|
const controller = new AbortControllerPolyfill();
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
);
|
|
|
|
testAbortController('mysticatea', () => new AbortControllerMysticatea());
|
|
|
|
if (process.version > 'v15') {
|
|
testAbortController('native', () => new AbortController());
|
|
}
|
|
|
|
it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => {
|
|
return Promise.all([
|
|
expect(fetch(`${base}inspect`, {signal: {}}))
|
|
.to.be.eventually.rejected
|
|
.and.be.an.instanceof(TypeError)
|
|
.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 gracefully handle a nullish signal', () => {
|
|
return Promise.all([
|
|
fetch(`${base}hello`, {signal: null}).then(res => {
|
|
return expect(res.ok).to.be.true;
|
|
}),
|
|
fetch(`${base}hello`, {signal: undefined}).then(res => {
|
|
return expect(res.ok).to.be.true;
|
|
})
|
|
]);
|
|
});
|
|
|
|
it('should set default User-Agent', () => {
|
|
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', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: {
|
|
'user-agent': 'faked'
|
|
}
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.headers['user-agent']).to.equal('faked');
|
|
});
|
|
});
|
|
|
|
it('should set default Accept header', () => {
|
|
const url = `${base}inspect`;
|
|
fetch(url).then(res => res.json()).then(res => {
|
|
expect(res.headers.accept).to.equal('*/*');
|
|
});
|
|
});
|
|
|
|
it('should allow setting Accept header', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
headers: {
|
|
accept: 'application/json'
|
|
}
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.headers.accept).to.equal('application/json');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('0');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with string body', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with buffer body', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: Buffer.from('a=1', 'utf-8')
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBuffer body', () => {
|
|
const encoder = new TextEncoder();
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: encoder.encode('Hello, world!\n').buffer
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('Hello, world!\n');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('14');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBuffer body from a VM context', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: new VMUint8Array(Buffer.from('Hello, world!\n')).buffer
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('Hello, world!\n');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('14');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBufferView (Uint8Array) body', () => {
|
|
const encoder = new TextEncoder();
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: encoder.encode('Hello, world!\n')
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('Hello, world!\n');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('14');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBufferView (DataView) body', () => {
|
|
const encoder = new TextEncoder();
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: new DataView(encoder.encode('Hello, world!\n').buffer)
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('Hello, world!\n');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('14');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: new VMUint8Array(Buffer.from('Hello, world!\n'))
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('Hello, world!\n');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('14');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', () => {
|
|
const encoder = new TextEncoder();
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: encoder.encode('Hello, world!\n').subarray(7, 13)
|
|
};
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('world!');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('6');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with blob body without type', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: new Blob(['a=1'])
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with blob body with type', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: new Blob(['a=1'], {
|
|
type: 'text/plain;charset=UTF-8'
|
|
})
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.equal('text/plain;charset=utf-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with readable stream as body', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: stream.Readable.from('a=1')
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.equal('chunked');
|
|
expect(res.headers['content-type']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.be.undefined;
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with form-data as body', () => {
|
|
const form = new FormData();
|
|
form.append('a', '1');
|
|
|
|
const url = `${base}multipart`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: form
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary=');
|
|
expect(res.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', () => {
|
|
const form = new FormData();
|
|
form.append('my_field', fs.createReadStream('test/utils/dummy.txt'));
|
|
|
|
const url = `${base}multipart`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: form
|
|
};
|
|
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.startWith('multipart/form-data;boundary=');
|
|
expect(res.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', () => {
|
|
const form = new FormData();
|
|
form.append('a', '1');
|
|
|
|
const headers = form.getHeaders();
|
|
headers.b = '2';
|
|
|
|
const url = `${base}multipart`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: form,
|
|
headers
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.startWith('multipart/form-data; boundary=');
|
|
expect(res.headers['content-length']).to.be.a('string');
|
|
expect(res.headers.b).to.equal('2');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
it('should support spec-compliant form-data as POST body', () => {
|
|
const form = new FormDataNode();
|
|
|
|
const filename = path.join('test', 'utils', 'dummy.txt');
|
|
|
|
form.set('field', 'some text');
|
|
form.set('file', fs.createReadStream(filename), {
|
|
size: fs.statSync(filename).size
|
|
});
|
|
|
|
const url = `${base}multipart`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: form
|
|
};
|
|
|
|
return fetch(url, options).then(res => res.json()).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.startWith('multipart/form-data');
|
|
expect(res.body).to.contain('field=');
|
|
expect(res.body).to.contain('file=');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with object body', () => {
|
|
const url = `${base}inspect`;
|
|
// Note that fetch simply calls tostring on an object
|
|
const options = {
|
|
method: 'POST',
|
|
body: {a: 1}
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('[object Object]');
|
|
expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('15');
|
|
});
|
|
});
|
|
|
|
it('constructing a Response with URLSearchParams as body should have a Content-Type', () => {
|
|
const parameters = new URLSearchParams();
|
|
const res = new Response(parameters);
|
|
res.headers.get('Content-Type');
|
|
expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8');
|
|
});
|
|
|
|
it('constructing a Request with URLSearchParams as body should have a Content-Type', () => {
|
|
const parameters = new URLSearchParams();
|
|
const request = new Request(base, {method: 'POST', body: parameters});
|
|
expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8');
|
|
});
|
|
|
|
it('Reading a body with URLSearchParams should echo back the result', () => {
|
|
const parameters = new URLSearchParams();
|
|
parameters.append('a', '1');
|
|
return new Response(parameters).text().then(text => {
|
|
expect(text).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
// Body should been cloned...
|
|
it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', () => {
|
|
const parameters = new URLSearchParams();
|
|
const request = new Request(`${base}inspect`, {method: 'POST', body: parameters});
|
|
parameters.append('a', '1');
|
|
return request.text().then(text => {
|
|
expect(text).to.equal('');
|
|
});
|
|
});
|
|
|
|
it('should allow POST request with URLSearchParams as body', () => {
|
|
const parameters = new URLSearchParams();
|
|
parameters.append('a', '1');
|
|
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: parameters
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
it('should still recognize URLSearchParams when extended', () => {
|
|
class CustomSearchParameters extends URLSearchParams { }
|
|
const parameters = new CustomSearchParameters();
|
|
parameters.append('a', '1');
|
|
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: parameters
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
/* 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', () => {
|
|
class CustomPolyfilledSearchParameters extends URLSearchParams { }
|
|
const parameters = new CustomPolyfilledSearchParameters();
|
|
parameters.append('a', '1');
|
|
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'POST',
|
|
body: parameters
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
it('should overwrite Content-Length if possible', () => {
|
|
const url = `${base}inspect`;
|
|
// Note that fetch simply calls tostring on an object
|
|
const options = {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Length': '1000'
|
|
},
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('POST');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-type']).to.equal('text/plain;charset=UTF-8');
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow PUT request', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'PUT',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('PUT');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
it('should allow DELETE request', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'DELETE'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('DELETE');
|
|
});
|
|
});
|
|
|
|
it('should allow DELETE request with string body', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'DELETE',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('DELETE');
|
|
expect(res.body).to.equal('a=1');
|
|
expect(res.headers['transfer-encoding']).to.be.undefined;
|
|
expect(res.headers['content-length']).to.equal('3');
|
|
});
|
|
});
|
|
|
|
it('should allow PATCH request', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
method: 'PATCH',
|
|
body: 'a=1'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.method).to.equal('PATCH');
|
|
expect(res.body).to.equal('a=1');
|
|
});
|
|
});
|
|
|
|
it('should allow HEAD request', () => {
|
|
const url = `${base}hello`;
|
|
const options = {
|
|
method: 'HEAD'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.statusText).to.equal('OK');
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
expect(res.body).to.be.an.instanceof(stream.Transform);
|
|
return res.text();
|
|
}).then(text => {
|
|
expect(text).to.equal('');
|
|
});
|
|
});
|
|
|
|
it('should allow HEAD request with content-encoding header', () => {
|
|
const url = `${base}error/404`;
|
|
const options = {
|
|
method: 'HEAD'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.status).to.equal(404);
|
|
expect(res.headers.get('content-encoding')).to.equal('gzip');
|
|
return res.text();
|
|
}).then(text => {
|
|
expect(text).to.equal('');
|
|
});
|
|
});
|
|
|
|
it('should allow OPTIONS request', () => {
|
|
const url = `${base}options`;
|
|
const options = {
|
|
method: 'OPTIONS'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.statusText).to.equal('OK');
|
|
expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS');
|
|
expect(res.body).to.be.an.instanceof(stream.Transform);
|
|
});
|
|
});
|
|
|
|
it('should reject decoding body twice', () => {
|
|
const url = `${base}plain`;
|
|
return fetch(url).then(res => {
|
|
expect(res.headers.get('content-type')).to.equal('text/plain');
|
|
return res.text().then(() => {
|
|
expect(res.bodyUsed).to.be.true;
|
|
return expect(res.text()).to.eventually.be.rejectedWith(Error);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should support maximum response size, multiple chunk', () => {
|
|
const url = `${base}size/chunk`;
|
|
const options = {
|
|
size: 5
|
|
};
|
|
return fetch(url, options).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', () => {
|
|
const url = `${base}size/long`;
|
|
const options = {
|
|
size: 5
|
|
};
|
|
return fetch(url, options).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', () => {
|
|
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', () => {
|
|
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', () => {
|
|
const url = `${base}json`;
|
|
return fetch(url).then(res => {
|
|
const r1 = res.clone();
|
|
return Promise.all([res.json(), r1.text()]).then(results => {
|
|
expect(results[0]).to.deep.equal({name: 'value'});
|
|
expect(results[1]).to.equal('{"name":"value"}');
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should allow cloning a json response, and then log it as text response', () => {
|
|
const url = `${base}json`;
|
|
return fetch(url).then(res => {
|
|
const r1 = res.clone();
|
|
return res.json().then(result => {
|
|
expect(result).to.deep.equal({name: 'value'});
|
|
return r1.text().then(result => {
|
|
expect(result).to.equal('{"name":"value"}');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should allow cloning a json response, first log as text response, then return json object', () => {
|
|
const url = `${base}json`;
|
|
return fetch(url).then(res => {
|
|
const r1 = res.clone();
|
|
return r1.text().then(result => {
|
|
expect(result).to.equal('{"name":"value"}');
|
|
return res.json().then(result => {
|
|
expect(result).to.deep.equal({name: 'value'});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
it('should not allow cloning a response after its been used', () => {
|
|
const url = `${base}hello`;
|
|
return fetch(url).then(res =>
|
|
res.text().then(() => {
|
|
expect(() => {
|
|
res.clone();
|
|
}).to.throw(Error);
|
|
})
|
|
);
|
|
});
|
|
|
|
it('the default highWaterMark should equal 16384', () => {
|
|
const url = `${base}hello`;
|
|
return fetch(url).then(res => {
|
|
expect(res.highWaterMark).to.equal(16384);
|
|
});
|
|
});
|
|
|
|
it('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () {
|
|
this.timeout(300);
|
|
const url = local.mockResponse(res => {
|
|
// Observed behavior of TCP packets splitting:
|
|
// - response body size <= 65438 → single packet sent
|
|
// - response body size > 65438 → multiple packets sent
|
|
// Max TCP packet size is 64kB (https://stackoverflow.com/a/2614188/5763764),
|
|
// but first packet probably transfers more than the response body.
|
|
const firstPacketMaxSize = 65438;
|
|
const secondPacketSize = 16 * 1024; // = defaultHighWaterMark
|
|
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize));
|
|
});
|
|
return expect(
|
|
fetch(url).then(res => res.clone().buffer())
|
|
).to.timeout;
|
|
});
|
|
|
|
it('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () {
|
|
this.timeout(300);
|
|
const url = local.mockResponse(res => {
|
|
const firstPacketMaxSize = 65438;
|
|
const secondPacketSize = 10;
|
|
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize));
|
|
});
|
|
return expect(
|
|
fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer())
|
|
).to.timeout;
|
|
});
|
|
|
|
it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () {
|
|
this.timeout(300);
|
|
const url = local.mockResponse(res => {
|
|
const firstPacketMaxSize = 65438;
|
|
const secondPacketSize = 16 * 1024; // = defaultHighWaterMark
|
|
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1));
|
|
});
|
|
return expect(
|
|
fetch(url).then(res => res.clone().buffer())
|
|
).not.to.timeout;
|
|
});
|
|
|
|
it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () {
|
|
this.timeout(300);
|
|
const url = local.mockResponse(res => {
|
|
const firstPacketMaxSize = 65438;
|
|
const secondPacketSize = 10;
|
|
res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1));
|
|
});
|
|
return expect(
|
|
fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer())
|
|
).not.to.timeout;
|
|
});
|
|
|
|
it('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () {
|
|
this.timeout(300);
|
|
const url = local.mockResponse(res => {
|
|
res.end(crypto.randomBytes((2 * 512 * 1024) - 1));
|
|
});
|
|
return expect(
|
|
fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer())
|
|
).not.to.timeout;
|
|
});
|
|
|
|
it('should allow get all responses of a header', () => {
|
|
const url = `${base}cookie`;
|
|
return fetch(url).then(res => {
|
|
const expected = 'a=1, b=1';
|
|
expect(res.headers.get('set-cookie')).to.equal(expected);
|
|
expect(res.headers.get('Set-Cookie')).to.equal(expected);
|
|
});
|
|
});
|
|
|
|
it('should return all headers using raw()', () => {
|
|
const url = `${base}cookie`;
|
|
return fetch(url).then(res => {
|
|
const expected = [
|
|
'a=1',
|
|
'b=1'
|
|
];
|
|
|
|
expect(res.headers.raw()['set-cookie']).to.deep.equal(expected);
|
|
});
|
|
});
|
|
|
|
it('should allow deleting header', () => {
|
|
const url = `${base}cookie`;
|
|
return fetch(url).then(res => {
|
|
res.headers.delete('set-cookie');
|
|
expect(res.headers.get('set-cookie')).to.be.null;
|
|
});
|
|
});
|
|
|
|
it('should send request with connection keep-alive if agent is provided', () => {
|
|
const url = `${base}inspect`;
|
|
const options = {
|
|
agent: new http.Agent({
|
|
keepAlive: true
|
|
})
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
expect(res.headers.connection).to.equal('keep-alive');
|
|
});
|
|
});
|
|
|
|
it('should support fetch with Request instance', () => {
|
|
const url = `${base}hello`;
|
|
const request = new Request(url);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should support fetch with Node.js URL object', () => {
|
|
const url = `${base}hello`;
|
|
const urlObject = new URL(url);
|
|
const request = new Request(urlObject);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should support fetch with WHATWG URL object', () => {
|
|
const url = `${base}hello`;
|
|
const urlObject = new URL(url);
|
|
const request = new Request(urlObject);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should keep `?` sign in URL when no params are given', () => {
|
|
const url = `${base}question?`;
|
|
const urlObject = new URL(url);
|
|
const request = new Request(urlObject);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('if params are given, do not modify anything', () => {
|
|
const url = `${base}question?a=1`;
|
|
const urlObject = new URL(url);
|
|
const request = new Request(urlObject);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should preserve the hash (#) symbol', () => {
|
|
const url = `${base}question?#`;
|
|
const urlObject = new URL(url);
|
|
const request = new Request(urlObject);
|
|
return fetch(request).then(res => {
|
|
expect(res.url).to.equal(url);
|
|
expect(res.ok).to.be.true;
|
|
expect(res.status).to.equal(200);
|
|
});
|
|
});
|
|
|
|
it('should support reading blob as text', () => {
|
|
return new Response('hello')
|
|
.blob()
|
|
.then(blob => blob.text())
|
|
.then(body => {
|
|
expect(body).to.equal('hello');
|
|
});
|
|
});
|
|
|
|
it('should support reading blob as arrayBuffer', () => {
|
|
return new Response('hello')
|
|
.blob()
|
|
.then(blob => blob.arrayBuffer())
|
|
.then(ab => {
|
|
const string = String.fromCharCode.apply(null, new Uint8Array(ab));
|
|
expect(string).to.equal('hello');
|
|
});
|
|
});
|
|
|
|
it('should support reading blob as stream', () => {
|
|
return new Response('hello')
|
|
.blob()
|
|
.then(blob => streamToPromise(blob.stream(), data => {
|
|
const string = data.toString();
|
|
expect(string).to.equal('hello');
|
|
}));
|
|
});
|
|
|
|
it('should support blob round-trip', () => {
|
|
const url = `${base}hello`;
|
|
|
|
let length;
|
|
let type;
|
|
|
|
return fetch(url).then(res => res.blob()).then(blob => {
|
|
const url = `${base}inspect`;
|
|
length = blob.size;
|
|
type = blob.type;
|
|
return fetch(url, {
|
|
method: 'POST',
|
|
body: blob
|
|
});
|
|
}).then(res => res.json()).then(({body, headers}) => {
|
|
expect(body).to.equal('world');
|
|
expect(headers['content-type']).to.equal(type);
|
|
expect(headers['content-length']).to.equal(String(length));
|
|
});
|
|
});
|
|
|
|
it('should support overwrite Request instance', () => {
|
|
const url = `${base}inspect`;
|
|
const request = new Request(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
a: '1'
|
|
}
|
|
});
|
|
return fetch(request, {
|
|
method: 'GET',
|
|
headers: {
|
|
a: '2'
|
|
}
|
|
}).then(res => {
|
|
return res.json();
|
|
}).then(body => {
|
|
expect(body.method).to.equal('GET');
|
|
expect(body.headers.a).to.equal('2');
|
|
});
|
|
});
|
|
|
|
it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => {
|
|
const body = new Body('a=1');
|
|
expect(body).to.have.property('arrayBuffer');
|
|
expect(body).to.have.property('blob');
|
|
expect(body).to.have.property('text');
|
|
expect(body).to.have.property('json');
|
|
expect(body).to.have.property('buffer');
|
|
});
|
|
|
|
/* eslint-disable-next-line func-names */
|
|
it('should create custom FetchError', function funcName() {
|
|
const systemError = new Error('system');
|
|
systemError.code = 'ESOMEERROR';
|
|
|
|
const err = new FetchError('test message', 'test-error', systemError);
|
|
expect(err).to.be.an.instanceof(Error);
|
|
expect(err).to.be.an.instanceof(FetchError);
|
|
expect(err.name).to.equal('FetchError');
|
|
expect(err.message).to.equal('test message');
|
|
expect(err.type).to.equal('test-error');
|
|
expect(err.code).to.equal('ESOMEERROR');
|
|
expect(err.errno).to.equal('ESOMEERROR');
|
|
// Reading the stack is quite slow (~30-50ms)
|
|
expect(err.stack).to.include('funcName').and.to.startWith(`${err.name}: ${err.message}`);
|
|
});
|
|
|
|
it('should support https request', function () {
|
|
this.timeout(5000);
|
|
const url = 'https://github.com/';
|
|
const options = {
|
|
method: 'HEAD'
|
|
};
|
|
return fetch(url, options).then(res => {
|
|
expect(res.status).to.equal(200);
|
|
expect(res.ok).to.be.true;
|
|
});
|
|
});
|
|
|
|
// Issue #414
|
|
it('should reject if attempt to accumulate body stream throws', () => {
|
|
const res = new Response(stream.Readable.from((async function * () {
|
|
yield Buffer.from('tada');
|
|
await new Promise(resolve => {
|
|
setTimeout(resolve, 200);
|
|
});
|
|
yield {tada: 'yes'};
|
|
})()));
|
|
|
|
return expect(res.text()).to.eventually.be.rejected
|
|
.and.be.an.instanceOf(FetchError)
|
|
.and.include({type: 'system'})
|
|
.and.have.property('message').that.include('Could not create Buffer');
|
|
});
|
|
|
|
it('supports supplying a lookup function to the agent', () => {
|
|
const url = `${base}redirect/301`;
|
|
let called = 0;
|
|
function lookupSpy(hostname, options, callback) {
|
|
called++;
|
|
return lookup(hostname, options, callback);
|
|
}
|
|
|
|
const agent = http.Agent({lookup: lookupSpy});
|
|
return fetch(url, {agent}).then(() => {
|
|
expect(called).to.equal(2);
|
|
});
|
|
});
|
|
|
|
it('supports supplying a famliy option to the agent', () => {
|
|
const url = `${base}redirect/301`;
|
|
const families = [];
|
|
const family = Symbol('family');
|
|
function lookupSpy(hostname, options, callback) {
|
|
families.push(options.family);
|
|
return lookup(hostname, {}, callback);
|
|
}
|
|
|
|
const agent = http.Agent({lookup: lookupSpy, family});
|
|
return fetch(url, {agent}).then(() => {
|
|
expect(families).to.have.length(2);
|
|
expect(families[0]).to.equal(family);
|
|
expect(families[1]).to.equal(family);
|
|
});
|
|
});
|
|
|
|
it('should allow a function supplying the agent', () => {
|
|
const url = `${base}inspect`;
|
|
|
|
const agent = new http.Agent({
|
|
keepAlive: true
|
|
});
|
|
|
|
let parsedURL;
|
|
|
|
return fetch(url, {
|
|
agent(_parsedURL) {
|
|
parsedURL = _parsedURL;
|
|
return agent;
|
|
}
|
|
}).then(res => {
|
|
return res.json();
|
|
}).then(res => {
|
|
// The agent provider should have been called
|
|
expect(parsedURL.protocol).to.equal('http:');
|
|
// The agent we returned should have been used
|
|
expect(res.headers.connection).to.equal('keep-alive');
|
|
});
|
|
});
|
|
|
|
it('should calculate content length and extract content type for each body type', () => {
|
|
const url = `${base}hello`;
|
|
const bodyContent = 'a=1';
|
|
|
|
const streamBody = stream.Readable.from(bodyContent);
|
|
const streamRequest = new Request(url, {
|
|
method: 'POST',
|
|
body: streamBody,
|
|
size: 1024
|
|
});
|
|
|
|
const blobBody = new Blob([bodyContent], {type: 'text/plain'});
|
|
const blobRequest = new Request(url, {
|
|
method: 'POST',
|
|
body: blobBody,
|
|
size: 1024
|
|
});
|
|
|
|
const formBody = new FormData();
|
|
formBody.append('a', '1');
|
|
const formRequest = new Request(url, {
|
|
method: 'POST',
|
|
body: formBody,
|
|
size: 1024
|
|
});
|
|
|
|
const bufferBody = Buffer.from(bodyContent);
|
|
const bufferRequest = new Request(url, {
|
|
method: 'POST',
|
|
body: bufferBody,
|
|
size: 1024
|
|
});
|
|
|
|
const stringRequest = new Request(url, {
|
|
method: 'POST',
|
|
body: bodyContent,
|
|
size: 1024
|
|
});
|
|
|
|
const nullRequest = new Request(url, {
|
|
method: 'GET',
|
|
body: null,
|
|
size: 1024
|
|
});
|
|
|
|
expect(getTotalBytes(streamRequest)).to.be.null;
|
|
expect(getTotalBytes(blobRequest)).to.equal(blobBody.size);
|
|
expect(getTotalBytes(formRequest)).to.not.be.null;
|
|
expect(getTotalBytes(bufferRequest)).to.equal(bufferBody.length);
|
|
expect(getTotalBytes(stringRequest)).to.equal(bodyContent.length);
|
|
expect(getTotalBytes(nullRequest)).to.equal(0);
|
|
|
|
expect(extractContentType(streamBody)).to.be.null;
|
|
expect(extractContentType(blobBody)).to.equal('text/plain');
|
|
expect(extractContentType(formBody)).to.startWith('multipart/form-data');
|
|
expect(extractContentType(bufferBody)).to.be.null;
|
|
expect(extractContentType(bodyContent)).to.equal('text/plain;charset=UTF-8');
|
|
expect(extractContentType(null)).to.be.null;
|
|
});
|
|
|
|
it('should encode URLs as UTF-8', async () => {
|
|
const url = `${base}möbius`;
|
|
const res = await fetch(url);
|
|
expect(res.url).to.equal(`${base}m%C3%B6bius`);
|
|
});
|
|
});
|