Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a8de4b3
fix(v2): validate timeoutSeconds per trigger type
kyungseopk1m Apr 20, 2026
14e4b08
docs: reference PR #1874 in CHANGELOG entry
kyungseopk1m Apr 20, 2026
e027119
test: add suite covering all v2 providers
Apr 23, 2026
d348297
fix: handle case where timeoutSeconds is NaN
Apr 23, 2026
3355592
style: format CHANGELOG.md with prettier
Apr 23, 2026
7c306a4
fix: don't allow timeoutSeconds to be zero
May 11, 2026
aac55eb
fix: allow only valid non-number cases
May 11, 2026
c50da50
tests: update tests
May 11, 2026
0f5da16
Merge branch 'pr-1874' of https://github.com/firebase/firebase-functi…
May 11, 2026
ef0f965
fix(v2): validate timeoutSeconds per trigger type
kyungseopk1m Apr 20, 2026
9e8b922
docs: reference PR #1874 in CHANGELOG entry
kyungseopk1m Apr 20, 2026
478d3bb
test: add suite covering all v2 providers
Apr 23, 2026
b0b37ec
fix: handle case where timeoutSeconds is NaN
Apr 23, 2026
68b4581
fix: don't allow timeoutSeconds to be zero
May 11, 2026
6180b78
fix: allow only valid non-number cases
May 11, 2026
20614e6
tests: update tests
May 11, 2026
14e28bf
style: format CHANGELOG.md with prettier
Apr 23, 2026
bf888d2
chore: update CHANGELOG
May 14, 2026
1642829
tests: update tests and cover edge cases
May 14, 2026
772e9de
fix: allow 0 second timeout
May 14, 2026
374295a
Merge branch 'pr-1874' of https://github.com/firebase/firebase-functi…
May 14, 2026
507bba4
feat: add identity timeout kind
May 15, 2026
df4687e
fix: enforce 7 second max timeout for identity blocking functions
May 15, 2026
5ab92f2
test: add tests for new identity kind
May 15, 2026
22244c9
Merge branch 'master' into pr-1874
May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Validate literal `timeoutSeconds` values per v2 trigger type (0-540s for events, 0-3600s for HTTPS/callable, 0-1800s for task queues) so misconfigured values fail at function-definition or manifest-extraction time instead of at deploy time. (#1877)
- fix(v1): Call onInit for schedule.onRun functions (#1801)
180 changes: 178 additions & 2 deletions spec/v2/options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@
// SOFTWARE.

import { expect } from "chai";
import { defineJsonSecret, defineSecret } from "../../src/params";
import { GlobalOptions, optionsToEndpoint, RESET_VALUE } from "../../src/v2/options";
import { defineInt, defineJsonSecret, defineSecret } from "../../src/params";
import {
assertTimeoutSecondsValid,
GlobalOptions,
optionsToEndpoint,
optionsToTriggerAnnotations,
RESET_VALUE,
setGlobalOptions,
} from "../../src/v2/options";

describe("GlobalOptions", () => {
it("should accept all valid secret types in secrets array (type test)", () => {
Expand Down Expand Up @@ -92,3 +99,172 @@ describe("optionsToEndpoint", () => {
expect(endpoint.vpc).to.equal(RESET_VALUE);
});
});

describe("assertTimeoutSecondsValid", () => {
afterEach(() => {
setGlobalOptions({});
});

it("is a no-op when timeoutSeconds is undefined", () => {
expect(() => assertTimeoutSecondsValid({}, "event")).to.not.throw();
expect(() => assertTimeoutSecondsValid({}, "https")).to.not.throw();
expect(() => assertTimeoutSecondsValid({}, "task")).to.not.throw();
expect(() => assertTimeoutSecondsValid({}, "identity")).to.not.throw();
});

it("accepts values within each kind's limit", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 540 }, "event")).to.not.throw();
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 3600 }, "https")).to.not.throw();
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 1800 }, "task")).to.not.throw();
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 0 }, "event")).to.not.throw();
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 1 }, "event")).to.not.throw();
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 7 }, "identity")).to.not.throw();
});

it("throws when timeoutSeconds exceeds the event-handler limit", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 3600 }, "event")).to.throw(
/between 0 and 540 for event-handling functions/
);
});

