'use strict' /** * body.js * * Body interface provides common methods for Request and Response */ const Buffer = require('safe-buffer').Buffer const Blob = require('./blob.js') const BUFFER = Blob.BUFFER const convert = require('encoding').convert const parseJson = require('json-parse-better-errors') const FetchError = require('./fetch-error.js') const Stream = require('stream') const PassThrough = Stream.PassThrough const DISTURBED = Symbol('disturbed') /** * 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 */ exports = module.exports = Body function Body (body, opts) { if (!opts) opts = {} const size = opts.size == null ? 0 : opts.size const timeout = opts.timeout == null ? 0 : opts.timeout 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 (body instanceof Stream) { // 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 * * @return Promise */ arrayBuffer () { return consumeBody.call(this).then(buf => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)) }, /** * Return raw response as Blob * * @return Promise */ blob () { let ct = (this.headers && this.headers.get('content-type')) || '' return consumeBody.call(this).then(buf => Object.assign( // Prevent copying new Blob([], { type: ct.toLowerCase() }), { [BUFFER]: buf } )) }, /** * Decode response as json * * @return Promise */ json () { return consumeBody.call(this).then(buffer => parseJson(buffer.toString())) }, /** * Decode response as text * * @return Promise */ text () { return consumeBody.call(this).then(buffer => buffer.toString()) }, /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer () { return consumeBody.call(this) }, /** * Decode response as text, while automatically detecting the encoding and * trying to decode to UTF-8 (non-spec api) * * @return Promise */ textConverted () { return consumeBody.call(this).then(buffer => convertBody(buffer, this.headers)) } } Body.mixIn = function (proto) { for (const name of Object.getOwnPropertyNames(Body.prototype)) { // istanbul ignore else: future proof if (!(name in proto)) { const desc = Object.getOwnPropertyDescriptor(Body.prototype, name) Object.defineProperty(proto, name, desc) } } } /** * 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}`)) } this[DISTURBED] = true // body is null if (this.body === null) { return Body.Promise.resolve(Buffer.alloc(0)) } // body is string if (typeof this.body === 'string') { return Body.Promise.resolve(Buffer.from(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 (!(this.body instanceof Stream)) { return Body.Promise.resolve(Buffer.alloc(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) } // 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)) }) 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)) }) }) } /** * Detect buffer encoding and convert to target encoding * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding * * @param Buffer buffer Incoming buffer * @param String encoding Target encoding * @return String */ function convertBody (buffer, headers) { 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 str = buffer.slice(0, 1024).toString() // html5 if (!res && str) { res = /