body.js 5.4 KB

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