UNPKG

get-uri

Version:

Returns a `stream.Readable` from a URI string

187 lines 6.92 kB
import http_ from 'http'; import https from 'https'; import { once } from 'events'; import createDebug from 'debug'; import HTTPError from './http-error.js'; import NotFoundError from './notfound.js'; import NotModifiedError from './notmodified.js'; const debug = createDebug('get-uri:http'); /** * Returns a Readable stream from an "http:" URI. */ export const http = async (url, opts = {}) => { debug('GET %o', url.href); const cache = getCache(url, opts.cache); // first check the previous Expires and/or Cache-Control headers // of a previous response if a `cache` was provided if (cache && isFresh(cache) && typeof cache.statusCode === 'number') { // check for a 3xx "redirect" status code on the previous cache const type = (cache.statusCode / 100) | 0; if (type === 3 && cache.headers.location) { debug('cached redirect'); throw new Error('TODO: implement cached redirects!'); } // otherwise we assume that it's the destination endpoint, // since there's nowhere else to redirect to throw new NotModifiedError(); } // 5 redirects allowed by default const maxRedirects = typeof opts.maxRedirects === 'number' ? opts.maxRedirects : 5; debug('allowing %o max redirects', maxRedirects); let mod; if (opts.http) { // the `https` module passed in from the "http.js" file mod = opts.http; debug('using secure `https` core module'); } else { mod = http_; debug('using `http` core module'); } const options = { ...opts }; // add "cache validation" headers if a `cache` was provided if (cache) { const prevHeaders = options.headers; const headers = {}; if (prevHeaders && !Array.isArray(prevHeaders)) { Object.assign(headers, prevHeaders); } options.headers = headers; const lastModified = cache.headers['last-modified']; if (lastModified) { headers['If-Modified-Since'] = lastModified; debug('added "If-Modified-Since" request header: %o', lastModified); } const etag = cache.headers.etag; if (etag) { headers['If-None-Match'] = etag; debug('added "If-None-Match" request header: %o', etag); } } const req = mod.get(url, options); const [res] = await once(req, 'response'); const code = res.statusCode || 0; // assign a Date to this response for the "Cache-Control" delta calculation res.date = Date.now(); res.parsed = url; debug('got %o response status code', code); // any 2xx response is a "success" code const type = (code / 100) | 0; // check for a 3xx "redirect" status code const location = res.headers.location; if (type === 3 && location) { if (!opts.redirects) opts.redirects = []; const redirects = opts.redirects; if (redirects.length < maxRedirects) { debug('got a "redirect" status code with Location: %o', location); // flush this response - we're not going to use it res.resume(); // hang on to this Response object for the "redirects" Array redirects.push(res); const newUri = new URL(location, url.href); debug('resolved redirect URL: %o', newUri.href); const left = maxRedirects - redirects.length; debug('%o more redirects allowed after this one', left); // check if redirecting to a different protocol if (newUri.protocol !== url.protocol) { opts.http = newUri.protocol === 'https:' ? https : undefined; } return http(newUri, opts); } } // if we didn't get a 2xx "success" status code, then create an Error object if (type !== 2) { res.resume(); if (code === 304) { throw new NotModifiedError(); } else if (code === 404) { throw new NotFoundError(); } // other HTTP-level error throw new HTTPError(code); } if (opts.redirects) { // store a reference to the "redirects" Array on the Response object so that // they can be inspected during a subsequent call to GET the same URI res.redirects = opts.redirects; } return res; }; /** * Returns `true` if the provided cache's "freshness" is valid. That is, either * the Cache-Control header or Expires header values are still within the allowed * time period. * * @return {Boolean} * @api private */ function isFresh(cache) { let fresh = false; let expires = parseInt(cache.headers.expires || '', 10); const cacheControl = cache.headers['cache-control']; if (cacheControl) { // for Cache-Control rules, see: http://www.mnot.net/cache_docs/#CACHE-CONTROL debug('Cache-Control: %o', cacheControl); const parts = cacheControl.split(/,\s*?\b/); for (let i = 0; i < parts.length; i++) { const part = parts[i]; const subparts = part.split('='); const name = subparts[0]; switch (name) { case 'max-age': expires = (cache.date || 0) + parseInt(subparts[1], 10) * 1000; fresh = Date.now() < expires; if (fresh) { debug('cache is "fresh" due to previous %o Cache-Control param', part); } return fresh; case 'must-revalidate': // XXX: what we supposed to do here? break; case 'no-cache': case 'no-store': debug('cache is "stale" due to explicit %o Cache-Control param', name); return false; default: // ignore unknown cache value break; } } } else if (expires) { // for Expires rules, see: http://www.mnot.net/cache_docs/#EXPIRES debug('Expires: %o', expires); fresh = Date.now() < expires; if (fresh) { debug('cache is "fresh" due to previous Expires response header'); } return fresh; } return false; } /** * Attempts to return a previous Response object from a previous GET call to the * same URI. * * @api private */ function getCache(url, cache) { if (cache) { if (cache.parsed && cache.parsed.href === url.href) { return cache; } if (cache.redirects) { for (let i = 0; i < cache.redirects.length; i++) { const c = getCache(url, cache.redirects[i]); if (c) { return c; } } } } return null; } //# sourceMappingURL=http.js.map