it("throws when timeoutSeconds exceeds the HTTPS limit", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 3601 }, "https")).to.throw(
/between 0 and 3600 for HTTPS and callable functions/
);
});

it("throws when timeoutSeconds exceeds the task-queue limit", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 1801 }, "task")).to.throw(
/between 0 and 1800 for task queue functions/
);
});

it("throws when timeoutSeconds exceeds the identity limit", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 8 }, "identity")).to.throw(
/between 0 and 7 for blocking functions/
);
});

it("throws when timeoutSeconds is negative", () => {
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: -1 }, "event")).to.throw(
/between 0 and 540/
);
});

it("skips validation for Expression timeouts", () => {
const expr = { timeoutSeconds: defineInt("TIMEOUT") };
expect(() => assertTimeoutSecondsValid(expr, "event")).to.not.throw();
});

it("skips validation for RESET_VALUE timeouts", () => {
const opts = { timeoutSeconds: RESET_VALUE as unknown as number };
expect(() => assertTimeoutSecondsValid(opts, "event")).to.not.throw();
});

it("throws when timeoutSeconds has an invalid non-number type", () => {
const opts = { timeoutSeconds: "30" as unknown as number };
expect(() => assertTimeoutSecondsValid(opts, "event")).to.throw(
/must be a number, Expression, or RESET_VALUE/
);
});

it("throws when timeoutSeconds is null", () => {
const opts = { timeoutSeconds: null as unknown as number };
expect(() => assertTimeoutSecondsValid(opts, "event")).to.throw(
/must be a number, Expression, or RESET_VALUE/
);
});

it("throws when global timeoutSeconds has an invalid non-number type", () => {
setGlobalOptions({ timeoutSeconds: true as unknown as number });
expect(() => assertTimeoutSecondsValid({}, "event")).to.throw(
/must be a number, Expression, or RESET_VALUE/
);
});

it("falls back to the global timeoutSeconds when the function-level option is absent", () => {
setGlobalOptions({ timeoutSeconds: 3600 });
expect(() => assertTimeoutSecondsValid({}, "event")).to.throw(
/between 0 and 540 for event-handling functions/
);
expect(() => assertTimeoutSecondsValid({}, "https")).to.not.throw();
expect(() => assertTimeoutSecondsValid({}, "task")).to.throw();
expect(() => assertTimeoutSecondsValid({}, "identity")).to.throw();
});

it("prefers the function-level timeoutSeconds over the global one", () => {
setGlobalOptions({ timeoutSeconds: 60 });
expect(() => assertTimeoutSecondsValid({ timeoutSeconds: 1000 }, "event")).to.throw(
/between 0 and 540/
);
});

it("treats a function-level RESET_VALUE as a clear of an out-of-range global", () => {
setGlobalOptions({ timeoutSeconds: 3600 });
expect(() =>
assertTimeoutSecondsValid({ timeoutSeconds: RESET_VALUE as unknown as number }, "event")
).to.not.throw();
});
});

describe("optionsToEndpoint timeout validation", () => {
afterEach(() => {
setGlobalOptions({});
});

it("does not validate when kind is omitted (backwards compatibility)", () => {
expect(() => optionsToEndpoint({ timeoutSeconds: 9999 })).to.not.throw();
});

it("throws when kind is provided and timeoutSeconds exceeds the limit", () => {
expect(() => optionsToEndpoint({ timeoutSeconds: 3600 }, "event")).to.throw(
/between 0 and 540/
);
expect(() => optionsToEndpoint({ timeoutSeconds: 3601 }, "https")).to.throw(
/between 0 and 3600/
);
expect(() => optionsToEndpoint({ timeoutSeconds: 1801 }, "task")).to.throw(
/between 0 and 1800/
);
expect(() => optionsToEndpoint({ timeoutSeconds: 8 }, "identity")).to.throw(/between 0 and 7/);
});

it("is a no-op for in-range timeouts when kind is provided", () => {
expect(() => optionsToEndpoint({ timeoutSeconds: 540 }, "event")).to.not.throw();
expect(() => optionsToEndpoint({ timeoutSeconds: 3600 }, "https")).to.not.throw();
expect(() => optionsToEndpoint({ timeoutSeconds: 1800 }, "task")).to.not.throw();
expect(() => optionsToEndpoint({ timeoutSeconds: 7 }, "identity")).to.not.throw();
});
});

