UNPKG

fetch-h2

Version:

HTTP/1+2 Fetch API client for Node.js

290 lines 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Context = void 0; const url_1 = require("url"); const already_1 = require("already"); const context_http1_1 = require("./context-http1"); const context_http2_1 = require("./context-http2"); const context_https_1 = require("./context-https"); const cookie_jar_1 = require("./cookie-jar"); const core_1 = require("./core"); const fetch_http1_1 = require("./fetch-http1"); const fetch_http2_1 = require("./fetch-http2"); const version_1 = require("./generated/version"); const request_1 = require("./request"); const utils_1 = require("./utils"); const origin_cache_1 = require("./origin-cache"); function makeDefaultUserAgent() { const name = `fetch-h2/${version_1.version} (+https://github.com/grantila/fetch-h2)`; const node = `nodejs/${process.versions.node}`; const nghttp2 = `nghttp2/${process.versions.nghttp2}`; const uv = `uv/${process.versions.uv}`; return `${name} ${node} ${nghttp2} ${uv}`; } const defaultUserAgent = makeDefaultUserAgent(); const defaultAccept = "application/json,text/*;q=0.9,*/*;q=0.8"; class Context { constructor(opts) { this._httpsFunnel = (0, already_1.funnel)(); this._http1Funnel = (0, already_1.funnel)(); this._http2Funnel = (0, already_1.funnel)(); this._originCache = new origin_cache_1.default(); this._userAgent = ""; this._overwriteUserAgent = false; this._accept = ""; this._cookieJar = void 0; this._decoders = []; this._sessionOptions = {}; this._httpProtocol = "http1"; this._httpsProtocols = ["http2", "http1"]; this._http1Options = {}; this.setup(opts); this.h1Context = new context_http1_1.H1Context(this._http1Options); this.h2Context = new context_http2_1.H2Context(this.decoders.bind(this), this.sessionOptions.bind(this)); } setup(opts) { opts = opts || {}; this._cookieJar = "cookieJar" in opts ? (opts.cookieJar || new cookie_jar_1.CookieJar()) : new cookie_jar_1.CookieJar(); this._userAgent = (0, core_1.parsePerOrigin)(opts.userAgent, ""); this._overwriteUserAgent = (0, core_1.parsePerOrigin)(opts.overwriteUserAgent, false); this._accept = (0, core_1.parsePerOrigin)(opts.accept, defaultAccept); this._decoders = (0, core_1.parsePerOrigin)(opts.decoders, []); this._sessionOptions = (0, core_1.parsePerOrigin)(opts.session, {}); this._httpProtocol = (0, core_1.parsePerOrigin)(opts.httpProtocol, "http1"); this._httpsProtocols = (0, core_1.parsePerOrigin)(opts.httpsProtocols, ["http2", "http1"]); Object.assign(this._http1Options, opts.http1 || {}); } userAgent(origin) { const combine = (userAgent, overwriteUserAgent) => { const defaultUA = overwriteUserAgent ? "" : defaultUserAgent; return userAgent ? defaultUA ? userAgent + " " + defaultUA : userAgent : defaultUA; }; return combine((0, core_1.getByOrigin)(this._userAgent, origin), (0, core_1.getByOrigin)(this._overwriteUserAgent, origin)); } decoders(origin) { return (0, core_1.getByOrigin)(this._decoders, origin); } sessionOptions(origin) { return (0, core_1.getByOrigin)(this._sessionOptions, origin); } onPush(pushHandler) { this.h2Context._pushHandler = pushHandler; } async fetch(input, init) { return this.retryFetch(input, init); } async disconnect(url) { const { origin } = this.parseInput(url); this._originCache.disconnect(origin); await Promise.all([ this.h1Context.disconnect(url), this.h2Context.disconnect(url), ]); } async disconnectAll() { this._originCache.disconnectAll(); await Promise.all([ this.h1Context.disconnectAll(), this.h2Context.disconnectAll(), ]); } async retryFetch(input, init, extra, count = 0) { ++count; return this.retryableFetch(input, init, extra) .catch((0, already_1.specific)(core_1.RetryError, err => { // TODO: Implement a more robust retry logic if (count > 10) throw err; return this.retryFetch(input, init, extra, count); })); } async retryableFetch(input, init, extra) { const { hostname, origin, port, protocol, url } = this.parseInput(input); // Rewrite url to get rid of "http1://" and "http2://" const request = input instanceof request_1.Request ? input.url !== url ? input.clone(url) : input : new request_1.Request(input, { ...(init || {}), url }); const { rejectUnauthorized } = this.sessionOptions(origin); const makeSimpleSession = (protocol) => ({ accept: () => (0, core_1.getByOrigin)(this._accept, origin), contentDecoders: () => (0, core_1.getByOrigin)(this._decoders, origin), cookieJar: this._cookieJar, protocol, userAgent: () => this.userAgent(origin), newFetch: this.retryFetch.bind(this), }); const doFetchHttp1 = (socket, cleanup) => { const sessionGetterHttp1 = { get: (url) => ({ cleanup, req: this.getHttp1(url, socket, request, rejectUnauthorized), }), ...makeSimpleSession("http1"), }; return (0, fetch_http1_1.fetch)(sessionGetterHttp1, request, init, extra); }; const doFetchHttp2 = async (cacheableSession) => { const { session, unref } = cacheableSession; const cleanup = (0, already_1.once)(unref); try { const sessionGetterHttp2 = { get: () => ({ session, cleanup }), ...makeSimpleSession("http2"), }; return await (0, fetch_http2_1.fetch)(sessionGetterHttp2, request, init, extra); } catch (err) { cleanup(); throw err; } }; const tryWaitForHttp1 = async (session) => { const { socket: freeHttp1Socket, cleanup, shouldCreateNew } = this.h1Context.getFreeSocketForSession(session); if (freeHttp1Socket) return doFetchHttp1(freeHttp1Socket, cleanup); if (!shouldCreateNew) { // We've maxed out HTTP/1 connections, wait for one to be // freed. const { socket, cleanup } = await this.h1Context.waitForSocketBySession(session); return doFetchHttp1(socket, cleanup); } }; if (protocol === "http1") { return this._http1Funnel(async (shouldRetry, retry, shortcut) => { var _a; if (shouldRetry()) return retry(); // Plain text HTTP/1(.1) const cacheItem = this._originCache.get("http1", origin); const session = (_a = cacheItem === null || cacheItem === void 0 ? void 0 : cacheItem.session) !== null && _a !== void 0 ? _a : this.h1Context.getSessionForOrigin(origin); const resp = await tryWaitForHttp1(session); if (resp) return resp; const socket = await this.h1Context.makeNewConnection(url); this._originCache.set(origin, "http1", session); shortcut(); const cleanup = this.h1Context.addUsedSocket(session, socket); return doFetchHttp1(socket, cleanup); }); } else if (protocol === "http2") { return this._http2Funnel(async (_, __, shortcut) => { // Plain text HTTP/2 const cacheItem = this._originCache.get("http2", origin); if (cacheItem) { cacheItem.session.ref(); shortcut(); return doFetchHttp2(cacheItem.session); } // Convert socket into http2 session, this will ref (*) const cacheableSession = this.h2Context.createHttp2(origin, () => { this._originCache.delete(cacheableSession); }); this._originCache.set(origin, "http2", cacheableSession); shortcut(); // Session now lingering, it will be re-used by the next get() return doFetchHttp2(cacheableSession); }); } else // protocol === "https" { return this._httpsFunnel((shouldRetry, retry, shortcut) => shouldRetry() ? retry() : this.connectSequenciallyTLS(shortcut, hostname, port, origin, tryWaitForHttp1, doFetchHttp1, doFetchHttp2)); } } async connectSequenciallyTLS(shortcut, hostname, port, origin, tryWaitForHttp1, doFetchHttp1, doFetchHttp2) { var _a, _b; const cacheItem = (_a = this._originCache.get("https2", origin)) !== null && _a !== void 0 ? _a : this._originCache.get("https1", origin); if (cacheItem) { if (cacheItem.protocol === "https1") { shortcut(); const resp = await tryWaitForHttp1(cacheItem.session); if (resp) return resp; } else if (cacheItem.protocol === "https2") { cacheItem.session.ref(); shortcut(); return doFetchHttp2(cacheItem.session); } } // Use ALPN to figure out protocol lazily const { protocol, socket, altNameMatch } = await (0, context_https_1.connectTLS)(hostname, port, (0, core_1.getByOrigin)(this._httpsProtocols, origin), (0, core_1.getByOrigin)(this._sessionOptions, origin)); const disconnect = (0, already_1.once)(() => { if (!socket.destroyed) { socket.destroy(); socket.unref(); } }); if (protocol === "http2") { // Convert socket into http2 session, this will ref (*) // const { cleanup, session, didCreate } = const cacheableSession = this.h2Context.createHttp2(origin, () => { this._originCache.delete(cacheableSession); }, { createConnection: () => socket, }); this._originCache.set(origin, "https2", cacheableSession, altNameMatch, disconnect); shortcut(); // Session now lingering, it will be re-used by the next get() return doFetchHttp2(cacheableSession); } else // protocol === "http1" { const session = (_b = cacheItem === null || cacheItem === void 0 ? void 0 : cacheItem.session) !== null && _b !== void 0 ? _b : this.h1Context.getSessionForOrigin(origin); // TODO: Update the alt-name list in the origin cache (if the new // TLS socket contains more/other alt-names). if (!cacheItem) this._originCache.set(origin, "https1", session, altNameMatch, disconnect); const cleanup = this.h1Context.addUsedSocket(session, socket); shortcut(); return doFetchHttp1(socket, cleanup); } } getHttp1(url, socket, request, rejectUnauthorized) { return this.h1Context.connect(new url_1.URL(url), { createConnection: () => socket, rejectUnauthorized, }, request); } parseInput(input) { const { hostname, origin, port, protocol, url } = (0, utils_1.parseInput)(typeof input !== "string" ? input.url : input); const defaultHttp = this._httpProtocol; if ((protocol === "http" && defaultHttp === "http1") || protocol === "http1") return { hostname, origin, port, protocol: "http1", url, }; else if ((protocol === "http" && defaultHttp === "http2") || protocol === "http2") return { hostname, origin, port, protocol: "http2", url, }; else if (protocol === "https") return { hostname, origin, port, protocol: "https", url, }; else throw new core_1.FetchError(`Invalid protocol "${protocol}"`); } } exports.Context = Context; //# sourceMappingURL=context.js.map