UNPKG

fetch-h2

Version:

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

204 lines 9.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.fetch = void 0; const http2_1 = require("http2"); const callguard_1 = require("callguard"); const abort_1 = require("./abort"); const core_1 = require("./core"); const fetch_common_1 = require("./fetch-common"); const headers_1 = require("./headers"); const response_1 = require("./response"); const utils_1 = require("./utils"); const utils_http2_1 = require("./utils-http2"); const { // Responses HTTP2_HEADER_STATUS, HTTP2_HEADER_LOCATION, HTTP2_HEADER_SET_COOKIE, // Error codes NGHTTP2_NO_ERROR, } = http2_1.constants; // This is from nghttp2.h, but undocumented in Node.js const NGHTTP2_ERR_START_STREAM_NOT_ALLOWED = -516; async function fetchImpl(session, input, init = {}, extra) { const { cleanup, contentDecoders, endStream, headersToSend, integrity, method, onTrailers, origin, redirect, redirected, request, signal, signalPromise, timeoutAt, timeoutInfo, url, } = await (0, fetch_common_1.setupFetch)(session, input, init, extra); const { raceConditionedGoaway } = extra; const streamPromise = session.get(); async function doFetch() { const { session: ph2session, cleanup: socketCleanup } = streamPromise; const h2session = await ph2session; const tryRetryOnGoaway = (resolve) => { // This could be due to a race-condition in GOAWAY. // As of current Node.js, the 'goaway' event is emitted on the // session before this event (at least frameError, probably // 'error' too) is emitted, so we will know if we got it. if (!raceConditionedGoaway.has(origin) && (0, utils_http2_1.hasGotGoaway)(h2session)) { // Don't retry again due to potential GOAWAY raceConditionedGoaway.add(origin); // Since we've got the 'goaway' event, the // context has already released the session, // so a retry will create a new session. resolve(fetchImpl(session, request, { signal, onTrailers }, { raceConditionedGoaway, redirected, timeoutAt, })); return true; } return false; }; let stream; let shouldCleanupSocket = true; try { stream = h2session.request(headersToSend, { endStream }); } catch (err) { if (err.code === "ERR_HTTP2_GOAWAY_SESSION") { // Retry with new session throw new core_1.RetryError(err.code); } throw err; } const response = new Promise((resolve, reject) => { const guard = (0, callguard_1.syncGuard)(reject, { catchAsync: true }); stream.on("aborted", guard((..._whatever) => { reject((0, fetch_common_1.makeAbortedError)()); })); stream.on("error", guard((err) => { if (err && err.code === "ERR_HTTP2_STREAM_ERROR" && err.message && err.message.includes("NGHTTP2_REFUSED_STREAM")) { if (tryRetryOnGoaway(resolve)) return; } reject(err); })); stream.on("frameError", guard((_type, code, _streamId) => { if (code === NGHTTP2_ERR_START_STREAM_NOT_ALLOWED && endStream) { if (tryRetryOnGoaway(resolve)) return; } reject(new Error("Request failed")); })); stream.on("close", guard(() => { if (shouldCleanupSocket) socketCleanup(); // We'll get an 'error' event if there actually is an // error, but not if we got NGHTTP2_NO_ERROR. // In case of an error, the 'error' event will be awaited // instead, to get (and propagate) the error object. if (stream.rstCode === NGHTTP2_NO_ERROR) reject(new core_1.AbortError("Stream prematurely closed")); })); stream.on("timeout", guard((..._whatever) => { reject((0, fetch_common_1.makeTimeoutError)()); })); stream.on("trailers", guard((_headers, _flags) => { if (!onTrailers) return; try { const headers = new headers_1.GuardedHeaders("response"); Object.keys(_headers).forEach(key => { if (Array.isArray(_headers[key])) _headers[key] .forEach(value => headers.append(key, value)); else headers.set(key, "" + _headers[key]); }); onTrailers(headers); } catch (err) { // TODO: Implement #8 // tslint:disable-next-line console.warn("Trailer handling failed", err); } })); // ClientHttp2Stream events stream.on("continue", guard((..._whatever) => { reject((0, fetch_common_1.make100Error)()); })); stream.on("response", guard((headers) => { const { signal: bodySignal = void 0, abort: bodyAbort = void 0, } = signal ? new abort_1.AbortController() : {}; if (signal) { const abortHandler = () => { bodyAbort(); stream.destroy(); }; if (signal.aborted) { // No reason to continue, the request is aborted abortHandler(); return; } signal.once("abort", abortHandler); stream.once("close", () => { signal.removeListener("abort", abortHandler); }); } const status = "" + headers[HTTP2_HEADER_STATUS]; const location = (0, utils_1.parseLocation)(headers[HTTP2_HEADER_LOCATION], url); const isRedirected = utils_1.isRedirectStatus[status]; if (headers[HTTP2_HEADER_SET_COOKIE]) { const setCookies = (0, utils_1.arrayify)(headers[HTTP2_HEADER_SET_COOKIE]); session.cookieJar.setCookies(setCookies, url); } if (!input.allowForbiddenHeaders) { delete headers["set-cookie"]; delete headers["set-cookie2"]; } if (isRedirected && !location) return reject((0, fetch_common_1.makeIllegalRedirectError)()); if (!isRedirected || redirect === "manual") return resolve(new response_1.StreamResponse(contentDecoders, url, stream, headers, redirect === "manual" ? false : extra.redirected.length > 0, {}, bodySignal, 2, input.allowForbiddenHeaders, integrity)); const { url: locationUrl, isRelative } = location; if (redirect === "error") return reject((0, fetch_common_1.makeRedirectionError)(locationUrl)); // redirect is 'follow' // We don't support re-sending a non-GET/HEAD request (as // we don't want to [can't, if its' streamed] re-send the // body). The concept is fundementally broken anyway... if (!endStream) return reject((0, fetch_common_1.makeRedirectionMethodError)(locationUrl, method)); if (!location) return reject((0, fetch_common_1.makeIllegalRedirectError)()); if (isRelative) { shouldCleanupSocket = false; stream.destroy(); resolve(fetchImpl(session, request.clone(locationUrl), init, { raceConditionedGoaway, redirected: redirected.concat(url), timeoutAt, })); } else { resolve(session.newFetch(request.clone(locationUrl), init, { timeoutAt, redirected: redirected.concat(url), })); } })); }); if (!endStream) await request.readable() .then(readable => { (0, utils_1.pipeline)(readable, stream) .catch(_err => { // TODO: Implement error handling }); }); return response; } return (0, fetch_common_1.handleSignalAndTimeout)(signalPromise, timeoutInfo, cleanup, doFetch, streamPromise.cleanup); } function fetch(session, input, init, extra) { var _a; const http2Extra = { timeoutAt: extra === null || extra === void 0 ? void 0 : extra.timeoutAt, redirected: (_a = extra === null || extra === void 0 ? void 0 : extra.redirected) !== null && _a !== void 0 ? _a : [], raceConditionedGoaway: new Set(), }; return fetchImpl(session, input, init, http2Extra); } exports.fetch = fetch; //# sourceMappingURL=fetch-http2.js.map