2224 lines
65 KiB
JavaScript
2224 lines
65 KiB
JavaScript
// Test tools
|
||
import zlib from 'zlib';
|
||
import crypto from 'crypto';
|
||
import {spawn} from 'child_process';
|
||
import * as http from 'http';
|
||
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
import * as stream from 'stream';
|
||
import {lookup} from 'dns';
|
||
import vm from 'vm';
|
||
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 {AbortController} from 'abortcontroller-polyfill/dist/abortcontroller';
|
||
import AbortController2 from 'abort-controller';
|
||
|
||
// Test subjects
|
||
import Blob from 'fetch-blob';
|
||
import fetch, {
|
||
FetchError,
|
||
Headers,
|
||
Request,
|
||
Response
|
||
} from '../src';
|
||
import FetchErrorOrig from '../src/errors/fetch-error';
|
||
import HeadersOrig, {createHeadersLenient} from '../src/headers';
|
||
import RequestOrig from '../src/request';
|
||
import ResponseOrig from '../src/response';
|
||
import Body, {getTotalBytes, extractContentType} from '../src/body';
|
||
import TestServer from './utils/server';
|
||
|
||
const {
|
||
Uint8Array: VMUint8Array
|
||
} = vm.runInNewContext('this');
|
||
|
||
import chaiTimeout from './utils/chai-timeout';
|
||
|
||
chai.use(chaiPromised);
|
||
chai.use(chaiIterator);
|
||
chai.use(chaiString);
|
||
chai.use(chaiTimeout);
|
||
const {expect} = chai;
|
||
|
||
const local = new TestServer();
|
||
const base = `http://${local.hostname}:${local.port}/`;
|
||
|
||
before(done => {
|
||
local.start(done);
|
||
});
|
||
|
||
after(done => {
|
||
local.stop(done);
|
||
});
|
||
|
||
const itIf = value => value ? it : it.skip;
|
||
|
||
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', () => {
|
||
it('should return a promise', () => {
|
||
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', () => {
|
||
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', () => {
|
||
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', () => {
|
||
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, 'Only absolute URLs are supported');
|
||
});
|
||
|
||
it('should reject with error if url is relative path', () => {
|
||
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', () => {
|
||
const url = 'ftp://example.com/';
|
||
return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, 'Only HTTP(S) protocols are supported');
|
||
});
|
||
|
||
itIf(process.platform !== 'win32')('should reject with error on network failure', () => {
|
||
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');
|
||
});
|
||
|
||
itIf(process.platform !== 'win32')('system error is extracted from failed requests', () => {
|
||
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: resumer().queue('a=1').end()
|
||
};
|
||
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, 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 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', () => {
|
||
let headers = {
|
||
'Invalid-Header ': 'abc\r\n',
|
||
'Invalid-Header-Value': '\u0007k\r\n',
|
||
'Set-Cookie': ['\u0007k\r\n', '\u0007kk\r\n']
|
||
};
|
||
headers = createHeadersLenient(headers);
|
||
expect(headers).to.not.have.property('Invalid-Header ');
|
||
expect(headers).to.not.have.property('Invalid-Header-Value');
|
||
expect(headers).to.not.have.property('Set-Cookie');
|
||
});
|
||
|
||
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 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', 'ENOTFOUND');
|
||
});
|
||
|
||
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 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', () => {
|
||
function delay(value) {
|
||
return new Promise(resolve => {
|
||
setTimeout(() => {
|
||
resolve(value);
|
||
}, 20);
|
||
});
|
||
}
|
||
|
||
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', () => {
|
||
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');
|
||
});
|
||
});
|
||
|
||
it('should allow custom timeout', () => {
|
||
const url = `${base}timeout`;
|
||
const options = {
|
||
timeout: 20
|
||
};
|
||
return expect(fetch(url, options)).to.eventually.be.rejected
|
||
.and.be.an.instanceOf(FetchError)
|
||
.and.have.property('type', 'request-timeout');
|
||
});
|
||
|
||
it('should allow custom timeout on response body', () => {
|
||
const url = `${base}slow`;
|
||
const options = {
|
||
timeout: 20
|
||
};
|
||
return fetch(url, options).then(res => {
|
||
expect(res.ok).to.be.true;
|
||
return expect(res.text()).to.eventually.be.rejected
|
||
.and.be.an.instanceOf(FetchError)
|
||
.and.have.property('type', 'body-timeout');
|
||
});
|
||
});
|
||
|
||
it('should allow custom timeout on redirected requests', () => {
|
||
const url = `${base}redirect/slow-chain`;
|
||
const options = {
|
||
timeout: 20
|
||
};
|
||
return expect(fetch(url, options)).to.eventually.be.rejected
|
||
.and.be.an.instanceOf(FetchError)
|
||
.and.have.property('type', 'request-timeout');
|
||
});
|
||
|
||
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)
|
||
.and.include({
|
||
type: 'aborted',
|
||
name: 'AbortError'
|
||
})
|
||
));
|
||
});
|
||
|
||
it('should reject immediately if signal has already been aborted', () => {
|
||
const url = `${base}timeout`;
|
||
const controller = new AbortController();
|
||
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 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(); }, 20);
|
||
`;
|
||
spawn('node', ['-e', script])
|
||
.on('exit', () => {
|
||
done();
|
||
});
|
||
});
|
||
|
||
it('should remove internal AbortSignal event listener after request is aborted', () => {
|
||
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', () => {
|
||
const abortController = new AbortController();
|
||
const request = new Request(`${base}redirect/slow`, {
|
||
signal: abortController.signal
|
||
});
|
||
setTimeout(() => {
|
||
abortController.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 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.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 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 (error_) {
|
||
reject(error_);
|
||
}
|
||
});
|
||
}),
|
||
expect(promise).to.eventually.be.rejected
|
||
.and.be.an.instanceof(Error)
|
||
.and.have.property('name', 'AbortError')
|
||
]);
|
||
|
||
controller.abort();
|
||
|
||
return result;
|
||
});
|
||
|
||
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', () => {
|
||
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 url = `${base}inspect`;
|
||
const options = {
|
||
method: 'POST',
|
||
body: stringToArrayBuffer('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 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 url = `${base}inspect`;
|
||
const options = {
|
||
method: 'POST',
|
||
body: new Uint8Array(stringToArrayBuffer('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 url = `${base}inspect`;
|
||
const options = {
|
||
method: 'POST',
|
||
body: new DataView(stringToArrayBuffer('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) 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 url = `${base}inspect`;
|
||
const options = {
|
||
method: 'POST',
|
||
body: new Uint8Array(stringToArrayBuffer('Hello, world!\n'), 7, 6)
|
||
};
|
||
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', () => {
|
||
let body = resumer().queue('a=1').end();
|
||
body = body.pipe(new stream.PassThrough());
|
||
|
||
const url = `${base}inspect`;
|
||
const options = {
|
||
method: 'POST',
|
||
body
|
||
};
|
||
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');
|
||
});
|
||
});
|
||
|
||
itIf(process.platform !== 'win32')('should allow POST request with form-data using stream as body', () => {
|
||
const form = new FormData();
|
||
form.append('my_field', fs.createReadStream(path.join(__dirname, './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 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('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', () => {
|
||
let body = resumer().queue('a=1').end();
|
||
body = body.pipe(new stream.PassThrough());
|
||
const res = new Response(body);
|
||
const bufferConcat = Buffer.concat;
|
||
const restoreBufferConcat = () => {
|
||
Buffer.concat = bufferConcat;
|
||
};
|
||
|
||
Buffer.concat = () => {
|
||
throw new Error('embedded error');
|
||
};
|
||
|
||
const textPromise = res.text();
|
||
// Ensure that `Buffer.concat` is always restored:
|
||
textPromise.then(restoreBufferConcat, restoreBufferConcat);
|
||
|
||
return expect(textPromise).to.eventually.be.rejected
|
||
.and.be.an.instanceOf(FetchError)
|
||
.and.include({type: 'system'})
|
||
.and.have.property('message').that.includes('Could not create Buffer')
|
||
.and.that.includes('embedded error');
|
||
});
|
||
|
||
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';
|
||
|
||
let streamBody = resumer().queue(bodyContent).end();
|
||
streamBody = streamBody.pipe(new stream.PassThrough());
|
||
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', () => {
|
||
const url = `${base}möbius`;
|
||
|
||
fetch(url).then(res => expect(res.url).to.equal(`${base}m%C3%B6bius`));
|
||
});
|
||
|
||
describe('data uri', () => {
|
||
const dataUrl = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
|
||
|
||
const invalidDataUrl = 'data:@@@@';
|
||
|
||
it('should accept data uri', () => {
|
||
return fetch(dataUrl).then(r => {
|
||
console.assert(r.status === 200);
|
||
console.assert(r.headers.get('Content-Type') === 'image/gif');
|
||
|
||
return r.buffer().then(b => {
|
||
console.assert(b instanceof Buffer);
|
||
});
|
||
});
|
||
});
|
||
|
||
it('should reject invalid data uri', () => {
|
||
return fetch(invalidDataUrl).catch(error => {
|
||
console.assert(error);
|
||
console.assert(error.message.includes('invalid URL'));
|
||
});
|
||
});
|
||
});
|
||
});
|