2016-04-05 10:43:12 -07:00
|
|
|
|
2015-08-10 12:35:01 -07:00
|
|
|
/**
|
2016-04-05 10:43:12 -07:00
|
|
|
* body.js
|
2015-08-10 12:35:01 -07:00
|
|
|
*
|
2016-04-05 10:43:12 -07:00
|
|
|
* Body interface provides common methods for Request and Response
|
2015-08-10 12:35:01 -07:00
|
|
|
*/
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
import {convert} from 'encoding';
|
|
|
|
import bodyStream from 'is-stream';
|
2016-10-08 19:41:45 -07:00
|
|
|
import toArrayBuffer from 'buffer-to-arraybuffer';
|
2016-10-10 11:50:04 -07:00
|
|
|
import {PassThrough} from 'stream';
|
2016-10-15 14:21:33 -07:00
|
|
|
import Blob, {BUFFER} from './blob.js';
|
2016-10-10 11:50:04 -07:00
|
|
|
import FetchError from './fetch-error.js';
|
|
|
|
|
|
|
|
const DISTURBED = Symbol('disturbed');
|
2016-10-08 18:51:34 -07:00
|
|
|
const CONSUME_BODY = Symbol('consumeBody');
|
2015-08-10 12:35:01 -07:00
|
|
|
|
|
|
|
/**
|
2016-04-05 10:43:12 -07:00
|
|
|
* Body class
|
2015-08-10 12:35:01 -07:00
|
|
|
*
|
|
|
|
* @param Stream body Readable stream
|
|
|
|
* @param Object opts Response options
|
|
|
|
* @return Void
|
|
|
|
*/
|
2016-10-10 11:50:04 -07:00
|
|
|
export default class Body {
|
|
|
|
constructor(body, {
|
|
|
|
size = 0,
|
|
|
|
timeout = 0
|
|
|
|
} = {}) {
|
2016-12-05 20:25:13 -08:00
|
|
|
if (body == null) {
|
|
|
|
// body is undefined or null
|
|
|
|
body = null;
|
|
|
|
} else if (typeof body === 'string') {
|
|
|
|
// body is string
|
|
|
|
} else if (body instanceof Blob) {
|
|
|
|
// body is blob
|
|
|
|
} else if (Buffer.isBuffer(body)) {
|
|
|
|
// body is buffer
|
|
|
|
} else if (bodyStream(body)) {
|
|
|
|
// body is stream
|
|
|
|
} else {
|
|
|
|
// none of the above
|
|
|
|
// coerce to string
|
|
|
|
body = String(body);
|
2016-10-15 14:21:33 -07:00
|
|
|
}
|
2016-10-10 11:50:04 -07:00
|
|
|
this.body = body;
|
|
|
|
this[DISTURBED] = false;
|
|
|
|
this.size = size;
|
|
|
|
this.timeout = timeout;
|
2016-09-24 02:12:18 -07:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
get bodyUsed() {
|
|
|
|
return this[DISTURBED];
|
|
|
|
}
|
2015-08-10 12:35:01 -07:00
|
|
|
|
2016-10-08 19:41:45 -07:00
|
|
|
/**
|
|
|
|
* Decode response as ArrayBuffer
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
arrayBuffer() {
|
|
|
|
return this[CONSUME_BODY]().then(buf => toArrayBuffer(buf));
|
|
|
|
}
|
|
|
|
|
2016-10-15 14:21:33 -07:00
|
|
|
/**
|
|
|
|
* Return raw response as Blob
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
blob() {
|
|
|
|
let ct = this.headers && this.headers.get('content-type') || '';
|
|
|
|
return this[CONSUME_BODY]().then(buf => Object.assign(
|
|
|
|
// Prevent copying
|
|
|
|
new Blob([], {
|
|
|
|
type: ct.toLowerCase()
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
[BUFFER]: buf
|
|
|
|
}
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
/**
|
|
|
|
* Decode response as json
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
json() {
|
2016-10-08 18:51:34 -07:00
|
|
|
return this[CONSUME_BODY]().then(buffer => JSON.parse(buffer.toString()));
|
2016-03-19 00:41:25 -07:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
/**
|
|
|
|
* Decode response as text
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
text() {
|
2016-10-08 18:51:34 -07:00
|
|
|
return this[CONSUME_BODY]().then(buffer => buffer.toString());
|
2016-10-10 11:50:04 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
/**
|
|
|
|
* Decode response as buffer (non-spec api)
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
buffer() {
|
2016-10-08 18:51:34 -07:00
|
|
|
return this[CONSUME_BODY]();
|
2016-10-10 11:50:04 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-12 21:47:36 -07:00
|
|
|
/**
|
|
|
|
* Decode response as text, while automatically detecting the encoding and
|
|
|
|
* trying to decode to UTF-8 (non-spec api)
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
|
|
|
textConverted() {
|
|
|
|
return this[CONSUME_BODY]().then(buffer => convertBody(buffer, this.headers));
|
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
/**
|
|
|
|
* Decode buffers into utf-8 string
|
|
|
|
*
|
|
|
|
* @return Promise
|
|
|
|
*/
|
2016-10-08 18:51:34 -07:00
|
|
|
[CONSUME_BODY]() {
|
2016-10-10 11:50:04 -07:00
|
|
|
if (this[DISTURBED]) {
|
|
|
|
return Body.Promise.reject(new Error(`body used already for: ${this.url}`));
|
2016-03-19 00:41:25 -07:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
this[DISTURBED] = true;
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 19:40:56 -07:00
|
|
|
// body is null
|
2016-12-05 20:25:13 -08:00
|
|
|
if (this.body === null) {
|
2016-10-08 19:40:56 -07:00
|
|
|
return Body.Promise.resolve(new Buffer(0));
|
|
|
|
}
|
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// body is string
|
|
|
|
if (typeof this.body === 'string') {
|
2016-10-12 21:47:36 -07:00
|
|
|
return Body.Promise.resolve(new Buffer(this.body));
|
2016-10-08 18:51:34 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-12-05 20:25:13 -08:00
|
|
|
// body is blob
|
|
|
|
if (this.body instanceof Blob) {
|
|
|
|
return Body.Promise.resolve(this.body[BUFFER]);
|
|
|
|
}
|
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// body is buffer
|
|
|
|
if (Buffer.isBuffer(this.body)) {
|
2016-10-12 21:47:36 -07:00
|
|
|
return Body.Promise.resolve(this.body);
|
2016-10-08 18:51:34 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-12-05 21:09:54 -08:00
|
|
|
// istanbul ignore if: should never happen
|
2016-12-05 20:25:13 -08:00
|
|
|
if (!bodyStream(this.body)) {
|
|
|
|
return Body.Promise.resolve(new Buffer(0));
|
|
|
|
}
|
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// body is stream
|
|
|
|
// get ready to actually consume the body
|
|
|
|
let accum = [];
|
|
|
|
let accumBytes = 0;
|
|
|
|
let abort = false;
|
|
|
|
|
|
|
|
return new Body.Promise((resolve, reject) => {
|
|
|
|
let resTimeout;
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
// allow timeout on slow response body
|
|
|
|
if (this.timeout) {
|
|
|
|
resTimeout = setTimeout(() => {
|
2016-10-08 18:51:34 -07:00
|
|
|
abort = true;
|
|
|
|
reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout'));
|
2016-10-10 11:50:04 -07:00
|
|
|
}, this.timeout);
|
2016-03-19 00:41:25 -07:00
|
|
|
}
|
|
|
|
|
2016-10-10 11:50:04 -07:00
|
|
|
// handle stream error, such as incorrect content-encoding
|
|
|
|
this.body.on('error', err => {
|
2016-10-08 18:51:34 -07:00
|
|
|
reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err));
|
2016-10-10 11:50:04 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
this.body.on('data', chunk => {
|
2016-10-08 18:51:34 -07:00
|
|
|
if (abort || chunk === null) {
|
2016-10-10 11:50:04 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
if (this.size && accumBytes + chunk.length > this.size) {
|
|
|
|
abort = true;
|
2016-10-10 11:50:04 -07:00
|
|
|
reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
accumBytes += chunk.length;
|
|
|
|
accum.push(chunk);
|
2016-10-10 11:50:04 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
this.body.on('end', () => {
|
2016-10-08 18:51:34 -07:00
|
|
|
if (abort) {
|
2016-10-10 11:50:04 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
clearTimeout(resTimeout);
|
2016-10-12 21:47:36 -07:00
|
|
|
resolve(Buffer.concat(accum));
|
2016-10-10 11:50:04 -07:00
|
|
|
});
|
2016-03-19 00:41:25 -07:00
|
|
|
});
|
2016-10-10 11:50:04 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
/**
|
|
|
|
* Detect buffer encoding and convert to target encoding
|
|
|
|
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
|
|
|
|
*
|
2016-10-12 21:47:36 -07:00
|
|
|
* @param Buffer buffer Incoming buffer
|
2016-10-08 18:51:34 -07:00
|
|
|
* @param String encoding Target encoding
|
|
|
|
* @return String
|
|
|
|
*/
|
2016-10-12 21:47:36 -07:00
|
|
|
function convertBody(buffer, headers) {
|
2016-10-08 18:51:34 -07:00
|
|
|
const ct = headers.get('content-type');
|
|
|
|
let charset = 'utf-8';
|
|
|
|
let res, str;
|
|
|
|
|
|
|
|
// header
|
|
|
|
if (ct) {
|
|
|
|
res = /charset=([^;]*)/i.exec(ct);
|
|
|
|
}
|
|
|
|
|
|
|
|
// no charset in content type, peek at response body for at most 1024 bytes
|
2016-10-12 21:47:36 -07:00
|
|
|
str = buffer.slice(0, 1024).toString();
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// html5
|
|
|
|
if (!res && str) {
|
|
|
|
res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str);
|
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// html4
|
|
|
|
if (!res && str) {
|
|
|
|
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
if (res) {
|
|
|
|
res = /charset=(.*)/i.exec(res.pop());
|
2016-03-19 00:41:25 -07:00
|
|
|
}
|
2016-10-08 18:51:34 -07:00
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// xml
|
|
|
|
if (!res && str) {
|
|
|
|
res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str);
|
|
|
|
}
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// found charset
|
|
|
|
if (res) {
|
|
|
|
charset = res.pop();
|
2016-03-19 00:41:25 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// prevent decode issues when sites use incorrect encoding
|
|
|
|
// ref: https://hsivonen.fi/encoding-menu/
|
|
|
|
if (charset === 'gb2312' || charset === 'gbk') {
|
|
|
|
charset = 'gb18030';
|
2016-03-19 00:41:25 -07:00
|
|
|
}
|
2016-10-10 11:50:04 -07:00
|
|
|
}
|
2015-08-10 12:35:01 -07:00
|
|
|
|
2016-10-08 18:51:34 -07:00
|
|
|
// turn raw buffers into a single utf-8 buffer
|
|
|
|
return convert(
|
2016-10-12 21:47:36 -07:00
|
|
|
buffer
|
|
|
|
, 'UTF-8'
|
2016-10-08 18:51:34 -07:00
|
|
|
, charset
|
2016-10-12 21:47:36 -07:00
|
|
|
).toString();
|
2016-10-10 11:50:04 -07:00
|
|
|
}
|
2015-08-10 12:35:01 -07:00
|
|
|
|
2016-03-19 03:06:33 -07:00
|
|
|
/**
|
|
|
|
* Clone body given Res/Req instance
|
|
|
|
*
|
|
|
|
* @param Mixed instance Response or Request instance
|
|
|
|
* @return Mixed
|
|
|
|
*/
|
2016-10-10 11:50:04 -07:00
|
|
|
export function clone(instance) {
|
|
|
|
let p1, p2;
|
|
|
|
let body = instance.body;
|
2016-03-19 03:06:33 -07:00
|
|
|
|
2016-03-23 00:02:04 -07:00
|
|
|
// don't allow cloning a used body
|
2016-03-19 03:06:33 -07:00
|
|
|
if (instance.bodyUsed) {
|
|
|
|
throw new Error('cannot clone body after it is used');
|
|
|
|
}
|
|
|
|
|
2016-03-23 00:02:04 -07:00
|
|
|
// check that body is a stream and not form-data object
|
|
|
|
// note: we can't clone the form-data object without having it as a dependency
|
|
|
|
if (bodyStream(body) && typeof body.getBoundary !== 'function') {
|
2016-05-25 10:01:56 -07:00
|
|
|
// tee instance body
|
|
|
|
p1 = new PassThrough();
|
|
|
|
p2 = new PassThrough();
|
|
|
|
body.pipe(p1);
|
|
|
|
body.pipe(p2);
|
|
|
|
// set instance body to teed body and return the other teed body
|
|
|
|
instance.body = p1;
|
|
|
|
body = p2;
|
2016-03-19 03:06:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return body;
|
|
|
|
}
|
|
|
|
|
2016-11-23 12:42:24 -08:00
|
|
|
/**
|
|
|
|
* Performs the operation "extract a `Content-Type` value from |object|" as
|
|
|
|
* specified in the specification:
|
|
|
|
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
|
|
|
|
*
|
|
|
|
* This function assumes that instance.body is present and non-null.
|
|
|
|
*
|
|
|
|
* @param Mixed instance Response or Request instance
|
|
|
|
*/
|
|
|
|
export function extractContentType(instance) {
|
2016-12-05 20:25:13 -08:00
|
|
|
const {body} = instance;
|
2016-11-23 12:42:24 -08:00
|
|
|
|
2016-12-05 21:09:54 -08:00
|
|
|
// istanbul ignore if: Currently, because of a guard in Request, body
|
|
|
|
// can never be null. Included here for completeness.
|
2016-12-05 20:25:13 -08:00
|
|
|
if (body === null) {
|
|
|
|
// body is null
|
|
|
|
return null;
|
|
|
|
} else if (typeof body === 'string') {
|
|
|
|
// body is string
|
2016-11-23 12:42:24 -08:00
|
|
|
return 'text/plain;charset=UTF-8';
|
2016-12-05 20:25:13 -08:00
|
|
|
} else if (body instanceof Blob) {
|
|
|
|
// body is blob
|
|
|
|
return body.type || null;
|
|
|
|
} else if (Buffer.isBuffer(body)) {
|
|
|
|
// body is buffer
|
|
|
|
return null;
|
|
|
|
} else if (typeof body.getBoundary === 'function') {
|
|
|
|
// detect form data input from form-data module
|
|
|
|
return `multipart/form-data;boundary=${body.getBoundary()}`;
|
|
|
|
} else {
|
|
|
|
// body is stream
|
|
|
|
// can't really do much about this
|
|
|
|
return null;
|
2016-11-23 12:42:24 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-23 13:39:35 -08:00
|
|
|
export function getTotalBytes(instance) {
|
|
|
|
const {body} = instance;
|
|
|
|
|
2016-12-05 20:25:13 -08:00
|
|
|
if (body === null) {
|
|
|
|
// body is null
|
|
|
|
return 0;
|
|
|
|
} else if (typeof body === 'string') {
|
|
|
|
// body is string
|
2016-11-23 13:39:35 -08:00
|
|
|
return Buffer.byteLength(body);
|
2016-12-05 20:25:13 -08:00
|
|
|
} else if (body instanceof Blob) {
|
|
|
|
// body is blob
|
|
|
|
return body.size;
|
2016-12-05 20:30:00 -08:00
|
|
|
} else if (Buffer.isBuffer(body)) {
|
|
|
|
// body is buffer
|
|
|
|
return body.length;
|
2016-11-23 13:39:35 -08:00
|
|
|
} else if (body && typeof body.getLengthSync === 'function') {
|
|
|
|
// detect form data input from form-data module
|
|
|
|
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x
|
|
|
|
body.hasKnownLength && body.hasKnownLength()) { // 2.x
|
|
|
|
return body.getLengthSync();
|
|
|
|
}
|
2016-12-05 20:25:13 -08:00
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
// body is stream
|
|
|
|
// can't really do much about this
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function writeToStream(dest, instance) {
|
|
|
|
const {body} = instance;
|
|
|
|
|
|
|
|
if (body === null) {
|
|
|
|
// body is null
|
|
|
|
dest.end();
|
|
|
|
} else if (typeof body === 'string') {
|
|
|
|
// body is string
|
|
|
|
dest.write(body);
|
|
|
|
dest.end();
|
|
|
|
} else if (body instanceof Blob) {
|
|
|
|
// body is blob
|
|
|
|
dest.write(body[BUFFER]);
|
|
|
|
dest.end();
|
|
|
|
} else if (Buffer.isBuffer(body)) {
|
|
|
|
// body is buffer
|
|
|
|
dest.write(body);
|
|
|
|
dest.end()
|
2016-12-05 21:09:54 -08:00
|
|
|
} else {
|
2016-12-05 20:25:13 -08:00
|
|
|
// body is stream
|
|
|
|
body.pipe(dest);
|
2016-11-23 13:39:35 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-10 12:35:01 -07:00
|
|
|
// expose Promise
|
2016-03-19 00:41:25 -07:00
|
|
|
Body.Promise = global.Promise;
|