UNPKG

5.32 kBJavaScriptView Raw
1
2/**
3 * body.js
4 *
5 * Body interface provides common methods for Request and Response
6 */
7
8var convert = require('encoding').convert;
9var bodyStream = require('is-stream');
10var PassThrough = require('stream').PassThrough;
11var FetchError = require('./fetch-error');
12
13module.exports = Body;
14
15/**
16 * Body class
17 *
18 * @param Stream body Readable stream
19 * @param Object opts Response options
20 * @return Void
21 */
22function Body(body, opts) {
23
24 opts = opts || {};
25
26 this.body = body;
27 this.bodyUsed = false;
28 this.size = opts.size || 0;
29 this.timeout = opts.timeout || 0;
30 this._raw = [];
31 this._abort = false;
32
33}
34
35/**
36 * Decode response as json
37 *
38 * @return Promise
39 */
40Body.prototype.json = function() {
41
42 return this._decode().then(function(buffer) {
43 return JSON.parse(buffer.toString());
44 });
45
46};
47
48/**
49 * Decode response as text
50 *
51 * @return Promise
52 */
53Body.prototype.text = function() {
54
55 return this._decode().then(function(buffer) {
56 return buffer.toString();
57 });
58
59};
60
61/**
62 * Decode response as buffer (non-spec api)
63 *
64 * @return Promise
65 */
66Body.prototype.buffer = function() {
67
68 return this._decode();
69
70};
71
72/**
73 * Decode buffers into utf-8 string
74 *
75 * @return Promise
76 */
77Body.prototype._decode = function() {
78
79 var self = this;
80
81 if (this.bodyUsed) {
82 return Body.Promise.reject(new Error('body used already for: ' + this.url));
83 }
84
85 this.bodyUsed = true;
86 this._bytes = 0;
87 this._abort = false;
88 this._raw = [];
89
90 return new Body.Promise(function(resolve, reject) {
91 var resTimeout;
92
93 // body is string
94 if (typeof self.body === 'string') {
95 self._bytes = self.body.length;
96 self._raw = [new Buffer(self.body)];
97 return resolve(self._convert());
98 }
99
100 // body is buffer
101 if (self.body instanceof Buffer) {
102 self._bytes = self.body.length;
103 self._raw = [self.body];
104 return resolve(self._convert());
105 }
106
107 // allow timeout on slow response body
108 if (self.timeout) {
109 resTimeout = setTimeout(function() {
110 self._abort = true;
111 reject(new FetchError('response timeout at ' + self.url + ' over limit: ' + self.timeout, 'body-timeout'));
112 }, self.timeout);
113 }
114
115 // handle stream error, such as incorrect content-encoding
116 self.body.on('error', function(err) {
117 reject(new FetchError('invalid response body at: ' + self.url + ' reason: ' + err.message, 'system', err));
118 });
119
120 // body is stream
121 self.body.on('data', function(chunk) {
122 if (self._abort || chunk === null) {
123 return;
124 }
125
126 if (self.size && self._bytes + chunk.length > self.size) {
127 self._abort = true;
128 reject(new FetchError('content size at ' + self.url + ' over limit: ' + self.size, 'max-size'));
129 return;
130 }
131
132 self._bytes += chunk.length;
133 self._raw.push(chunk);
134 });
135
136 self.body.on('end', function() {
137 if (self._abort) {
138 return;
139 }
140
141 clearTimeout(resTimeout);
142 resolve(self._convert());
143 });
144 });
145
146};
147
148/**
149 * Detect buffer encoding and convert to target encoding
150 * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
151 *
152 * @param String encoding Target encoding
153 * @return String
154 */
155Body.prototype._convert = function(encoding) {
156
157 encoding = encoding || 'utf-8';
158
159 var ct = this.headers.get('content-type');
160 var charset = 'utf-8';
161 var res, str;
162
163 // header
164 if (ct) {
165 // skip encoding detection altogether if not html/xml/plain text
166 if (!/text\/html|text\/plain|\+xml|\/xml/i.test(ct)) {
167 return Buffer.concat(this._raw);
168 }
169
170 res = /charset=([^;]*)/i.exec(ct);
171 }
172
173 // no charset in content type, peek at response body for at most 1024 bytes
174 if (!res && this._raw.length > 0) {
175 for (var i = 0; i < this._raw.length; i++) {
176 str += this._raw[i].toString()
177 if (str.length > 1024) {
178 break;
179 }
180 }
181 str = str.substr(0, 1024);
182 }
183
184 // html5
185 if (!res && str) {
186 res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str);
187 }
188
189 // html4
190 if (!res && str) {
191 res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
192
193 if (res) {
194 res = /charset=(.*)/i.exec(res.pop());
195 }
196 }
197
198 // xml
199 if (!res && str) {
200 res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str);
201 }
202
203 // found charset
204 if (res) {
205 charset = res.pop();
206
207 // prevent decode issues when sites use incorrect encoding
208 // ref: https://hsivonen.fi/encoding-menu/
209 if (charset === 'gb2312' || charset === 'gbk') {
210 charset = 'gb18030';
211 }
212 }
213
214 // turn raw buffers into a single utf-8 buffer
215 return convert(
216 Buffer.concat(this._raw)
217 , encoding
218 , charset
219 );
220
221};
222
223/**
224 * Clone body given Res/Req instance
225 *
226 * @param Mixed instance Response or Request instance
227 * @return Mixed
228 */
229Body.prototype._clone = function(instance) {
230 var p1, p2;
231 var body = instance.body;
232
233 // don't allow cloning a used body
234 if (instance.bodyUsed) {
235 throw new Error('cannot clone body after it is used');
236 }
237
238 // check that body is a stream and not form-data object
239 // note: we can't clone the form-data object without having it as a dependency
240 if (bodyStream(body) && typeof body.getBoundary !== 'function') {
241 // tee instance body
242 p1 = new PassThrough();
243 p2 = new PassThrough();
244 body.pipe(p1);
245 body.pipe(p2);
246 // set instance body to teed body and return the other teed body
247 instance.body = p1;
248 body = p2;
249 }
250
251 return body;
252}
253
254// expose Promise
255Body.Promise = global.Promise;