Comparing version 2.3.0 to 2.4.0
/// <reference types="node" /> | ||
import { AbortSignal } from "./abort"; | ||
import { BodyTypes, IBody } from "./core"; | ||
import { BodyTypes, IBody, StorageBodyTypes } from "./core"; | ||
export declare class Body implements IBody { | ||
@@ -8,3 +8,3 @@ readonly bodyUsed: boolean; | ||
protected _mime?: string; | ||
private _body?; | ||
protected _body?: StorageBodyTypes | null; | ||
private _used; | ||
@@ -42,4 +42,6 @@ private _integrity?; | ||
private _getLength; | ||
private _getBody; | ||
get mime(): string | undefined; | ||
get length(): number | null; | ||
get stream(): NodeJS.ReadableStream | undefined; | ||
} |
@@ -232,2 +232,5 @@ "use strict"; | ||
} | ||
_getBody() { | ||
return this._body; | ||
} | ||
get mime() { | ||
@@ -239,4 +242,8 @@ return this._getMime.call(this._ref); | ||
} | ||
get stream() { | ||
const rawBody = this._getBody.call(this._ref); | ||
return rawBody && isStream(rawBody) ? rawBody : undefined; | ||
} | ||
} | ||
exports.BodyInspector = BodyInspector; | ||
//# sourceMappingURL=body.js.map |
/// <reference types="node" /> | ||
import { RequestOptions } from "https"; | ||
import { Socket } from "net"; | ||
@@ -23,8 +24,32 @@ import { URL } from "url"; | ||
export declare type FreeSocketInfo = FreeSocketInfoWithSocket | FreeSocketInfoWithoutSocket; | ||
export declare class OriginPool { | ||
private usedSockets; | ||
private unusedSockets; | ||
private waiting; | ||
private keepAlive; | ||
private keepAliveMsecs; | ||
private maxSockets; | ||
private maxFreeSockets; | ||
private connOpts; | ||
constructor(keepAlive: boolean, keepAliveMsecs: number, maxSockets: number, maxFreeSockets: number, timeout: number | void); | ||
connect(options: RequestOptions): import("http").ClientRequest; | ||
addUsed(socket: Socket): () => void; | ||
getFreeSocket(): FreeSocketInfo; | ||
waitForSocket(): Promise<SocketAndCleanup>; | ||
disconnectAll(): Promise<void>; | ||
private getFirstUnused; | ||
private tryReuse; | ||
private pumpWaiting; | ||
private disconnectSocket; | ||
private makeCleaner; | ||
private moveToUnused; | ||
private moveToUsed; | ||
} | ||
export declare class H1Context { | ||
private contextPool; | ||
constructor(options: Partial<Http1Options>); | ||
getFreeSocketForOrigin(origin: string): FreeSocketInfo; | ||
addUsedSocket(origin: string, socket: Socket): () => void; | ||
waitForSocket(origin: string): Promise<SocketAndCleanup>; | ||
getSessionForOrigin(origin: string): OriginPool; | ||
getFreeSocketForSession(session: OriginPool): FreeSocketInfo; | ||
addUsedSocket(session: OriginPool, socket: Socket): () => void; | ||
waitForSocketBySession(session: OriginPool): Promise<SocketAndCleanup>; | ||
connect(url: URL, extraOptions: ConnectOptions, request: Request): import("http").ClientRequest; | ||
@@ -31,0 +56,0 @@ makeNewConnection(url: string): Promise<Socket>; |
@@ -117,2 +117,3 @@ "use strict"; | ||
} | ||
exports.OriginPool = OriginPool; | ||
class ContextPool { | ||
@@ -154,2 +155,5 @@ constructor(options) { | ||
} | ||
function sessionToPool(session) { | ||
return session; | ||
} | ||
class H1Context { | ||
@@ -159,13 +163,16 @@ constructor(options) { | ||
} | ||
getFreeSocketForOrigin(origin) { | ||
return this.contextPool.hasOrigin(origin) | ||
? this.contextPool.getOriginPool(origin).getFreeSocket() | ||
: { shouldCreateNew: true }; | ||
getSessionForOrigin(origin) { | ||
return this.contextPool.getOriginPool(origin); | ||
} | ||
addUsedSocket(origin, socket) { | ||
return this.contextPool.getOriginPool(origin).addUsed(socket); | ||
getFreeSocketForSession(session) { | ||
const pool = sessionToPool(session); | ||
return pool.getFreeSocket(); | ||
} | ||
waitForSocket(origin) { | ||
return this.contextPool.getOriginPool(origin).waitForSocket(); | ||
addUsedSocket(session, socket) { | ||
const pool = sessionToPool(session); | ||
return pool.addUsed(socket); | ||
} | ||
waitForSocketBySession(session) { | ||
return sessionToPool(session).waitForSocket(); | ||
} | ||
connect(url, extraOptions, request) { | ||
@@ -172,0 +179,0 @@ const { origin, protocol, hostname, password, pathname, search, username, } = url; |
@@ -7,2 +7,3 @@ /// <reference types="node" /> | ||
interface H2SessionItem { | ||
firstOrigin: string; | ||
session: ClientHttp2Session; | ||
@@ -13,2 +14,7 @@ promise: Promise<ClientHttp2Session>; | ||
} | ||
export interface CacheableH2Session { | ||
ref: () => void; | ||
session: Promise<ClientHttp2Session>; | ||
unref: () => void; | ||
} | ||
export declare type PushHandler = (origin: string, request: Request, getResponse: () => Promise<Response>) => void; | ||
@@ -24,8 +30,3 @@ export declare type GetDecoders = (origin: string) => ReadonlyArray<Decoder>; | ||
constructor(getDecoders: GetDecoders, getSessionOptions: GetSessionOptions); | ||
hasOrigin(origin: string): boolean; | ||
getOrCreateHttp2(origin: string, extraOptions?: SecureClientSessionOptions): { | ||
didCreate: boolean; | ||
session: Promise<ClientHttp2Session>; | ||
cleanup: () => void; | ||
}; | ||
createHttp2(origin: string, onGotGoaway: () => void, extraOptions?: SecureClientSessionOptions): CacheableH2Session; | ||
disconnectSession(session: ClientHttp2Session): Promise<void>; | ||
@@ -32,0 +33,0 @@ releaseSession(origin: string): void; |
@@ -14,2 +14,3 @@ "use strict"; | ||
constructor(getDecoders, getSessionOptions) { | ||
// TODO: Remove in favor of protocol-agnostic origin cache | ||
this._h2sessions = new Map(); | ||
@@ -26,3 +27,3 @@ this._h2staleSessions = new Map(); | ||
const printSession = (origin, session) => { | ||
debug(" Origin:", origin); | ||
debug(" First origin:", origin); | ||
debug(" Ref-counter:", session.__fetch_h2_refcount); | ||
@@ -50,40 +51,24 @@ debug(" Destroyed:", session.destroyed); | ||
} | ||
hasOrigin(origin) { | ||
return this._h2sessions.has(origin); | ||
} | ||
getOrCreateHttp2(origin, extraOptions) { | ||
const willCreate = !this._h2sessions.has(origin); | ||
if (willCreate) { | ||
const sessionItem = this.connectHttp2(origin, extraOptions); | ||
const { promise } = sessionItem; | ||
// Handle session closure (delete from store) | ||
promise | ||
.then(session => { | ||
session.once("close", () => this.disconnect(origin, session)); | ||
session.once("goaway", (_errorCode, _lastStreamID, _opaqueData) => { | ||
utils_http2_1.setGotGoaway(session); | ||
this.releaseSession(origin); | ||
}); | ||
}) | ||
.catch(() => { | ||
if (sessionItem.session) | ||
this.disconnect(origin, sessionItem.session); | ||
createHttp2(origin, onGotGoaway, extraOptions) { | ||
const sessionItem = this.connectHttp2(origin, extraOptions); | ||
const { promise } = sessionItem; | ||
// Handle session closure (delete from store) | ||
promise | ||
.then(session => { | ||
session.once("close", () => this.disconnect(origin, session)); | ||
session.once("goaway", (_errorCode, _lastStreamID, _opaqueData) => { | ||
utils_http2_1.setGotGoaway(session); | ||
onGotGoaway(); | ||
this.releaseSession(origin); | ||
}); | ||
this._h2sessions.set(origin, sessionItem); | ||
} | ||
const { promise: session, ref, unref } = this._h2sessions.get(origin); | ||
if (!willCreate) | ||
// This was re-used | ||
ref(); | ||
// Avoid potential double-clean races | ||
let hasCleanedUp = false; | ||
const cleanup = () => { | ||
if (hasCleanedUp) | ||
return; | ||
hasCleanedUp = true; | ||
unref(); | ||
}; | ||
}) | ||
.catch(() => { | ||
if (sessionItem.session) | ||
this.disconnect(origin, sessionItem.session); | ||
}); | ||
this._h2sessions.set(origin, sessionItem); | ||
const { promise: session, ref, unref } = sessionItem; | ||
return { | ||
cleanup, | ||
didCreate: willCreate, | ||
ref, | ||
unref, | ||
session, | ||
@@ -219,3 +204,3 @@ }; | ||
sessionRefs.unref = () => { | ||
if (session.destroyed) | ||
if (utils_http2_1.isDestroyed(session)) | ||
return; | ||
@@ -236,3 +221,3 @@ --monkeySession.__fetch_h2_refcount; | ||
makeRefs(session); | ||
session.on("stream", aGuard((stream, headers) => this.handlePush(origin, stream, headers, sessionRefs.ref, sessionRefs.unref))); | ||
session.on("stream", aGuard((stream, headers) => this.handlePush(origin, stream, headers, () => sessionRefs.ref(), () => sessionRefs.unref()))); | ||
session.once("close", () => reject(utils_1.makeOkError(makeError()))); | ||
@@ -243,6 +228,7 @@ session.once("timeout", () => reject(makeConnectionTimeout())); | ||
return { | ||
firstOrigin: origin, | ||
promise, | ||
ref: sessionRefs.ref, | ||
ref: () => sessionRefs.ref(), | ||
session, | ||
unref: sessionRefs.unref, | ||
unref: () => sessionRefs.unref(), | ||
}; | ||
@@ -249,0 +235,0 @@ } |
@@ -5,6 +5,8 @@ /// <reference types="node" /> | ||
import { HttpProtocols } from "./core"; | ||
import { AltNameMatch } from "./san"; | ||
export interface HttpsSocketResult { | ||
socket: TLSSocket; | ||
protocol: "http1" | "http2"; | ||
altNameMatch: AltNameMatch; | ||
} | ||
export declare function connectTLS(host: string, port: string, protocols: ReadonlyArray<HttpProtocols>, connOpts: SecureClientSessionOptions): Promise<HttpsSocketResult>; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tls_1 = require("tls"); | ||
const san_1 = require("./san"); | ||
const alpnProtocols = { | ||
@@ -29,2 +30,4 @@ http1: Buffer.from("\x08http/1.1"), | ||
const { authorized, authorizationError, alpnProtocol = "" } = socket; | ||
const cert = socket.getPeerCertificate(); | ||
const altNameMatch = san_1.parseOrigin(cert); | ||
if (!authorized && opts.rejectUnauthorized !== false) | ||
@@ -36,8 +39,16 @@ return reject(authorizationError); | ||
if (_protocols.length === 1) | ||
return resolve({ protocol: _protocols[0], socket }); | ||
return resolve({ | ||
altNameMatch, | ||
protocol: _protocols[0], | ||
socket, | ||
}); | ||
else | ||
return resolve({ protocol: "http1", socket }); | ||
return resolve({ | ||
altNameMatch, | ||
protocol: "http1", | ||
socket, | ||
}); | ||
} | ||
const protocol = alpnProtocol === "h2" ? "http2" : "http1"; | ||
resolve({ socket, protocol }); | ||
resolve({ socket, protocol, altNameMatch }); | ||
}); | ||
@@ -44,0 +55,0 @@ socket.once("error", reject); |
@@ -31,2 +31,6 @@ /// <reference types="node" /> | ||
private _http1Options; | ||
private _httpsFunnel; | ||
private _http1Funnel; | ||
private _http2Funnel; | ||
private _originCache; | ||
constructor(opts?: Partial<ContextOptions>); | ||
@@ -41,6 +45,7 @@ setup(opts?: Partial<ContextOptions>): void; | ||
disconnectAll(): Promise<void>; | ||
private retryFetch; | ||
private retryableFetch; | ||
private connectSequenciallyTLS; | ||
private getHttp1; | ||
private getOrCreateHttp2; | ||
private getHttp2; | ||
private parseInput; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const url_1 = require("url"); | ||
const already_1 = require("already"); | ||
const context_http1_1 = require("./context-http1"); | ||
@@ -14,2 +15,3 @@ const context_http2_1 = require("./context-http2"); | ||
const utils_1 = require("./utils"); | ||
const origin_cache_1 = require("./origin-cache"); | ||
function makeDefaultUserAgent() { | ||
@@ -26,2 +28,6 @@ const name = `fetch-h2/${version_1.version} (+https://github.com/grantila/fetch-h2)`; | ||
constructor(opts) { | ||
this._httpsFunnel = already_1.funnel(); | ||
this._http1Funnel = already_1.funnel(); | ||
this._http2Funnel = already_1.funnel(); | ||
this._originCache = new origin_cache_1.default(); | ||
this._userAgent = ""; | ||
@@ -76,2 +82,33 @@ this._overwriteUserAgent = false; | ||
async fetch(input, init) { | ||
return this.retryFetch(input, init, 0); | ||
} | ||
async disconnect(url) { | ||
const { origin } = this.parseInput(url); | ||
const sessions = this._originCache.getAny(origin); | ||
sessions.forEach(({ session }) => { | ||
this._originCache.delete(session); | ||
}); | ||
await Promise.all([ | ||
this.h1Context.disconnect(url), | ||
this.h2Context.disconnect(url), | ||
]); | ||
} | ||
async disconnectAll() { | ||
this._originCache.clear(); | ||
await Promise.all([ | ||
this.h1Context.disconnectAll(), | ||
this.h2Context.disconnectAll(), | ||
]); | ||
} | ||
async retryFetch(input, init, count) { | ||
++count; | ||
return this.retryableFetch(input, init) | ||
.catch(already_1.specific(core_1.RetryError, err => { | ||
// TODO: Implement a more robust retry logic | ||
if (count > 10) | ||
throw err; | ||
return this.retryFetch(input, init, count); | ||
})); | ||
} | ||
async retryableFetch(input, init) { | ||
const { hostname, origin, port, protocol, url } = this.parseInput(input); | ||
@@ -102,11 +139,19 @@ // Rewrite url to get rid of "http1://" and "http2://" | ||
}; | ||
const doFetchHttp2 = () => { | ||
const sessionGetterHttp2 = { | ||
get: (url) => this.getHttp2(url), | ||
...makeSimpleSession("http2"), | ||
}; | ||
return fetch_http2_1.fetch(sessionGetterHttp2, request, init); | ||
const doFetchHttp2 = async (cacheableSession) => { | ||
const { session, unref } = cacheableSession; | ||
const cleanup = already_1.once(unref); | ||
try { | ||
const sessionGetterHttp2 = { | ||
get: () => ({ session, cleanup }), | ||
...makeSimpleSession("http2"), | ||
}; | ||
return await fetch_http2_1.fetch(sessionGetterHttp2, request, init); | ||
} | ||
catch (err) { | ||
cleanup(); | ||
throw err; | ||
} | ||
}; | ||
const tryWaitForHttp1 = async () => { | ||
const { socket: freeHttp1Socket, cleanup, shouldCreateNew } = this.h1Context.getFreeSocketForOrigin(origin); | ||
const tryWaitForHttp1 = async (session) => { | ||
const { socket: freeHttp1Socket, cleanup, shouldCreateNew } = this.h1Context.getFreeSocketForSession(session); | ||
if (freeHttp1Socket) | ||
@@ -117,3 +162,3 @@ return doFetchHttp1(freeHttp1Socket, cleanup); | ||
// freed. | ||
const { socket, cleanup } = await this.h1Context.waitForSocket(origin); | ||
const { socket, cleanup } = await this.h1Context.waitForSocketBySession(session); | ||
return doFetchHttp1(socket, cleanup); | ||
@@ -123,59 +168,82 @@ } | ||
if (protocol === "http1") { | ||
// Plain text HTTP/1(.1) | ||
const resp = await tryWaitForHttp1(); | ||
if (resp) | ||
return resp; | ||
const socket = await this.h1Context.makeNewConnection(url); | ||
const cleanup = this.h1Context.addUsedSocket(origin, socket); | ||
return doFetchHttp1(socket, cleanup); | ||
return this._http1Funnel(async (shouldRetry, retry, shortcut) => { | ||
var _a, _b; | ||
if (shouldRetry()) | ||
return retry(); | ||
// Plain text HTTP/1(.1) | ||
const cacheItem = this._originCache.get("http1", origin); | ||
const session = (_b = (_a = cacheItem) === null || _a === void 0 ? void 0 : _a.session, (_b !== null && _b !== void 0 ? _b : 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") { | ||
// Plain text HTTP/2 | ||
return doFetchHttp2(); | ||
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" | ||
{ | ||
// If we already have a session/socket open to this origin, | ||
// re-use it | ||
if (this.h2Context.hasOrigin(origin)) | ||
return doFetchHttp2(); | ||
const resp = await tryWaitForHttp1(); | ||
if (resp) | ||
return resp; | ||
// TODO: Make queue for subsequent fetch requests to the same | ||
// origin, so they can re-use the http2 session, or http1 | ||
// pool once we know what protocol will be used. | ||
// This must apply to plain-text http1 too. | ||
// Use ALPN to figure out protocol lazily | ||
const { protocol, socket } = await context_https_1.connectTLS(hostname, port, core_1.getByOrigin(this._httpsProtocols, origin), core_1.getByOrigin(this._sessionOptions, origin)); | ||
if (protocol === "http2") { | ||
// Convert socket into http2 session, this will ref (*) | ||
const { cleanup } = await this.h2Context.getOrCreateHttp2(origin, { | ||
createConnection: () => socket, | ||
}); | ||
// Session now lingering, it will be re-used by the next get() | ||
const ret = doFetchHttp2(); | ||
// Unref lingering ref | ||
cleanup(); | ||
return ret; | ||
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, _c; | ||
const cacheItem = (_a = this._originCache.get("https2", origin), (_a !== null && _a !== void 0 ? _a : this._originCache.get("https1", origin))); | ||
if (cacheItem) { | ||
if (cacheItem.protocol === "https1") { | ||
const resp = await tryWaitForHttp1(cacheItem.session); | ||
if (resp) | ||
return resp; | ||
} | ||
else // protocol === "http1" | ||
{ | ||
const cleanup = this.h1Context.addUsedSocket(origin, socket); | ||
return doFetchHttp1(socket, cleanup); | ||
else if (cacheItem.protocol === "https2") { | ||
cacheItem.session.ref(); | ||
return doFetchHttp2(cacheItem.session); | ||
} | ||
} | ||
// Use ALPN to figure out protocol lazily | ||
const { protocol, socket, altNameMatch } = await context_https_1.connectTLS(hostname, port, core_1.getByOrigin(this._httpsProtocols, origin), core_1.getByOrigin(this._sessionOptions, origin)); | ||
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); | ||
shortcut(); | ||
// Session now lingering, it will be re-used by the next get() | ||
return doFetchHttp2(cacheableSession); | ||
} | ||
else // protocol === "http1" | ||
{ | ||
const session = (_c = (_b = cacheItem) === null || _b === void 0 ? void 0 : _b.session, (_c !== null && _c !== void 0 ? _c : 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); | ||
const cleanup = this.h1Context.addUsedSocket(session, socket); | ||
shortcut(); | ||
return doFetchHttp1(socket, cleanup); | ||
} | ||
} | ||
async disconnect(url) { | ||
await Promise.all([ | ||
this.h1Context.disconnect(url), | ||
this.h2Context.disconnect(url), | ||
]); | ||
} | ||
async disconnectAll() { | ||
await Promise.all([ | ||
this.h1Context.disconnectAll(), | ||
this.h2Context.disconnectAll(), | ||
]); | ||
} | ||
getHttp1(url, socket, request, rejectUnauthorized) { | ||
@@ -187,19 +255,2 @@ return this.h1Context.connect(new url_1.URL(url), { | ||
} | ||
getOrCreateHttp2(origin, created = false) { | ||
const { didCreate, session, cleanup } = this.h2Context.getOrCreateHttp2(origin); | ||
return session | ||
.catch(err => { | ||
if (didCreate || created) | ||
// Created in this request, forward error | ||
throw err; | ||
// Not created in this request, try again | ||
return this.getOrCreateHttp2(origin, true) | ||
.then(({ session }) => session); | ||
}) | ||
.then(session => ({ session, cleanup })); | ||
} | ||
getHttp2(url) { | ||
const { origin } = typeof url === "string" ? new url_1.URL(url) : url; | ||
return this.getOrCreateHttp2(origin); | ||
} | ||
parseInput(input) { | ||
@@ -206,0 +257,0 @@ const { hostname, origin, port, protocol, url } = utils_1.parseInput(typeof input !== "string" ? input.url : input); |
@@ -68,2 +68,5 @@ /// <reference types="node" /> | ||
} | ||
export declare class RetryError extends Error { | ||
constructor(message: string); | ||
} | ||
export declare type DecodeFunction = (stream: NodeJS.ReadableStream) => NodeJS.ReadableStream; | ||
@@ -96,3 +99,3 @@ export interface Decoder { | ||
export interface SimpleSessionHttp2Session { | ||
session: ClientHttp2Session; | ||
session: Promise<ClientHttp2Session>; | ||
cleanup: () => void; | ||
@@ -104,3 +107,3 @@ } | ||
export interface SimpleSessionHttp2 extends SimpleSession { | ||
get(url: string): Promise<SimpleSessionHttp2Session>; | ||
get(): SimpleSessionHttp2Session; | ||
} |
@@ -24,2 +24,9 @@ "use strict"; | ||
exports.TimeoutError = TimeoutError; | ||
class RetryError extends Error { | ||
constructor(message) { | ||
super(message); | ||
Object.setPrototypeOf(this, RetryError.prototype); | ||
} | ||
} | ||
exports.RetryError = RetryError; | ||
function getByOrigin(val, origin) { | ||
@@ -26,0 +33,0 @@ return typeof val === "function" |
@@ -143,4 +143,5 @@ "use strict"; | ||
function cleanup() { | ||
if (timeoutInfo && timeoutInfo.clear) | ||
timeoutInfo.clear(); | ||
var _a, _b, _c, _d, _e; | ||
(_c = (_a = timeoutInfo) === null || _a === void 0 ? void 0 : (_b = _a).clear) === null || _c === void 0 ? void 0 : _c.call(_b); | ||
(_e = (_d = timeoutInfo) === null || _d === void 0 ? void 0 : _d.promise) === null || _e === void 0 ? void 0 : _e.catch(_err => { }); | ||
if (signal && abortHandler) | ||
@@ -147,0 +148,0 @@ signal.removeListener("abort", abortHandler); |
@@ -121,3 +121,6 @@ "use strict"; | ||
.then(readable => { | ||
readable.pipe(req); | ||
utils_1.pipeline(readable, req) | ||
.catch(_err => { | ||
// TODO: Implement error handling | ||
}); | ||
}); | ||
@@ -124,0 +127,0 @@ return response; |
@@ -22,29 +22,40 @@ "use strict"; | ||
const { raceConditionedGoaway } = extra; | ||
const streamPromise = session.get(url); | ||
const streamPromise = session.get(); | ||
async function doFetch() { | ||
const { session: h2session, cleanup: socketCleanup } = await streamPromise; | ||
const stream = h2session.request(headersToSend, { endStream }); | ||
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) && | ||
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; | ||
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 = callguard_1.syncGuard(reject, { catchAsync: true }); | ||
const tryRetryOnGoaway = () => { | ||
// 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) && | ||
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; | ||
}; | ||
stream.on("aborted", guard((..._whatever) => { | ||
@@ -58,3 +69,3 @@ reject(fetch_common_1.makeAbortedError()); | ||
err.message.includes("NGHTTP2_REFUSED_STREAM")) { | ||
if (tryRetryOnGoaway()) | ||
if (tryRetryOnGoaway(resolve)) | ||
return; | ||
@@ -67,3 +78,3 @@ } | ||
endStream) { | ||
if (tryRetryOnGoaway()) | ||
if (tryRetryOnGoaway(resolve)) | ||
return; | ||
@@ -170,11 +181,10 @@ } | ||
.then(readable => { | ||
readable.pipe(stream); | ||
utils_1.pipeline(readable, stream) | ||
.catch(_err => { | ||
// TODO: Implement error handling | ||
}); | ||
}); | ||
return response; | ||
} | ||
return fetch_common_1.handleSignalAndTimeout(signalPromise, timeoutInfo, cleanup, doFetch, () => { | ||
streamPromise | ||
.then(({ cleanup }) => cleanup()) | ||
.catch(_err => { }); | ||
}); | ||
return fetch_common_1.handleSignalAndTimeout(signalPromise, timeoutInfo, cleanup, doFetch, streamPromise.cleanup); | ||
} | ||
@@ -181,0 +191,0 @@ function fetch(session, input, init) { |
@@ -1,1 +0,1 @@ | ||
export declare const version = "2.3.0"; | ||
export declare const version = "2.4.0"; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.version = "2.3.0"; | ||
exports.version = "2.4.0"; | ||
//# sourceMappingURL=version.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const http2_1 = require("http2"); | ||
const stream_1 = require("stream"); | ||
const zlib_1 = require("zlib"); | ||
@@ -127,8 +128,11 @@ const { HTTP2_HEADER_LOCATION, HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_CONTENT_ENCODING, HTTP2_HEADER_CONTENT_LENGTH, } = http2_1.constants; | ||
return stream; | ||
const handleStreamResult = (_err) => { | ||
// TODO: Add error handling | ||
}; | ||
const decoders = { | ||
deflate: (stream) => stream.pipe(zlib_1.createInflate()), | ||
gzip: (stream) => stream.pipe(zlib_1.createGunzip()), | ||
deflate: (stream) => stream_1.pipeline(stream, zlib_1.createInflate(), handleStreamResult), | ||
gzip: (stream) => stream_1.pipeline(stream, zlib_1.createGunzip(), handleStreamResult), | ||
}; | ||
if (utils_1.hasBuiltinBrotli()) { | ||
decoders.br = (stream) => stream.pipe(zlib_1.createBrotliDecompress()); | ||
decoders.br = (stream) => stream_1.pipeline(stream, zlib_1.createBrotliDecompress(), handleStreamResult); | ||
} | ||
@@ -135,0 +139,0 @@ contentDecoders.forEach(decoder => { |
@@ -0,1 +1,4 @@ | ||
/// <reference types="node" /> | ||
import * as stream from "stream"; | ||
export declare const pipeline: typeof stream.pipeline.__promisify__; | ||
export declare function arrayify<T>(value: T | Array<T> | Readonly<T> | ReadonlyArray<T> | undefined | null): Array<T>; | ||
@@ -2,0 +5,0 @@ export declare function parseLocation(location: string | Array<string> | undefined, origin: string): string | null; |
@@ -5,2 +5,5 @@ "use strict"; | ||
const zlib_1 = require("zlib"); | ||
const util_1 = require("util"); | ||
const stream = require("stream"); | ||
exports.pipeline = util_1.promisify(stream.pipeline); | ||
function arrayify(value) { | ||
@@ -7,0 +10,0 @@ if (value != null && Array.isArray(value)) |
@@ -5,2 +5,4 @@ "use strict"; | ||
const __1 = require(".."); | ||
const stream_1 = require("stream"); | ||
// tslint:disable no-console | ||
async function work() { | ||
@@ -23,8 +25,11 @@ const args = process.argv.slice(2); | ||
}); | ||
const readable = await response.readable(); | ||
readable.pipe(process.stdout); | ||
stream_1.pipeline(await response.readable(), process.stdout, err => { | ||
if (!err) | ||
return; | ||
console.error("Failed to fetch", err.stack); | ||
process.exit(1); | ||
}); | ||
} | ||
work() | ||
// tslint:disable-next-line | ||
.catch(err => { console.error(err.stack); }); | ||
.catch(err => { console.error(err, err.stack); }); | ||
//# sourceMappingURL=index.js.map |
@@ -21,3 +21,3 @@ "use strict"; | ||
const body = { foo: "bar" }; | ||
const { stdout } = await execa(script, ["GET", url, version, "insecure"], { input: JSON.stringify(body) }); | ||
const { stdout } = await execa(script, ["GET", url, version, "insecure"], { input: JSON.stringify(body), stderr: 'inherit' }); | ||
const responseBody = JSON.parse(stdout); | ||
@@ -24,0 +24,0 @@ expect(responseBody["user-agent"]).toContain("fetch-h2/"); |
@@ -27,3 +27,3 @@ "use strict"; | ||
return async () => { | ||
const { fetch, disconnectAll } = index_1.context({ | ||
const { fetch } = index_1.context({ | ||
httpsProtocols: protos, | ||
@@ -34,3 +34,5 @@ session: certs | ||
}); | ||
await fn(fetch).then(...already_1.Finally(disconnectAll)); | ||
// Disconnection shouldn't be necessary, fetch-h2 should unref | ||
// the sockets correctly. | ||
await fn(fetch); | ||
}; | ||
@@ -98,2 +100,3 @@ } | ||
const redirectedTo = responseSet.headers.get("location"); | ||
await responseSet.text(); | ||
const response = await fetch(baseHost + redirectedTo); | ||
@@ -100,0 +103,0 @@ const data = await response.json(); |
@@ -6,2 +6,3 @@ "use strict"; | ||
const https_1 = require("https"); | ||
const utils_1 = require("../../lib/utils"); | ||
const crypto_1 = require("crypto"); | ||
@@ -68,3 +69,3 @@ const zlib_1 = require("zlib"); | ||
sendHeaders(responseHeaders); | ||
request.pipe(response); | ||
utils_1.pipeline(request, response); | ||
} | ||
@@ -100,3 +101,3 @@ else if (path === "/set-cookie") { | ||
sendHeaders(responseHeaders); | ||
request.pipe(response); | ||
utils_1.pipeline(request, response); | ||
} | ||
@@ -132,3 +133,3 @@ catch (err) | ||
}); | ||
request.pipe(hash); | ||
utils_1.pipeline(request, hash); | ||
} | ||
@@ -155,5 +156,5 @@ else if (path.startsWith("/compressed/")) { | ||
if (encoder) | ||
request.pipe(encoder).pipe(response); | ||
utils_1.pipeline(request, encoder, response); | ||
else | ||
request.pipe(response); | ||
utils_1.pipeline(request, response); | ||
} | ||
@@ -160,0 +161,0 @@ else if (path.startsWith("/delay/")) { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const http2_1 = require("http2"); | ||
const utils_1 = require("../../lib/utils"); | ||
const crypto_1 = require("crypto"); | ||
@@ -63,3 +64,3 @@ const zlib_1 = require("zlib"); | ||
stream.respond(responseHeaders); | ||
stream.pipe(stream); | ||
utils_1.pipeline(stream, stream); | ||
} | ||
@@ -93,3 +94,3 @@ else if (path === "/set-cookie") { | ||
stream.respond(responseHeaders); | ||
stream.pipe(stream); | ||
utils_1.pipeline(stream, stream); | ||
} | ||
@@ -132,3 +133,3 @@ catch (err) | ||
}); | ||
stream.pipe(hash); | ||
utils_1.pipeline(stream, hash); | ||
} | ||
@@ -175,5 +176,5 @@ else if (path === "/push") { | ||
if (encoder) | ||
stream.pipe(encoder).pipe(stream); | ||
utils_1.pipeline(stream, encoder, stream); | ||
else | ||
stream.pipe(stream); | ||
utils_1.pipeline(stream, stream); | ||
} | ||
@@ -180,0 +181,0 @@ else if (path.startsWith("/goaway")) { |
{ | ||
"name": "fetch-h2", | ||
"version": "2.3.0", | ||
"version": "2.4.0", | ||
"description": "HTTP/1+2 Fetch API client for Node.js", | ||
@@ -25,3 +25,3 @@ "author": "Gustaf Räntilä", | ||
"lint": "node_modules/.bin/tslint --project .", | ||
"jest:core": "node_modules/.bin/jest --forceExit --detectOpenHandles --coverage", | ||
"jest:core": "node_modules/.bin/jest --detectOpenHandles --coverage", | ||
"jest:fast": "yarn jest:core --config jest.config.unit.js $@", | ||
@@ -37,4 +37,2 @@ "jest:integration": "node_modules/.bin/compd -f test/docker-compose.yaml yarn jest:core", | ||
"makecerts": "openssl req -x509 -nodes -days 7300 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem", | ||
"travis-deploy-once": "travis-deploy-once", | ||
"semantic-release": "semantic-release", | ||
"cz": "git-cz" | ||
@@ -58,6 +56,6 @@ }, | ||
"@types/execa": "^2.0.0", | ||
"@types/from2": "2.x", | ||
"@types/jest": "25.1.0", | ||
"@types/node": "13.5.1", | ||
"@types/through2": "2.x", | ||
"@types/from2": "^2.3.0", | ||
"@types/jest": "^25.1.1", | ||
"@types/node": "^13.7.0", | ||
"@types/through2": "^2.0.34", | ||
"commitizen": "^4.0.3", | ||
@@ -68,19 +66,19 @@ "compd": "^1.3.7", | ||
"execa": "^4.0.0", | ||
"from2": "2.x", | ||
"jest": "25.1.0", | ||
"from2": "^2.3.0", | ||
"jest": "^25.1.0", | ||
"mkcert": "^1.2.0", | ||
"rimraf": "^3.0.1", | ||
"ts-jest": "25.0.0", | ||
"ts-node": "8.6.2", | ||
"tslint": "6.0.0", | ||
"typescript": "3.7.5" | ||
"ts-jest": "^25.1.0", | ||
"ts-node": "^8.6.2", | ||
"tslint": "^6.0.0", | ||
"typescript": "^3.7.5" | ||
}, | ||
"dependencies": { | ||
"@types/tough-cookie": "2.x", | ||
"already": "1.10.1", | ||
"callguard": "1.x", | ||
"get-stream": "5.x", | ||
"through2": "3.x", | ||
"to-arraybuffer": "1.x", | ||
"tough-cookie": "3.x" | ||
"@types/tough-cookie": "^2.3.6", | ||
"already": "^1.10.1", | ||
"callguard": "^1.2.1", | ||
"get-stream": "^5.1.0", | ||
"through2": "^3.0.1", | ||
"to-arraybuffer": "^1.0.1", | ||
"tough-cookie": "^3.0.1" | ||
}, | ||
@@ -87,0 +85,0 @@ "config": { |
@@ -31,3 +31,5 @@ [![npm version][npm-image]][npm-url] | ||
Since 2.4.0, `fetch-h2` has full TLS SAN (Subject Alternative Name) support. | ||
# API | ||
@@ -34,0 +36,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
387561
114
5001
397
+ Addedalready@1.13.2(transitive)
- Removedalready@1.10.1(transitive)
Updated@types/tough-cookie@^2.3.6
Updatedalready@^1.10.1
Updatedcallguard@^1.2.1
Updatedget-stream@^5.1.0
Updatedthrough2@^3.0.1
Updatedto-arraybuffer@^1.0.1
Updatedtough-cookie@^3.0.1