UNPKG

3.47 kBJavaScriptView Raw
1
2/**
3 * response.js
4 *
5 * Response class provides content decoding
6 */
7
8var http = require('http');
9var convert = require('encoding').convert;
10
11module.exports = Response;
12
13/**
14 * Response class
15 *
16 * @param Stream body Readable stream
17 * @param Object opts Response options
18 * @return Void
19 */
20function Response(body, opts) {
21
22 this.url = opts.url;
23 this.status = opts.status;
24 this.statusText = http.STATUS_CODES[this.status];
25 this.headers = opts.headers;
26 this.body = body;
27 this.bodyUsed = false;
28 this.size = opts.size;
29 this.ok = this.status >= 200 && this.status < 300;
30 this.timeout = opts.timeout;
31
32}
33
34/**
35 * Decode response as json
36 *
37 * @return Promise
38 */
39Response.prototype.json = function() {
40
41 return this._decode().then(function(text) {
42 return JSON.parse(text);
43 });
44
45}
46
47/**
48 * Decode response as text
49 *
50 * @return Promise
51 */
52Response.prototype.text = function() {
53
54 return this._decode();
55
56}
57
58/**
59 * Decode buffers into utf-8 string
60 *
61 * @return Promise
62 */
63Response.prototype._decode = function() {
64
65 var self = this;
66
67 if (this.bodyUsed) {
68 return Response.Promise.reject(new Error('body used already for: ' + this.url));
69 }
70
71 this.bodyUsed = true;
72 this._bytes = 0;
73 this._abort = false;
74 this._raw = [];
75
76 return new Response.Promise(function(resolve, reject) {
77 var resTimeout;
78
79 // allow timeout on slow response body
80 if (self.timeout) {
81 resTimeout = setTimeout(function() {
82 self._abort = true;
83 reject(new Error('response timeout at ' + self.url + ' over limit: ' + self.timeout));
84 }, self.timeout);
85 }
86
87 self.body.on('data', function(chunk) {
88 if (self._abort || chunk === null) {
89 return;
90 }
91
92 if (self.size && self._bytes + chunk.length > self.size) {
93 self._abort = true;
94 reject(new Error('content size at ' + self.url + ' over limit: ' + self.size));
95 return;
96 }
97
98 self._bytes += chunk.length;
99 self._raw.push(chunk);
100 });
101
102 self.body.on('end', function() {
103 if (self._abort) {
104 return;
105 }
106
107 clearTimeout(resTimeout);
108 resolve(self._convert());
109 });
110 });
111
112};
113
114/**
115 * Detect buffer encoding and convert to target encoding
116 * ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
117 *
118 * @param String encoding Target encoding
119 * @return String
120 */
121Response.prototype._convert = function(encoding) {
122
123 encoding = encoding || 'utf-8';
124
125 var charset = 'utf-8';
126 var res, str;
127
128 // header
129 if (this.headers.has('content-type')) {
130 res = /charset=([^;]*)/i.exec(this.headers.get('content-type'));
131 }
132
133 // no charset in content type, peek at response body
134 if (!res && this._raw.length > 0) {
135 str = this._raw[0].toString().substr(0, 1024);
136 }
137
138 // html5
139 if (!res && str) {
140 res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str);
141 }
142
143 // html4
144 if (!res && str) {
145 res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
146
147 if (res) {
148 res = /charset=(.*)/i.exec(res.pop());
149 }
150 }
151
152 // xml
153 if (!res && str) {
154 res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str);
155 }
156
157 // found charset
158 if (res) {
159 charset = res.pop();
160
161 // prevent decode issues when sites use incorrect encoding
162 // ref: https://hsivonen.fi/encoding-menu/
163 if (charset === 'gb2312' || charset === 'gbk') {
164 charset = 'gb18030';
165 }
166 }
167
168 // turn raw buffers into utf-8 string
169 return convert(
170 Buffer.concat(this._raw)
171 , encoding
172 , charset
173 ).toString();
174
175}
176
177// expose Promise
178Response.Promise = global.Promise;