Your IP : 18.217.14.208
'use strict'
/**
* index.js
*
* a request API compatible with window.fetch
*/
const url = require('url')
const http = require('http')
const https = require('https')
const zlib = require('zlib')
const PassThrough = require('stream').PassThrough
const Body = require('./body.js')
const writeToStream = Body.writeToStream
const Response = require('./response')
const Headers = require('./headers')
const Request = require('./request')
const getNodeRequestOptions = Request.getNodeRequestOptions
const FetchError = require('./fetch-error')
const isURL = /^https?:/
/**
* Fetch function
*
* @param Mixed url Absolute url or Request instance
* @param Object opts Fetch options
* @return Promise
*/
exports = module.exports = fetch
function fetch (uri, opts) {
// allow custom promise
if (!fetch.Promise) {
throw new Error('native promise missing, set fetch.Promise to your favorite alternative')
}
Body.Promise = fetch.Promise
// wrap http.request into fetch
return new fetch.Promise((resolve, reject) => {
// build request object
const request = new Request(uri, opts)
const options = getNodeRequestOptions(request)
const send = (options.protocol === 'https:' ? https : http).request
// http.request only support string as host header, this hack make custom host header possible
if (options.headers.host) {
options.headers.host = options.headers.host[0]
}
// send request
const req = send(options)
let reqTimeout
if (request.timeout) {
req.once('socket', socket => {
reqTimeout = setTimeout(() => {
req.abort()
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'))
}, request.timeout)
})
}
req.on('error', err => {
clearTimeout(reqTimeout)
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err))
})
req.on('response', res => {
clearTimeout(reqTimeout)
// handle redirect
if (fetch.isRedirect(res.statusCode) && request.redirect !== 'manual') {
if (request.redirect === 'error') {
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'))
return
}
if (request.counter >= request.follow) {
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'))
return
}
if (!res.headers.location) {
reject(new FetchError(`redirect location header missing at: ${request.url}`, 'invalid-redirect'))
return
}
// Remove authorization if changing hostnames (but not if just
// changing ports or protocols). This matches the behavior of request:
// https://github.com/request/request/blob/b12a6245/lib/redirect.js#L134-L138
const resolvedUrl = url.resolve(request.url, res.headers.location)
let redirectURL = ''
if (!isURL.test(res.headers.location)) {
redirectURL = url.parse(resolvedUrl)
} else {
redirectURL = url.parse(res.headers.location)
}
if (url.parse(request.url).hostname !== redirectURL.hostname) {
request.headers.delete('authorization')
}
// per fetch spec, for POST request with 301/302 response, or any request with 303 response, use GET when following redirect
if (res.statusCode === 303 ||
((res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST')) {
request.method = 'GET'
request.body = null
request.headers.delete('content-length')
}
request.counter++
resolve(fetch(resolvedUrl, request))
return
}
// normalize location header for manual redirect mode
const headers = new Headers()
for (const name of Object.keys(res.headers)) {
if (Array.isArray(res.headers[name])) {
for (const val of res.headers[name]) {
headers.append(name, val)
}
} else {
headers.append(name, res.headers[name])
}
}
if (request.redirect === 'manual' && headers.has('location')) {
headers.set('location', url.resolve(request.url, headers.get('location')))
}
// prepare response
let body = res.pipe(new PassThrough())
const responseOptions = {
url: request.url,
status: res.statusCode,
statusText: res.statusMessage,
headers: headers,
size: request.size,
timeout: request.timeout
}
// HTTP-network fetch step 16.1.2
const codings = headers.get('Content-Encoding')
// HTTP-network fetch step 16.1.3: handle content codings
// in following scenarios we ignore compression support
// 1. compression support is disabled
// 2. HEAD request
// 3. no Content-Encoding header
// 4. no content response (204)
// 5. content not modified response (304)
if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) {
resolve(new Response(body, responseOptions))
return
}
// Be less strict when decoding compressed responses, since sometimes
// servers send slightly invalid responses that are still accepted
// by common browsers.
// Always using Z_SYNC_FLUSH is what cURL does.
const zlibOptions = {
flush: zlib.Z_SYNC_FLUSH,
finishFlush: zlib.Z_SYNC_FLUSH
}
// for gzip
if (codings === 'gzip' || codings === 'x-gzip') {
body = body.pipe(zlib.createGunzip(zlibOptions))
resolve(new Response(body, responseOptions))
return
}
// for deflate
if (codings === 'deflate' || codings === 'x-deflate') {
// handle the infamous raw deflate response from old servers
// a hack for old IIS and Apache servers
const raw = res.pipe(new PassThrough())
raw.once('data', chunk => {
// see http://stackoverflow.com/questions/37519828
if ((chunk[0] & 0x0F) === 0x08) {
body = body.pipe(zlib.createInflate(zlibOptions))
} else {
body = body.pipe(zlib.createInflateRaw(zlibOptions))
}
resolve(new Response(body, responseOptions))
})
return
}
// otherwise, use response as-is
resolve(new Response(body, responseOptions))
})
writeToStream(req, request)
})
};
/**
* Redirect code matching
*
* @param Number code Status code
* @return Boolean
*/
fetch.isRedirect = code => code === 301 || code === 302 || code === 303 || code === 307 || code === 308
// expose Promise
fetch.Promise = global.Promise
exports.Headers = Headers
exports.Request = Request
exports.Response = Response
exports.FetchError = FetchError