describe("optionsToTriggerAnnotations timeout validation", () => {
afterEach(() => {
setGlobalOptions({});
});

it("does not validate when kind is omitted (backwards compatibility)", () => {
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 9999 })).to.not.throw();
});

it("throws when kind is provided and timeoutSeconds exceeds the limit", () => {
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 3600 }, "event")).to.throw(
/between 0 and 540/
);
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 3601 }, "https")).to.throw(
/between 0 and 3600/
);
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 1801 }, "task")).to.throw(
/between 0 and 1800/
);
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 8 }, "identity")).to.throw(
/between 0 and 7/
);
});

it("is a no-op for in-range timeouts when kind is provided", () => {
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 540 }, "event")).to.not.throw();
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 3600 }, "https")).to.not.throw();
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 1800 }, "task")).to.not.throw();
expect(() => optionsToTriggerAnnotations({ timeoutSeconds: 7 }, "identity")).to.not.throw();
});
});
14 changes: 14 additions & 0 deletions spec/v2/providers/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ describe("onRequest", () => {
await runHandler(func, req);
expect(hello).to.equal("world");
});

it("rejects timeoutSeconds above the 3600s HTTPS limit", () => {
expect(() =>
https.onRequest({ timeoutSeconds: 3601 }, (_req, res) => {
res.end();
})
).to.throw(/between 0 and 3600 for HTTPS and callable functions/);
});
});

describe("onCall", () => {
Expand Down Expand Up @@ -605,6 +613,12 @@ describe("onCall", () => {
expect(hello).to.equal("world");
});

it("rejects timeoutSeconds above the 3600s HTTPS limit", () => {
expect(() => https.onCall({ timeoutSeconds: 3601 }, () => 42)).to.throw(
/between 0 and 3600 for HTTPS and callable functions/
);
});

describe("authPolicy", () => {
before(() => {
sinon.stub(debug, "isDebugFeatureEnabled").withArgs("skipTokenVerification").returns(true);
Expand Down
13 changes: 13 additions & 0 deletions spec/v2/providers/pubsub.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ describe("onMessagePublished", () => {
expect(res).to.equal("input");
});

it("rejects timeoutSeconds above the 540s event-handler limit", () => {
expect(() =>
pubsub.onMessagePublished({ topic: "topic", timeoutSeconds: 3600 }, () => 42)
).to.throw(/between 0 and 540 for event-handling functions/);
});

it("rejects a global timeoutSeconds above the 540s event-handler limit", () => {
options.setGlobalOptions({ timeoutSeconds: 3600 });
expect(() => pubsub.onMessagePublished("topic", () => 42)).to.throw(
/between 0 and 540 for event-handling functions/
);
});

it("should parse pubsub messages", async () => {
let json: unknown;
const messageJSON = {
Expand Down
8 changes: 8 additions & 0 deletions spec/v2/providers/storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,14 @@ describe("v2/storage", () => {
await storage.onObjectFinalized("bucket", () => null)(event);
expect(hello).to.equal("world");
});

it("rejects timeoutSeconds above the 540s event-handler limit on __endpoint access", () => {
const func = storage.onObjectFinalized(
{ bucket: "my-bucket", timeoutSeconds: 3600 },
() => 42
);
expect(() => func.__endpoint).to.throw(/between 0 and 540 for event-handling functions/);
});
});

describe("onObjectDeleted", () => {
Expand Down
6 changes: 6 additions & 0 deletions spec/v2/providers/tasks.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ describe("onTaskDispatched", () => {
expect(hello).to.equal("world");
});

it("rejects timeoutSeconds above the 1800s task-queue limit", () => {
expect(() => onTaskDispatched({ timeoutSeconds: 1801 }, () => null)).to.throw(
/between 0 and 1800 for task queue functions/
);
});

describe("v1-compatible getters", () => {
it("should provide v1-compatible context on the request object", async () => {
let capturedRequest: any;
Expand Down
Loading
Loading