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