• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3/**
4 * index.js
5 *
6 * a request API compatible with window.fetch
7 */
8
9const url = require('url')
10const http = require('http')
11const https = require('https')
12const zlib = require('zlib')
13const PassThrough = require('stream').PassThrough
14
15const Body = require('./body.js')
16const writeToStream = Body.writeToStream
17const Response = require('./response')
18const Headers = require('./headers')
19const Request = require('./request')
20const getNodeRequestOptions = Request.getNodeRequestOptions
21const FetchError = require('./fetch-error')
22const isURL = /^https?:/
23
24/**
25 * Fetch function
26 *
27 * @param   Mixed    url   Absolute url or Request instance
28 * @param   Object   opts  Fetch options
29 * @return  Promise
30 */
31exports = module.exports = fetch
32function fetch (uri, opts) {
33  // allow custom promise
34  if (!fetch.Promise) {
35    throw new Error('native promise missing, set fetch.Promise to your favorite alternative')
36  }
37
38  Body.Promise = fetch.Promise
39
40  // wrap http.request into fetch
41  return new fetch.Promise((resolve, reject) => {
42    // build request object
43    const request = new Request(uri, opts)
44    const options = getNodeRequestOptions(request)
45
46    const send = (options.protocol === 'https:' ? https : http).request
47
48    // http.request only support string as host header, this hack make custom host header possible
49    if (options.headers.host) {
50      options.headers.host = options.headers.host[0]
51    }
52
53    // send request
54    const req = send(options)
55    let reqTimeout
56
57    if (request.timeout) {
58      req.once('socket', socket => {
59        reqTimeout = setTimeout(() => {
60          req.abort()
61          reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'))
62        }, request.timeout)
63      })
64    }
65
66    req.on('error', err => {
67      clearTimeout(reqTimeout)
68      reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err))
69    })
70
71    req.on('response', res => {
72      clearTimeout(reqTimeout)
73
74      // handle redirect
75      if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') {
76        if (request.redirect === 'error') {
77          reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'))
78          return
79        }
80
81        if (request.counter >= request.follow) {
82          reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'))
83          return
84        }
85
86        if (!res.headers.location) {
87          reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect'))
88          return
89        }
90        // Remove authorization if changing hostnames (but not if just
91        // changing ports or protocols).  This matches the behavior of request:
92        // https://github.com/request/request/blob/b12a6245/lib/redirect.js#L134-L138
93        const resolvedUrl = url.resolve(request.url, res.headers.location)
94        let redirectURL = ''
95        if (!isURL.test(res.headers.location)) {
96          redirectURL = url.parse(resolvedUrl)
97        } else {
98          redirectURL = url.parse(res.headers.location)
99        }
100        if (url.parse(request.url).hostname !== redirectURL.hostname) {
101          request.headers.delete('authorization')
102        }
103
104        // per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
105        if (res.statusCode === 303 ||
106          ((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST')) {
107          request.method = 'GET'
108          request.body = null
109          request.headers.delete('content-length')
110        }
111
112        request.counter++
113
114        resolve(fetch(resolvedUrl, request))
115        return
116      }
117
118      // normalize location header for manual redirect mode
119      const headers = new Headers()
120      for (const name of Object.keys(res.headers)) {
121        if (Array.isArray(res.headers[name])) {
122          for (const val of res.headers[name]) {
123            headers.append(name, val)
124          }
125        } else {
126          headers.append(name, res.headers[name])
127        }
128      }
129      if (request.redirect === 'manual' && headers.has('location')) {
130        headers.set('location', url.resolve(request.url, headers.get('location')))
131      }
132
133      // prepare response
134      let body = res.pipe(new PassThrough())
135      const responseOptions = {
136        url: request.url,
137        status: res.statusCode,
138        statusText: res.statusMessage,
139        headers: headers,
140        size: request.size,
141        timeout: request.timeout
142      }
143
144      // HTTP-network fetch step 16.1.2
145      const codings = headers.get('Content-Encoding')
146
147      // HTTP-network fetch step 16.1.3: handle content codings
148
149      // in following scenarios we ignore compression support
150      // 1. compression support is disabled
151      // 2. HEAD request
152      // 3. no Content-Encoding header
153      // 4. no content response (204)
154      // 5. content not modified response (304)
155      if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) {
156        resolve(new Response(body, responseOptions))
157        return
158      }
159
160      // Be less strict when decoding compressed responses, since sometimes
161      // servers send slightly invalid responses that are still accepted
162      // by common browsers.
163      // Always using Z_SYNC_FLUSH is what cURL does.
164      const zlibOptions = {
165        flush: zlib.Z_SYNC_FLUSH,
166        finishFlush: zlib.Z_SYNC_FLUSH
167      }
168
169      // for gzip
170      if (codings === 'gzip' || codings === 'x-gzip') {
171        body = body.pipe(zlib.createGunzip(zlibOptions))
172        resolve(new Response(body, responseOptions))
173        return
174      }
175
176      // for deflate
177      if (codings === 'deflate' || codings === 'x-deflate') {
178        // handle the infamous raw deflate response from old servers
179        // a hack for old IIS and Apache servers
180        const raw = res.pipe(new PassThrough())
181        raw.once('data', chunk => {
182          // see http://stackoverflow.com/questions/37519828
183          if ((chunk[0] & 0x0F) === 0x08) {
184            body = body.pipe(zlib.createInflate(zlibOptions))
185          } else {
186            body = body.pipe(zlib.createInflateRaw(zlibOptions))
187          }
188          resolve(new Response(body, responseOptions))
189        })
190        return
191      }
192
193      // otherwise, use response as-is
194      resolve(new Response(body, responseOptions))
195    })
196
197    writeToStream(req, request)
198  })
199};
200
201/**
202 * Redirect code matching
203 *
204 * @param   Number   code  Status code
205 * @return  Boolean
206 */
207fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308
208
209// expose Promise
210fetch.Promise = global.Promise
211exports.Headers = Headers
212exports.Request = Request
213exports.Response = Response
214exports.FetchError = FetchError
215