Do not inherit from body

Per spec, make Body a proper mixin
This commit is contained in:
Timothy Gu 2017-02-26 14:29:40 -08:00
parent 56f786f896
commit f08b120771
3 changed files with 135 additions and 119 deletions

View File

@ -13,45 +13,46 @@ import Blob, {BUFFER} from './blob.js';
import FetchError from './fetch-error.js';
const DISTURBED = Symbol('disturbed');
const CONSUME_BODY = Symbol('consumeBody');
/**
* Body class
*
* Cannot use ES6 class because Body must be called with .call().
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Body {
constructor(body, {
size = 0,
timeout = 0
} = {}) {
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);
}
this.body = body;
this[DISTURBED] = false;
this.size = size;
this.timeout = timeout;
export default function Body(body, {
size = 0,
timeout = 0
} = {}) {
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);
}
this.body = body;
this[DISTURBED] = false;
this.size = size;
this.timeout = timeout;
}
Body.prototype = {
get bodyUsed() {
return this[DISTURBED];
}
},
/**
* Decode response as ArrayBuffer
@ -59,8 +60,8 @@ export default class Body {
* @return Promise
*/
arrayBuffer() {
return this[CONSUME_BODY]().then(buf => toArrayBuffer(buf));
}
return consumeBody.call(this).then(buf => toArrayBuffer(buf));
},
/**
* Return raw response as Blob
@ -69,7 +70,7 @@ export default class Body {
*/
blob() {
let ct = this.headers && this.headers.get('content-type') || '';
return this[CONSUME_BODY]().then(buf => Object.assign(
return consumeBody.call(this).then(buf => Object.assign(
// Prevent copying
new Blob([], {
type: ct.toLowerCase()
@ -78,7 +79,7 @@ export default class Body {
[BUFFER]: buf
}
));
}
},
/**
* Decode response as json
@ -86,8 +87,8 @@ export default class Body {
* @return Promise
*/
json() {
return this[CONSUME_BODY]().then(buffer => JSON.parse(buffer.toString()));
}
return consumeBody.call(this).then(buffer => JSON.parse(buffer.toString()));
},
/**
* Decode response as text
@ -95,8 +96,8 @@ export default class Body {
* @return Promise
*/
text() {
return this[CONSUME_BODY]().then(buffer => buffer.toString());
}
return consumeBody.call(this).then(buffer => buffer.toString());
},
/**
* Decode response as buffer (non-spec api)
@ -104,8 +105,8 @@ export default class Body {
* @return Promise
*/
buffer() {
return this[CONSUME_BODY]();
}
return consumeBody.call(this);
},
/**
* Decode response as text, while automatically detecting the encoding and
@ -114,94 +115,95 @@ export default class Body {
* @return Promise
*/
textConverted() {
return this[CONSUME_BODY]().then(buffer => convertBody(buffer, this.headers));
return consumeBody.call(this).then(buffer => convertBody(buffer, this.headers));
},
};
/**
* Decode buffers into utf-8 string
*
* @return Promise
*/
function consumeBody(body) {
if (this[DISTURBED]) {
return Body.Promise.reject(new Error(`body used already for: ${this.url}`));
}
/**
* Decode buffers into utf-8 string
*
* @return Promise
*/
[CONSUME_BODY]() {
if (this[DISTURBED]) {
return Body.Promise.reject(new Error(`body used already for: ${this.url}`));
this[DISTURBED] = true;
// body is null
if (this.body === null) {
return Body.Promise.resolve(new Buffer(0));
}
// body is string
if (typeof this.body === 'string') {
return Body.Promise.resolve(new Buffer(this.body));
}
// body is blob
if (this.body instanceof Blob) {
return Body.Promise.resolve(this.body[BUFFER]);
}
// body is buffer
if (Buffer.isBuffer(this.body)) {
return Body.Promise.resolve(this.body);
}
// istanbul ignore if: should never happen
if (!bodyStream(this.body)) {
return Body.Promise.resolve(new Buffer(0));
}
// 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;
// allow timeout on slow response body
if (this.timeout) {
resTimeout = setTimeout(() => {
abort = true;
reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout'));
}, this.timeout);
}
this[DISTURBED] = true;
// handle stream error, such as incorrect content-encoding
this.body.on('error', err => {
reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err));
});
// body is null
if (this.body === null) {
return Body.Promise.resolve(new Buffer(0));
}
// body is string
if (typeof this.body === 'string') {
return Body.Promise.resolve(new Buffer(this.body));
}
// body is blob
if (this.body instanceof Blob) {
return Body.Promise.resolve(this.body[BUFFER]);
}
// body is buffer
if (Buffer.isBuffer(this.body)) {
return Body.Promise.resolve(this.body);
}
// istanbul ignore if: should never happen
if (!bodyStream(this.body)) {
return Body.Promise.resolve(new Buffer(0));
}
// 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;
// allow timeout on slow response body
if (this.timeout) {
resTimeout = setTimeout(() => {
abort = true;
reject(new FetchError(`Response timeout while trying to fetch ${this.url} (over ${this.timeout}ms)`, 'body-timeout'));
}, this.timeout);
this.body.on('data', chunk => {
if (abort || chunk === null) {
return;
}
// handle stream error, such as incorrect content-encoding
this.body.on('error', err => {
reject(new FetchError(`Invalid response body while trying to fetch ${this.url}: ${err.message}`, 'system', err));
});
if (this.size && accumBytes + chunk.length > this.size) {
abort = true;
reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size'));
return;
}
this.body.on('data', chunk => {
if (abort || chunk === null) {
return;
}
if (this.size && accumBytes + chunk.length > this.size) {
abort = true;
reject(new FetchError(`content size at ${this.url} over limit: ${this.size}`, 'max-size'));
return;
}
accumBytes += chunk.length;
accum.push(chunk);
});
this.body.on('end', () => {
if (abort) {
return;
}
clearTimeout(resTimeout);
resolve(Buffer.concat(accum));
});
accumBytes += chunk.length;
accum.push(chunk);
});
}
this.body.on('end', () => {
if (abort) {
return;
}
clearTimeout(resTimeout);
resolve(Buffer.concat(accum));
});
});
}
/**

View File

@ -18,7 +18,7 @@ const PARSED_URL = Symbol('url');
* @param Object init Custom options
* @return Void
*/
export default class Request extends Body {
export default class Request {
constructor(input, init = {}) {
let parsedURL;
@ -51,7 +51,7 @@ export default class Request extends Body {
clone(input) :
null;
super(inputBody, {
Body.call(this, inputBody, {
timeout: init.timeout || input.timeout || 0,
size: init.size || input.size || 0
});
@ -101,6 +101,13 @@ export default class Request extends Body {
}
}
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
if (!(name in Request.prototype)) {
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name);
Object.defineProperty(Request.prototype, name, desc);
}
}
Object.defineProperty(Request.prototype, Symbol.toStringTag, {
value: 'RequestPrototype',
writable: false,

View File

@ -16,9 +16,9 @@ import Body, { clone } from './body';
* @param Object opts Response options
* @return Void
*/
export default class Response extends Body {
export default class Response {
constructor(body = null, opts = {}) {
super(body, opts);
Body.call(this, body, opts);
this.url = opts.url;
this.status = opts.status || 200;
@ -58,6 +58,13 @@ export default class Response extends Body {
}
}
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
if (!(name in Response.prototype)) {
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name);
Object.defineProperty(Response.prototype, name, desc);
}
}
Object.defineProperty(Response.prototype, Symbol.toStringTag, {
value: 'ResponsePrototype',
writable: false,