$
This commit is contained in:
350
node_modules/minipass-fetch/lib/body.js
generated
vendored
Normal file
350
node_modules/minipass-fetch/lib/body.js
generated
vendored
Normal file
@@ -0,0 +1,350 @@
|
||||
'use strict'
|
||||
const Minipass = require('minipass')
|
||||
const MinipassSized = require('minipass-sized')
|
||||
|
||||
const Blob = require('./blob.js')
|
||||
const { BUFFER } = Blob
|
||||
const FetchError = require('./fetch-error.js')
|
||||
|
||||
// optional dependency on 'encoding'
|
||||
let convert
|
||||
try {
|
||||
convert = require('encoding').convert
|
||||
} catch (e) {
|
||||
// defer error until textConverted is called
|
||||
}
|
||||
|
||||
const INTERNALS = Symbol('Body internals')
|
||||
const CONSUME_BODY = Symbol('consumeBody')
|
||||
|
||||
class Body {
|
||||
constructor (bodyArg, options = {}) {
|
||||
const { size = 0, timeout = 0 } = options
|
||||
const body = bodyArg === undefined || bodyArg === null ? null
|
||||
: isURLSearchParams(bodyArg) ? Buffer.from(bodyArg.toString())
|
||||
: isBlob(bodyArg) ? bodyArg
|
||||
: Buffer.isBuffer(bodyArg) ? bodyArg
|
||||
: Object.prototype.toString.call(bodyArg) === '[object ArrayBuffer]'
|
||||
? Buffer.from(bodyArg)
|
||||
: ArrayBuffer.isView(bodyArg)
|
||||
? Buffer.from(bodyArg.buffer, bodyArg.byteOffset, bodyArg.byteLength)
|
||||
: Minipass.isStream(bodyArg) ? bodyArg
|
||||
: Buffer.from(String(bodyArg))
|
||||
|
||||
this[INTERNALS] = {
|
||||
body,
|
||||
disturbed: false,
|
||||
error: null,
|
||||
}
|
||||
|
||||
this.size = size
|
||||
this.timeout = timeout
|
||||
|
||||
if (Minipass.isStream(body)) {
|
||||
body.on('error', er => {
|
||||
const error = er.name === 'AbortError' ? er
|
||||
: new FetchError(`Invalid response while trying to fetch ${
|
||||
this.url}: ${er.message}`, 'system', er)
|
||||
this[INTERNALS].error = error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get body () {
|
||||
return this[INTERNALS].body
|
||||
}
|
||||
|
||||
get bodyUsed () {
|
||||
return this[INTERNALS].disturbed
|
||||
}
|
||||
|
||||
arrayBuffer () {
|
||||
return this[CONSUME_BODY]().then(buf =>
|
||||
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength))
|
||||
}
|
||||
|
||||
blob () {
|
||||
const ct = this.headers && this.headers.get('content-type') || ''
|
||||
return this[CONSUME_BODY]().then(buf => Object.assign(
|
||||
new Blob([], { type: ct.toLowerCase() }),
|
||||
{ [BUFFER]: buf }
|
||||
))
|
||||
}
|
||||
|
||||
async json () {
|
||||
const buf = await this[CONSUME_BODY]()
|
||||
try {
|
||||
return JSON.parse(buf.toString())
|
||||
} catch (er) {
|
||||
throw new FetchError(
|
||||
`invalid json response body at ${this.url} reason: ${er.message}`,
|
||||
'invalid-json'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
text () {
|
||||
return this[CONSUME_BODY]().then(buf => buf.toString())
|
||||
}
|
||||
|
||||
buffer () {
|
||||
return this[CONSUME_BODY]()
|
||||
}
|
||||
|
||||
textConverted () {
|
||||
return this[CONSUME_BODY]().then(buf => convertBody(buf, this.headers))
|
||||
}
|
||||
|
||||
[CONSUME_BODY] () {
|
||||
if (this[INTERNALS].disturbed) {
|
||||
return Promise.reject(new TypeError(`body used already for: ${
|
||||
this.url}`))
|
||||
}
|
||||
|
||||
this[INTERNALS].disturbed = true
|
||||
|
||||
if (this[INTERNALS].error) {
|
||||
return Promise.reject(this[INTERNALS].error)
|
||||
}
|
||||
|
||||
// body is null
|
||||
if (this.body === null) {
|
||||
return Promise.resolve(Buffer.alloc(0))
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(this.body)) {
|
||||
return Promise.resolve(this.body)
|
||||
}
|
||||
|
||||
const upstream = isBlob(this.body) ? this.body.stream() : this.body
|
||||
|
||||
/* istanbul ignore if: should never happen */
|
||||
if (!Minipass.isStream(upstream)) {
|
||||
return Promise.resolve(Buffer.alloc(0))
|
||||
}
|
||||
|
||||
const stream = this.size && upstream instanceof MinipassSized ? upstream
|
||||
: !this.size && upstream instanceof Minipass &&
|
||||
!(upstream instanceof MinipassSized) ? upstream
|
||||
: this.size ? new MinipassSized({ size: this.size })
|
||||
: new Minipass()
|
||||
|
||||
// allow timeout on slow response body, but only if the stream is still writable. this
|
||||
// makes the timeout center on the socket stream from lib/index.js rather than the
|
||||
// intermediary minipass stream we create to receive the data
|
||||
const resTimeout = this.timeout && stream.writable ? setTimeout(() => {
|
||||
stream.emit('error', new FetchError(
|
||||
`Response timeout while trying to fetch ${
|
||||
this.url} (over ${this.timeout}ms)`, 'body-timeout'))
|
||||
}, this.timeout) : null
|
||||
|
||||
// do not keep the process open just for this timeout, even
|
||||
// though we expect it'll get cleared eventually.
|
||||
if (resTimeout && resTimeout.unref) {
|
||||
resTimeout.unref()
|
||||
}
|
||||
|
||||
// do the pipe in the promise, because the pipe() can send too much
|
||||
// data through right away and upset the MP Sized object
|
||||
return new Promise((resolve, reject) => {
|
||||
// if the stream is some other kind of stream, then pipe through a MP
|
||||
// so we can collect it more easily.
|
||||
if (stream !== upstream) {
|
||||
upstream.on('error', er => stream.emit('error', er))
|
||||
upstream.pipe(stream)
|
||||
}
|
||||
resolve()
|
||||
}).then(() => stream.concat()).then(buf => {
|
||||
clearTimeout(resTimeout)
|
||||
return buf
|
||||
}).catch(er => {
|
||||
clearTimeout(resTimeout)
|
||||
// request was aborted, reject with this Error
|
||||
if (er.name === 'AbortError' || er.name === 'FetchError') {
|
||||
throw er
|
||||
} else if (er.name === 'RangeError') {
|
||||
throw new FetchError(`Could not create Buffer from response body for ${
|
||||
this.url}: ${er.message}`, 'system', er)
|
||||
} else {
|
||||
// other errors, such as incorrect content-encoding or content-length
|
||||
throw new FetchError(`Invalid response body while trying to fetch ${
|
||||
this.url}: ${er.message}`, 'system', er)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static clone (instance) {
|
||||
if (instance.bodyUsed) {
|
||||
throw new Error('cannot clone body after it is used')
|
||||
}
|
||||
|
||||
const body = instance.body
|
||||
|
||||
// check that body is a stream and not form-data object
|
||||
// NB: can't clone the form-data object without having it as a dependency
|
||||
if (Minipass.isStream(body) && typeof body.getBoundary !== 'function') {
|
||||
// create a dedicated tee stream so that we don't lose data
|
||||
// potentially sitting in the body stream's buffer by writing it
|
||||
// immediately to p1 and not having it for p2.
|
||||
const tee = new Minipass()
|
||||
const p1 = new Minipass()
|
||||
const p2 = new Minipass()
|
||||
tee.on('error', er => {
|
||||
p1.emit('error', er)
|
||||
p2.emit('error', er)
|
||||
})
|
||||
body.on('error', er => tee.emit('error', er))
|
||||
tee.pipe(p1)
|
||||
tee.pipe(p2)
|
||||
body.pipe(tee)
|
||||
// set instance body to one fork, return the other
|
||||
instance[INTERNALS].body = p1
|
||||
return p2
|
||||
} else {
|
||||
return instance.body
|
||||
}
|
||||
}
|
||||
|
||||
static extractContentType (body) {
|
||||
return body === null || body === undefined ? null
|
||||
: typeof body === 'string' ? 'text/plain;charset=UTF-8'
|
||||
: isURLSearchParams(body)
|
||||
? 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
: isBlob(body) ? body.type || null
|
||||
: Buffer.isBuffer(body) ? null
|
||||
: Object.prototype.toString.call(body) === '[object ArrayBuffer]' ? null
|
||||
: ArrayBuffer.isView(body) ? null
|
||||
: typeof body.getBoundary === 'function'
|
||||
? `multipart/form-data;boundary=${body.getBoundary()}`
|
||||
: Minipass.isStream(body) ? null
|
||||
: 'text/plain;charset=UTF-8'
|
||||
}
|
||||
|
||||
static getTotalBytes (instance) {
|
||||
const { body } = instance
|
||||
return (body === null || body === undefined) ? 0
|
||||
: isBlob(body) ? body.size
|
||||
: Buffer.isBuffer(body) ? body.length
|
||||
: body && typeof body.getLengthSync === 'function' && (
|
||||
// detect form data input from form-data module
|
||||
body._lengthRetrievers &&
|
||||
/* istanbul ignore next */ body._lengthRetrievers.length === 0 || // 1.x
|
||||
body.hasKnownLength && body.hasKnownLength()) // 2.x
|
||||
? body.getLengthSync()
|
||||
: null
|
||||
}
|
||||
|
||||
static writeToStream (dest, instance) {
|
||||
const { body } = instance
|
||||
|
||||
if (body === null || body === undefined) {
|
||||
dest.end()
|
||||
} else if (Buffer.isBuffer(body) || typeof body === 'string') {
|
||||
dest.end(body)
|
||||
} else {
|
||||
// body is stream or blob
|
||||
const stream = isBlob(body) ? body.stream() : body
|
||||
stream.on('error', er => dest.emit('error', er)).pipe(dest)
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(Body.prototype, {
|
||||
body: { enumerable: true },
|
||||
bodyUsed: { enumerable: true },
|
||||
arrayBuffer: { enumerable: true },
|
||||
blob: { enumerable: true },
|
||||
json: { enumerable: true },
|
||||
text: { enumerable: true },
|
||||
})
|
||||
|
||||
const isURLSearchParams = obj =>
|
||||
// Duck-typing as a necessary condition.
|
||||
(typeof obj !== 'object' ||
|
||||
typeof obj.append !== 'function' ||
|
||||
typeof obj.delete !== 'function' ||
|
||||
typeof obj.get !== 'function' ||
|
||||
typeof obj.getAll !== 'function' ||
|
||||
typeof obj.has !== 'function' ||
|
||||
typeof obj.set !== 'function') ? false
|
||||
// Brand-checking and more duck-typing as optional condition.
|
||||
: obj.constructor.name === 'URLSearchParams' ||
|
||||
Object.prototype.toString.call(obj) === '[object URLSearchParams]' ||
|
||||
typeof obj.sort === 'function'
|
||||
|
||||
const isBlob = obj =>
|
||||
typeof obj === 'object' &&
|
||||
typeof obj.arrayBuffer === 'function' &&
|
||||
typeof obj.type === 'string' &&
|
||||
typeof obj.stream === 'function' &&
|
||||
typeof obj.constructor === 'function' &&
|
||||
typeof obj.constructor.name === 'string' &&
|
||||
/^(Blob|File)$/.test(obj.constructor.name) &&
|
||||
/^(Blob|File)$/.test(obj[Symbol.toStringTag])
|
||||
|
||||
const convertBody = (buffer, headers) => {
|
||||
/* istanbul ignore if */
|
||||
if (typeof convert !== 'function') {
|
||||
throw new Error('The package `encoding` must be installed to use the textConverted() function')
|
||||
}
|
||||
|
||||
const ct = headers && headers.get('content-type')
|
||||
let charset = 'utf-8'
|
||||
let res
|
||||
|
||||
// header
|
||||
if (ct) {
|
||||
res = /charset=([^;]*)/i.exec(ct)
|
||||
}
|
||||
|
||||
// no charset in content type, peek at response body for at most 1024 bytes
|
||||
const str = buffer.slice(0, 1024).toString()
|
||||
|
||||
// html5
|
||||
if (!res && str) {
|
||||
res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str)
|
||||
}
|
||||
|
||||
// html4
|
||||
if (!res && str) {
|
||||
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str)
|
||||
|
||||
if (!res) {
|
||||
res = /<meta[\s]+?content=(['"])(.+?)\1[\s]+?http-equiv=(['"])content-type\3/i.exec(str)
|
||||
if (res) {
|
||||
res.pop()
|
||||
} // drop last quote
|
||||
}
|
||||
|
||||
if (res) {
|
||||
res = /charset=(.*)/i.exec(res.pop())
|
||||
}
|
||||
}
|
||||
|
||||
// xml
|
||||
if (!res && str) {
|
||||
res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str)
|
||||
}
|
||||
|
||||
// found charset
|
||||
if (res) {
|
||||
charset = res.pop()
|
||||
|
||||
// prevent decode issues when sites use incorrect encoding
|
||||
// ref: https://hsivonen.fi/encoding-menu/
|
||||
if (charset === 'gb2312' || charset === 'gbk') {
|
||||
charset = 'gb18030'
|
||||
}
|
||||
}
|
||||
|
||||
// turn raw buffers into a single utf-8 buffer
|
||||
return convert(
|
||||
buffer,
|
||||
'UTF-8',
|
||||
charset
|
||||
).toString()
|
||||
}
|
||||
|
||||
module.exports = Body
|
Reference in New Issue
Block a user