diff --git a/src/apiv2.spec.ts b/src/apiv2.spec.ts index 1120b2cb4ed..4e79c09b6c5 100644 --- a/src/apiv2.spec.ts +++ b/src/apiv2.spec.ts @@ -219,6 +219,55 @@ describe("apiv2", () => { expect(nock.isDone()).to.be.true; }); + it("should not retry when retryOnPrematureClose is false", async () => { + nock("https://example.com").patch("/path/to/foo").once().replyWithError({ + message: + "Invalid response body while trying to fetch https://example.com/path/to/foo: Premature close", + code: "ERR_STREAM_PREMATURE_CLOSE", + }); + // If a retry fires, this second interceptor would be consumed + const retryScope = nock("https://example.com") + .patch("/path/to/foo") + .once() + .reply(200, { ok: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + await expect( + c.request({ + method: "PATCH", + path: "/path/to/foo", + body: { foo: "bar" }, + retries: 1, + retryMinTimeout: 10, + retryMaxTimeout: 15, + retryOnPrematureClose: false, + }), + ).to.eventually.be.rejectedWith(FirebaseError); + expect(retryScope.isDone()).to.be.false; // proves no retry occurred + nock.cleanAll(); + }); + + it("should retry a request by default after a premature close error", async () => { + nock("https://example.com").post("/path/to/foo").once().replyWithError({ + message: + "Invalid response body while trying to fetch https://example.com/path/to/foo: Premature close", + code: "ERR_STREAM_PREMATURE_CLOSE", + }); + nock("https://example.com").post("/path/to/foo").once().reply(200, { ok: true }); + + const c = new Client({ urlPrefix: "https://example.com" }); + const r = await c.request({ + method: "POST", + path: "/path/to/foo", + body: { foo: "bar" }, + retries: 1, + retryMinTimeout: 10, + retryMaxTimeout: 15, + }); + expect(r.status).to.equal(200); + expect(nock.isDone()).to.be.true; + }); + it("should not allow resolving on http error when streaming", async () => { const c = new Client({ urlPrefix: "https://example.com" }); const r = c.request({ diff --git a/src/apiv2.ts b/src/apiv2.ts index 386baf7ae68..6d86a311e7d 100644 --- a/src/apiv2.ts +++ b/src/apiv2.ts @@ -94,6 +94,8 @@ interface ClientHandlingOptions { retryMinTimeout?: number; /** Maximum timeout between retries. Defaults to 5s. */ retryMaxTimeout?: number; + /** Set to false to disable the premature-close keep-alive retry for this request. Defaults to true. */ + retryOnPrematureClose?: boolean; } export type ClientRequestOptions = RequestOptions & ClientVerbOptions; @@ -581,7 +583,8 @@ export class Client { !disabledKeepAlive && bodyReplayable && !proxyURIFromEnv() && - isPrematureCloseError(err) + isPrematureCloseError(err) && + options.retryOnPrematureClose !== false ) { disabledKeepAlive = true; fetchOptions.agent = noKeepAliveAgent; diff --git a/src/gcp/cloudfunctions.ts b/src/gcp/cloudfunctions.ts index ff766ef5189..27faa0a7136 100644 --- a/src/gcp/cloudfunctions.ts +++ b/src/gcp/cloudfunctions.ts @@ -244,6 +244,7 @@ export async function createFunction( const res = await client.post, CloudFunction>( endpoint, cloudFunction, + { retryOnPrematureClose: false }, ); return { name: res.body.name, @@ -412,6 +413,7 @@ export async function updateFunction( queryParams: { updateMask: fieldMasks.join(","), }, + retryOnPrematureClose: false, }, ); return { @@ -431,7 +433,7 @@ export async function updateFunction( export async function deleteFunction(name: string): Promise { const endpoint = `/${name}`; try { - const res = await client.delete(endpoint); + const res = await client.delete(endpoint, { retryOnPrematureClose: false }); return { done: false, name: res.body.name, diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index e3d263c7000..c4f9f0cfb3e 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -321,7 +321,7 @@ export async function createFunction(cloudFunction: InputCloudFunction): Promise const res = await client.post( components.join("/"), cloudFunction, - { queryParams: { functionId } }, + { queryParams: { functionId }, retryOnPrematureClose: false }, ); return res.body; } catch (err: any) { @@ -415,7 +415,7 @@ export async function updateFunction(cloudFunction: InputCloudFunction): Promise const res = await client.patch( cloudFunction.name, cloudFunction, - { queryParams }, + { queryParams, retryOnPrematureClose: false }, ); return res.body; } catch (err: any) { @@ -429,7 +429,7 @@ export async function updateFunction(cloudFunction: InputCloudFunction): Promise */ export async function deleteFunction(cloudFunction: string): Promise { try { - const res = await client.delete(cloudFunction); + const res = await client.delete(cloudFunction, { retryOnPrematureClose: false }); return res.body; } catch (err: any) { throw functionsOpLogReject({ name: cloudFunction } as InputCloudFunction, "update", err);