1'use strict'; 2const EventEmitter = require('events'); 3const http = require('http'); 4const https = require('https'); 5const PassThrough = require('stream').PassThrough; 6const urlLib = require('url'); 7const querystring = require('querystring'); 8const duplexer3 = require('duplexer3'); 9const isStream = require('is-stream'); 10const getStream = require('get-stream'); 11const timedOut = require('timed-out'); 12const urlParseLax = require('url-parse-lax'); 13const lowercaseKeys = require('lowercase-keys'); 14const isRedirect = require('is-redirect'); 15const unzipResponse = require('unzip-response'); 16const createErrorClass = require('create-error-class'); 17const isRetryAllowed = require('is-retry-allowed'); 18const Buffer = require('safe-buffer').Buffer; 19const pkg = require('./package'); 20 21function requestAsEventEmitter(opts) { 22 opts = opts || {}; 23 24 const ee = new EventEmitter(); 25 const requestUrl = opts.href || urlLib.resolve(urlLib.format(opts), opts.path); 26 let redirectCount = 0; 27 let retryCount = 0; 28 let redirectUrl; 29 30 const get = opts => { 31 const fn = opts.protocol === 'https:' ? https : http; 32 33 const req = fn.request(opts, res => { 34 const statusCode = res.statusCode; 35 36 if (isRedirect(statusCode) && opts.followRedirect && 'location' in res.headers && (opts.method === 'GET' || opts.method === 'HEAD')) { 37 res.resume(); 38 39 if (++redirectCount > 10) { 40 ee.emit('error', new got.MaxRedirectsError(statusCode, opts), null, res); 41 return; 42 } 43 44 const bufferString = Buffer.from(res.headers.location, 'binary').toString(); 45 46 redirectUrl = urlLib.resolve(urlLib.format(opts), bufferString); 47 const redirectOpts = Object.assign({}, opts, urlLib.parse(redirectUrl)); 48 49 ee.emit('redirect', res, redirectOpts); 50 51 get(redirectOpts); 52 53 return; 54 } 55 56 setImmediate(() => { 57 const response = typeof unzipResponse === 'function' && req.method !== 'HEAD' ? unzipResponse(res) : res; 58 response.url = redirectUrl || requestUrl; 59 response.requestUrl = requestUrl; 60 61 ee.emit('response', response); 62 }); 63 }); 64 65 req.once('error', err => { 66 const backoff = opts.retries(++retryCount, err); 67 68 if (backoff) { 69 setTimeout(get, backoff, opts); 70 return; 71 } 72 73 ee.emit('error', new got.RequestError(err, opts)); 74 }); 75 76 if (opts.gotTimeout) { 77 timedOut(req, opts.gotTimeout); 78 } 79 80 setImmediate(() => { 81 ee.emit('request', req); 82 }); 83 }; 84 85 get(opts); 86 return ee; 87} 88 89function asPromise(opts) { 90 return new Promise((resolve, reject) => { 91 const ee = requestAsEventEmitter(opts); 92 93 ee.on('request', req => { 94 if (isStream(opts.body)) { 95 opts.body.pipe(req); 96 opts.body = undefined; 97 return; 98 } 99 100 req.end(opts.body); 101 }); 102 103 ee.on('response', res => { 104 const stream = opts.encoding === null ? getStream.buffer(res) : getStream(res, opts); 105 106 stream 107 .catch(err => reject(new got.ReadError(err, opts))) 108 .then(data => { 109 const statusCode = res.statusCode; 110 const limitStatusCode = opts.followRedirect ? 299 : 399; 111 112 res.body = data; 113 114 if (opts.json && res.body) { 115 try { 116 res.body = JSON.parse(res.body); 117 } catch (e) { 118 throw new got.ParseError(e, statusCode, opts, data); 119 } 120 } 121 122 if (statusCode < 200 || statusCode > limitStatusCode) { 123 throw new got.HTTPError(statusCode, opts); 124 } 125 126 resolve(res); 127 }) 128 .catch(err => { 129 Object.defineProperty(err, 'response', {value: res}); 130 reject(err); 131 }); 132 }); 133 134 ee.on('error', reject); 135 }); 136} 137 138function asStream(opts) { 139 const input = new PassThrough(); 140 const output = new PassThrough(); 141 const proxy = duplexer3(input, output); 142 143 if (opts.json) { 144 throw new Error('got can not be used as stream when options.json is used'); 145 } 146 147 if (opts.body) { 148 proxy.write = () => { 149 throw new Error('got\'s stream is not writable when options.body is used'); 150 }; 151 } 152 153 const ee = requestAsEventEmitter(opts); 154 155 ee.on('request', req => { 156 proxy.emit('request', req); 157 158 if (isStream(opts.body)) { 159 opts.body.pipe(req); 160 return; 161 } 162 163 if (opts.body) { 164 req.end(opts.body); 165 return; 166 } 167 168 if (opts.method === 'POST' || opts.method === 'PUT' || opts.method === 'PATCH') { 169 input.pipe(req); 170 return; 171 } 172 173 req.end(); 174 }); 175 176 ee.on('response', res => { 177 const statusCode = res.statusCode; 178 179 res.pipe(output); 180 181 if (statusCode < 200 || statusCode > 299) { 182 proxy.emit('error', new got.HTTPError(statusCode, opts), null, res); 183 return; 184 } 185 186 proxy.emit('response', res); 187 }); 188 189 ee.on('redirect', proxy.emit.bind(proxy, 'redirect')); 190 ee.on('error', proxy.emit.bind(proxy, 'error')); 191 192 return proxy; 193} 194 195function normalizeArguments(url, opts) { 196 if (typeof url !== 'string' && typeof url !== 'object') { 197 throw new Error(`Parameter \`url\` must be a string or object, not ${typeof url}`); 198 } 199 200 if (typeof url === 'string') { 201 url = url.replace(/^unix:/, 'http://$&'); 202 url = urlParseLax(url); 203 204 if (url.auth) { 205 throw new Error('Basic authentication must be done with auth option'); 206 } 207 } 208 209 opts = Object.assign( 210 { 211 protocol: 'http:', 212 path: '', 213 retries: 5 214 }, 215 url, 216 opts 217 ); 218 219 opts.headers = Object.assign({ 220 'user-agent': `${pkg.name}/${pkg.version} (https://github.com/sindresorhus/got)`, 221 'accept-encoding': 'gzip,deflate' 222 }, lowercaseKeys(opts.headers)); 223 224 const query = opts.query; 225 226 if (query) { 227 if (typeof query !== 'string') { 228 opts.query = querystring.stringify(query); 229 } 230 231 opts.path = `${opts.path.split('?')[0]}?${opts.query}`; 232 delete opts.query; 233 } 234 235 if (opts.json && opts.headers.accept === undefined) { 236 opts.headers.accept = 'application/json'; 237 } 238 239 let body = opts.body; 240 241 if (body) { 242 if (typeof body !== 'string' && !(body !== null && typeof body === 'object')) { 243 throw new Error('options.body must be a ReadableStream, string, Buffer or plain Object'); 244 } 245 246 opts.method = opts.method || 'POST'; 247 248 if (isStream(body) && typeof body.getBoundary === 'function') { 249 // Special case for https://github.com/form-data/form-data 250 opts.headers['content-type'] = opts.headers['content-type'] || `multipart/form-data; boundary=${body.getBoundary()}`; 251 } else if (body !== null && typeof body === 'object' && !Buffer.isBuffer(body) && !isStream(body)) { 252 opts.headers['content-type'] = opts.headers['content-type'] || 'application/x-www-form-urlencoded'; 253 body = opts.body = querystring.stringify(body); 254 } 255 256 if (opts.headers['content-length'] === undefined && opts.headers['transfer-encoding'] === undefined && !isStream(body)) { 257 const length = typeof body === 'string' ? Buffer.byteLength(body) : body.length; 258 opts.headers['content-length'] = length; 259 } 260 } 261 262 opts.method = (opts.method || 'GET').toUpperCase(); 263 264 if (opts.hostname === 'unix') { 265 const matches = /(.+):(.+)/.exec(opts.path); 266 267 if (matches) { 268 opts.socketPath = matches[1]; 269 opts.path = matches[2]; 270 opts.host = null; 271 } 272 } 273 274 if (typeof opts.retries !== 'function') { 275 const retries = opts.retries; 276 277 opts.retries = (iter, err) => { 278 if (iter > retries || !isRetryAllowed(err)) { 279 return 0; 280 } 281 282 const noise = Math.random() * 100; 283 284 return ((1 << iter) * 1000) + noise; 285 }; 286 } 287 288 if (opts.followRedirect === undefined) { 289 opts.followRedirect = true; 290 } 291 292 if (opts.timeout) { 293 opts.gotTimeout = opts.timeout; 294 delete opts.timeout; 295 } 296 297 return opts; 298} 299 300function got(url, opts) { 301 try { 302 return asPromise(normalizeArguments(url, opts)); 303 } catch (err) { 304 return Promise.reject(err); 305 } 306} 307 308const helpers = [ 309 'get', 310 'post', 311 'put', 312 'patch', 313 'head', 314 'delete' 315]; 316 317helpers.forEach(el => { 318 got[el] = (url, opts) => got(url, Object.assign({}, opts, {method: el})); 319}); 320 321got.stream = (url, opts) => asStream(normalizeArguments(url, opts)); 322 323for (const el of helpers) { 324 got.stream[el] = (url, opts) => got.stream(url, Object.assign({}, opts, {method: el})); 325} 326 327function stdError(error, opts) { 328 if (error.code !== undefined) { 329 this.code = error.code; 330 } 331 332 Object.assign(this, { 333 message: error.message, 334 host: opts.host, 335 hostname: opts.hostname, 336 method: opts.method, 337 path: opts.path 338 }); 339} 340 341got.RequestError = createErrorClass('RequestError', stdError); 342got.ReadError = createErrorClass('ReadError', stdError); 343got.ParseError = createErrorClass('ParseError', function (e, statusCode, opts, data) { 344 stdError.call(this, e, opts); 345 this.statusCode = statusCode; 346 this.statusMessage = http.STATUS_CODES[this.statusCode]; 347 this.message = `${e.message} in "${urlLib.format(opts)}": \n${data.slice(0, 77)}...`; 348}); 349 350got.HTTPError = createErrorClass('HTTPError', function (statusCode, opts) { 351 stdError.call(this, {}, opts); 352 this.statusCode = statusCode; 353 this.statusMessage = http.STATUS_CODES[this.statusCode]; 354 this.message = `Response code ${this.statusCode} (${this.statusMessage})`; 355}); 356 357got.MaxRedirectsError = createErrorClass('MaxRedirectsError', function (statusCode, opts) { 358 stdError.call(this, {}, opts); 359 this.statusCode = statusCode; 360 this.statusMessage = http.STATUS_CODES[this.statusCode]; 361 this.message = 'Redirected 10 times. Aborting.'; 362}); 363 364module.exports = got; 365