source: trip-planner-front/node_modules/minipass-fetch/lib/body.js@ b738035

Last change on this file since b738035 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 9.9 KB
Line 
1'use strict'
2const Minipass = require('minipass')
3const MinipassSized = require('minipass-sized')
4
5const Blob = require('./blob.js')
6const {BUFFER} = Blob
7const FetchError = require('./fetch-error.js')
8
9// optional dependency on 'encoding'
10let convert
11try {
12 convert = require('encoding').convert
13} catch (e) {}
14
15const INTERNALS = Symbol('Body internals')
16const CONSUME_BODY = Symbol('consumeBody')
17
18class Body {
19 constructor (bodyArg, options = {}) {
20 const { size = 0, timeout = 0 } = options
21 const body = bodyArg === undefined || bodyArg === null ? null
22 : isURLSearchParams(bodyArg) ? Buffer.from(bodyArg.toString())
23 : isBlob(bodyArg) ? bodyArg
24 : Buffer.isBuffer(bodyArg) ? bodyArg
25 : Object.prototype.toString.call(bodyArg) === '[object ArrayBuffer]'
26 ? Buffer.from(bodyArg)
27 : ArrayBuffer.isView(bodyArg)
28 ? Buffer.from(bodyArg.buffer, bodyArg.byteOffset, bodyArg.byteLength)
29 : Minipass.isStream(bodyArg) ? bodyArg
30 : Buffer.from(String(bodyArg))
31
32 this[INTERNALS] = {
33 body,
34 disturbed: false,
35 error: null,
36 }
37
38 this.size = size
39 this.timeout = timeout
40
41 if (Minipass.isStream(body)) {
42 body.on('error', er => {
43 const error = er.name === 'AbortError' ? er
44 : new FetchError(`Invalid response while trying to fetch ${
45 this.url}: ${er.message}`, 'system', er)
46 this[INTERNALS].error = error
47 })
48 }
49 }
50
51 get body () {
52 return this[INTERNALS].body
53 }
54
55 get bodyUsed () {
56 return this[INTERNALS].disturbed
57 }
58
59 arrayBuffer () {
60 return this[CONSUME_BODY]().then(buf =>
61 buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength))
62 }
63
64 blob () {
65 const ct = this.headers && this.headers.get('content-type') || ''
66 return this[CONSUME_BODY]().then(buf => Object.assign(
67 new Blob([], { type: ct.toLowerCase() }),
68 { [BUFFER]: buf }
69 ))
70 }
71
72 json () {
73 return this[CONSUME_BODY]().then(buf => {
74 try {
75 return JSON.parse(buf.toString())
76 } catch (er) {
77 return Promise.reject(new FetchError(
78 `invalid json response body at ${
79 this.url} reason: ${er.message}`, 'invalid-json'))
80 }
81 })
82 }
83
84 text () {
85 return this[CONSUME_BODY]().then(buf => buf.toString())
86 }
87
88 buffer () {
89 return this[CONSUME_BODY]()
90 }
91
92 textConverted () {
93 return this[CONSUME_BODY]().then(buf => convertBody(buf, this.headers))
94 }
95
96 [CONSUME_BODY] () {
97 if (this[INTERNALS].disturbed)
98 return Promise.reject(new TypeError(`body used already for: ${
99 this.url}`))
100
101 this[INTERNALS].disturbed = true
102
103 if (this[INTERNALS].error)
104 return Promise.reject(this[INTERNALS].error)
105
106 // body is null
107 if (this.body === null) {
108 return Promise.resolve(Buffer.alloc(0))
109 }
110
111 if (Buffer.isBuffer(this.body))
112 return Promise.resolve(this.body)
113
114 const upstream = isBlob(this.body) ? this.body.stream() : this.body
115
116 /* istanbul ignore if: should never happen */
117 if (!Minipass.isStream(upstream))
118 return Promise.resolve(Buffer.alloc(0))
119
120 const stream = this.size && upstream instanceof MinipassSized ? upstream
121 : !this.size && upstream instanceof Minipass &&
122 !(upstream instanceof MinipassSized) ? upstream
123 : this.size ? new MinipassSized({ size: this.size })
124 : new Minipass()
125
126 // allow timeout on slow response body
127 const resTimeout = this.timeout ? setTimeout(() => {
128 stream.emit('error', new FetchError(
129 `Response timeout while trying to fetch ${
130 this.url} (over ${this.timeout}ms)`, 'body-timeout'))
131 }, this.timeout) : null
132
133 // do not keep the process open just for this timeout, even
134 // though we expect it'll get cleared eventually.
135 if (resTimeout) {
136 resTimeout.unref()
137 }
138
139 // do the pipe in the promise, because the pipe() can send too much
140 // data through right away and upset the MP Sized object
141 return new Promise((resolve, reject) => {
142 // if the stream is some other kind of stream, then pipe through a MP
143 // so we can collect it more easily.
144 if (stream !== upstream) {
145 upstream.on('error', er => stream.emit('error', er))
146 upstream.pipe(stream)
147 }
148 resolve()
149 }).then(() => stream.concat()).then(buf => {
150 clearTimeout(resTimeout)
151 return buf
152 }).catch(er => {
153 clearTimeout(resTimeout)
154 // request was aborted, reject with this Error
155 if (er.name === 'AbortError' || er.name === 'FetchError')
156 throw er
157 else if (er.name === 'RangeError')
158 throw new FetchError(`Could not create Buffer from response body for ${
159 this.url}: ${er.message}`, 'system', er)
160 else
161 // other errors, such as incorrect content-encoding or content-length
162 throw new FetchError(`Invalid response body while trying to fetch ${
163 this.url}: ${er.message}`, 'system', er)
164 })
165 }
166
167 static clone (instance) {
168 if (instance.bodyUsed)
169 throw new Error('cannot clone body after it is used')
170
171 const body = instance.body
172
173 // check that body is a stream and not form-data object
174 // NB: can't clone the form-data object without having it as a dependency
175 if (Minipass.isStream(body) && typeof body.getBoundary !== 'function') {
176 // create a dedicated tee stream so that we don't lose data
177 // potentially sitting in the body stream's buffer by writing it
178 // immediately to p1 and not having it for p2.
179 const tee = new Minipass()
180 const p1 = new Minipass()
181 const p2 = new Minipass()
182 tee.on('error', er => {
183 p1.emit('error', er)
184 p2.emit('error', er)
185 })
186 body.on('error', er => tee.emit('error', er))
187 tee.pipe(p1)
188 tee.pipe(p2)
189 body.pipe(tee)
190 // set instance body to one fork, return the other
191 instance[INTERNALS].body = p1
192 return p2
193 } else
194 return instance.body
195 }
196
197 static extractContentType (body) {
198 return body === null || body === undefined ? null
199 : typeof body === 'string' ? 'text/plain;charset=UTF-8'
200 : isURLSearchParams(body)
201 ? 'application/x-www-form-urlencoded;charset=UTF-8'
202 : isBlob(body) ? body.type || null
203 : Buffer.isBuffer(body) ? null
204 : Object.prototype.toString.call(body) === '[object ArrayBuffer]' ? null
205 : ArrayBuffer.isView(body) ? null
206 : typeof body.getBoundary === 'function'
207 ? `multipart/form-data;boundary=${body.getBoundary()}`
208 : Minipass.isStream(body) ? null
209 : 'text/plain;charset=UTF-8'
210 }
211
212 static getTotalBytes (instance) {
213 const {body} = instance
214 return (body === null || body === undefined) ? 0
215 : isBlob(body) ? body.size
216 : Buffer.isBuffer(body) ? body.length
217 : body && typeof body.getLengthSync === 'function' && (
218 // detect form data input from form-data module
219 body._lengthRetrievers &&
220 /* istanbul ignore next */ body._lengthRetrievers.length == 0 || // 1.x
221 body.hasKnownLength && body.hasKnownLength()) // 2.x
222 ? body.getLengthSync()
223 : null
224 }
225
226 static writeToStream (dest, instance) {
227 const {body} = instance
228
229 if (body === null || body === undefined)
230 dest.end()
231 else if (Buffer.isBuffer(body) || typeof body === 'string')
232 dest.end(body)
233 else {
234 // body is stream or blob
235 const stream = isBlob(body) ? body.stream() : body
236 stream.on('error', er => dest.emit('error', er)).pipe(dest)
237 }
238
239 return dest
240 }
241}
242
243Object.defineProperties(Body.prototype, {
244 body: { enumerable: true },
245 bodyUsed: { enumerable: true },
246 arrayBuffer: { enumerable: true },
247 blob: { enumerable: true },
248 json: { enumerable: true },
249 text: { enumerable: true }
250})
251
252
253const isURLSearchParams = obj =>
254 // Duck-typing as a necessary condition.
255 (typeof obj !== 'object' ||
256 typeof obj.append !== 'function' ||
257 typeof obj.delete !== 'function' ||
258 typeof obj.get !== 'function' ||
259 typeof obj.getAll !== 'function' ||
260 typeof obj.has !== 'function' ||
261 typeof obj.set !== 'function') ? false
262 // Brand-checking and more duck-typing as optional condition.
263 : obj.constructor.name === 'URLSearchParams' ||
264 Object.prototype.toString.call(obj) === '[object URLSearchParams]' ||
265 typeof obj.sort === 'function'
266
267const isBlob = obj =>
268 typeof obj === 'object' &&
269 typeof obj.arrayBuffer === 'function' &&
270 typeof obj.type === 'string' &&
271 typeof obj.stream === 'function' &&
272 typeof obj.constructor === 'function' &&
273 typeof obj.constructor.name === 'string' &&
274 /^(Blob|File)$/.test(obj.constructor.name) &&
275 /^(Blob|File)$/.test(obj[Symbol.toStringTag])
276
277
278const convertBody = (buffer, headers) => {
279 /* istanbul ignore if */
280 if (typeof convert !== 'function')
281 throw new Error('The package `encoding` must be installed to use the textConverted() function')
282
283 const ct = headers && headers.get('content-type')
284 let charset = 'utf-8'
285 let res, str
286
287 // header
288 if (ct)
289 res = /charset=([^;]*)/i.exec(ct)
290
291 // no charset in content type, peek at response body for at most 1024 bytes
292 str = buffer.slice(0, 1024).toString()
293
294 // html5
295 if (!res && str)
296 res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str)
297
298 // html4
299 if (!res && str) {
300 res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str)
301
302 if (!res) {
303 res = /<meta[\s]+?content=(['"])(.+?)\1[\s]+?http-equiv=(['"])content-type\3/i.exec(str)
304 if (res)
305 res.pop() // drop last quote
306 }
307
308 if (res)
309 res = /charset=(.*)/i.exec(res.pop())
310 }
311
312 // xml
313 if (!res && str)
314 res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str)
315
316 // found charset
317 if (res) {
318 charset = res.pop()
319
320 // prevent decode issues when sites use incorrect encoding
321 // ref: https://hsivonen.fi/encoding-menu/
322 if (charset === 'gb2312' || charset === 'gbk')
323 charset = 'gb18030'
324 }
325
326 // turn raw buffers into a single utf-8 buffer
327 return convert(
328 buffer,
329 'UTF-8',
330 charset
331 ).toString()
332}
333
334module.exports = Body
Note: See TracBrowser for help on using the repository browser.