From a8de4b3add1a09606b2a3abe4f5240363bb06850 Mon Sep 17 00:00:00 2001 From: kyungseopk1m Date: Mon, 20 Apr 2026 22:22:47 +0900 Subject: [PATCH 1/3] fix(v2): validate timeoutSeconds per trigger type The v2 SDK accepts any timeoutSeconds value at function-definition time and leaves the rejection to the Cloud Functions control plane at deploy time. v1 has had assertRuntimeOptionsValid for a long time but v2 was missing the equivalent. Add per-trigger-type validation so misconfigured values fail locally instead of mid-deploy: - 540s for event-handling functions (firestore, database, pubsub, storage, scheduler, eventarc, testLab, remoteConfig, alerts, dataconnect) - 3600s for HTTPS and callable functions (onRequest, onCall, onCallGenkit, identity, dataconnect/graphql, ai) - 1800s for task queue functions (onTaskDispatched) Implementation notes: - New internal helper assertTimeoutSecondsValid(opts, kind) and MAX_{EVENT,HTTPS,TASK}_TIMEOUT_SECONDS constants in src/v2/options.ts. - optionsToEndpoint and optionsToTriggerAnnotations gained an optional kind argument. When omitted they behave exactly as before, so existing callers are unaffected. - Expression and RESET_VALUE are passed through untouched; the SDK cannot know the concrete value in those cases. - For providers whose __endpoint is a lazy getter (storage) the throw happens the first time the manifest is read instead of at function-definition time; a regression test covers this path. Fixes #1737 --- CHANGELOG.md | 2 + spec/v2/options.spec.ts | 142 +++++++++++++++++++++++- spec/v2/providers/https.spec.ts | 14 +++ spec/v2/providers/pubsub.spec.ts | 13 +++ spec/v2/providers/storage.spec.ts | 8 ++ spec/v2/providers/tasks.spec.ts | 6 + src/v2/options.ts | 77 ++++++++++++- src/v2/providers/ai/index.ts | 4 +- src/v2/providers/alerts/alerts.ts | 2 +- src/v2/providers/database.ts | 2 +- src/v2/providers/dataconnect/graphql.ts | 2 +- src/v2/providers/dataconnect/index.ts | 2 +- src/v2/providers/eventarc.ts | 2 +- src/v2/providers/firestore.ts | 2 +- src/v2/providers/https.ts | 11 +- src/v2/providers/identity.ts | 2 +- src/v2/providers/pubsub.ts | 4 +- src/v2/providers/remoteConfig.ts | 2 +- src/v2/providers/scheduler.ts | 2 +- src/v2/providers/storage.ts | 4 +- src/v2/providers/tasks.ts | 7 +- src/v2/providers/testLab.ts | 2 +- 22 files changed, 286 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..e845eb43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +- Validate `timeoutSeconds` per v2 trigger type (540s for events, 3600s for HTTPS/callable, 1800s for task queues) so misconfigured values fail at function-definition or manifest-extraction time instead of at deploy time. + diff --git a/spec/v2/options.spec.ts b/spec/v2/options.spec.ts index d83b16894..1f2b9dcbc 100644 --- a/spec/v2/options.spec.ts +++ b/spec/v2/options.spec.ts @@ -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)", () => { @@ -92,3 +99,134 @@ 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(); + }); + + 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(); + }); + + 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 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("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(); + }); + + 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/ + ); + }); + + 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(); + }); +}); + +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/ + ); + }); + + 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(); + }); +}); diff --git a/spec/v2/providers/https.spec.ts b/spec/v2/providers/https.spec.ts index a62dfea5a..38cd4c8c1 100644 --- a/spec/v2/providers/https.spec.ts +++ b/spec/v2/providers/https.spec.ts @@ -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", () => { @@ -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); diff --git a/spec/v2/providers/pubsub.spec.ts b/spec/v2/providers/pubsub.spec.ts index cf885c6a5..a975c7f18 100644 --- a/spec/v2/providers/pubsub.spec.ts +++ b/spec/v2/providers/pubsub.spec.ts @@ -134,6 +134,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 = { diff --git a/spec/v2/providers/storage.spec.ts b/spec/v2/providers/storage.spec.ts index 2933771d4..7dced5ddb 100644 --- a/spec/v2/providers/storage.spec.ts +++ b/spec/v2/providers/storage.spec.ts @@ -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", () => { diff --git a/spec/v2/providers/tasks.spec.ts b/spec/v2/providers/tasks.spec.ts index b4e326ba6..4e3c9bbf4 100644 --- a/spec/v2/providers/tasks.spec.ts +++ b/spec/v2/providers/tasks.spec.ts @@ -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; diff --git a/src/v2/options.ts b/src/v2/options.ts index c5f8f1ba9..4ec459e1b 100644 --- a/src/v2/options.ts +++ b/src/v2/options.ts @@ -41,6 +41,31 @@ import * as logger from "../logger"; export { RESET_VALUE } from "../common/options"; +/** + * Maximum timeout in seconds for event-handling functions (e.g. Firestore, + * Realtime Database, Pub/Sub, Storage, Eventarc, alerts, scheduler). + * @internal + */ +export const MAX_EVENT_TIMEOUT_SECONDS = 540; + +/** + * Maximum timeout in seconds for HTTPS and callable functions. + * @internal + */ +export const MAX_HTTPS_TIMEOUT_SECONDS = 3600; + +/** + * Maximum timeout in seconds for Task Queue functions. + * @internal + */ +export const MAX_TASK_TIMEOUT_SECONDS = 1800; + +/** + * Function category used to pick a `timeoutSeconds` upper bound. + * @internal + */ +export type TimeoutKind = "event" | "https" | "task"; + /** * List of all regions supported by Cloud Functions (2nd gen). */ @@ -334,13 +359,57 @@ export interface EventHandlerOptions extends Omit max) { + throw new Error( + `timeoutSeconds must be between 0 and ${max} for ${label} functions. Got ${timeoutSeconds}.` + ); + } +} + /** * Apply GlobalOptions to trigger definitions. * @internal */ export function optionsToTriggerAnnotations( - opts: GlobalOptions | EventHandlerOptions | HttpsOptions + opts: GlobalOptions | EventHandlerOptions | HttpsOptions, + kind?: TimeoutKind ): TriggerAnnotation { + if (kind !== undefined) { + assertTimeoutSecondsValid(opts, kind); + } const annotation: TriggerAnnotation = {}; copyIfPresent( annotation, @@ -398,8 +467,12 @@ export function optionsToTriggerAnnotations( * @internal */ export function optionsToEndpoint( - opts: GlobalOptions | EventHandlerOptions | HttpsOptions + opts: GlobalOptions | EventHandlerOptions | HttpsOptions, + kind?: TimeoutKind ): ManifestEndpoint { + if (kind !== undefined) { + assertTimeoutSecondsValid(opts, kind); + } const endpoint: ManifestEndpoint = {}; copyIfPresent( endpoint, diff --git a/src/v2/providers/ai/index.ts b/src/v2/providers/ai/index.ts index 413beb685..2096a7aac 100644 --- a/src/v2/providers/ai/index.ts +++ b/src/v2/providers/ai/index.ts @@ -237,7 +237,7 @@ export function beforeGenerateContent( func = wrapTraceContext(withInit(func)); const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "https"); func.__endpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), platform: "gcfv2", @@ -344,7 +344,7 @@ export function afterGenerateContent( func = wrapTraceContext(withInit(func)); const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "https"); func.__endpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), platform: "gcfv2", diff --git a/src/v2/providers/alerts/alerts.ts b/src/v2/providers/alerts/alerts.ts index 1cfacd706..1e7f7dd79 100644 --- a/src/v2/providers/alerts/alerts.ts +++ b/src/v2/providers/alerts/alerts.ts @@ -235,7 +235,7 @@ export function getEndpointAnnotation( appId?: string ): ManifestEndpoint { const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "event"); const endpoint: ManifestEndpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), platform: "gcfv2", diff --git a/src/v2/providers/database.ts b/src/v2/providers/database.ts index 6c5f5e83c..f86bc7277 100644 --- a/src/v2/providers/database.ts +++ b/src/v2/providers/database.ts @@ -601,7 +601,7 @@ export function makeEndpoint( instance: PathPattern ): ManifestEndpoint { const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "event"); const eventFilters: Record = {}; const eventFilterPathPatterns: Record = { diff --git a/src/v2/providers/dataconnect/graphql.ts b/src/v2/providers/dataconnect/graphql.ts index ef24d68a9..65a6fa267 100644 --- a/src/v2/providers/dataconnect/graphql.ts +++ b/src/v2/providers/dataconnect/graphql.ts @@ -94,7 +94,7 @@ export function onGraphRequest(opts: GraphqlServerOptions): HttpsFunction { const baseOpts = options.optionsToEndpoint(globalOpts); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. - const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions, "https"); const endpoint: Partial = { ...initV2Endpoint(globalOpts, opts), platform: "gcfv2", diff --git a/src/v2/providers/dataconnect/index.ts b/src/v2/providers/dataconnect/index.ts index 5d88dd2bd..d805d636a 100644 --- a/src/v2/providers/dataconnect/index.ts +++ b/src/v2/providers/dataconnect/index.ts @@ -253,7 +253,7 @@ function makeEndpoint( operation: PathPattern | undefined ): ManifestEndpoint { const baseOpts = optionsToEndpoint(getGlobalOptions()); - const specificOpts = optionsToEndpoint(opts); + const specificOpts = optionsToEndpoint(opts, "event"); const eventFilters: Record = {}; const eventFilterPathPatterns: Record = {}; diff --git a/src/v2/providers/eventarc.ts b/src/v2/providers/eventarc.ts index f00597bab..2a138f25b 100644 --- a/src/v2/providers/eventarc.ts +++ b/src/v2/providers/eventarc.ts @@ -203,7 +203,7 @@ export function onCustomEventPublished( const channel = opts.channel ?? "locations/us-central1/channels/firebase"; const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "event"); const endpoint: ManifestEndpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), diff --git a/src/v2/providers/firestore.ts b/src/v2/providers/firestore.ts index a7b7ffb81..e76649488 100644 --- a/src/v2/providers/firestore.ts +++ b/src/v2/providers/firestore.ts @@ -899,7 +899,7 @@ export function makeEndpoint( namespace: string | Expression ): ManifestEndpoint { const baseOpts = optionsToEndpoint(getGlobalOptions()); - const specificOpts = optionsToEndpoint(opts); + const specificOpts = optionsToEndpoint(opts, "event"); const eventFilters: Record> = { database, diff --git a/src/v2/providers/https.ts b/src/v2/providers/https.ts index cfb3cfee3..7c8e01112 100644 --- a/src/v2/providers/https.ts +++ b/src/v2/providers/https.ts @@ -355,7 +355,10 @@ export function onRequest( const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. - const specificOpts = options.optionsToTriggerAnnotations(opts as options.GlobalOptions); + const specificOpts = options.optionsToTriggerAnnotations( + opts as options.GlobalOptions, + "https" + ); const trigger: any = { platform: "gcfv2", ...baseOpts, @@ -384,7 +387,7 @@ export function onRequest( const baseOpts = options.optionsToEndpoint(globalOpts); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. - const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions, "https"); const endpoint: Partial = { ...initV2Endpoint(globalOpts, opts), platform: "gcfv2", @@ -485,7 +488,7 @@ export function onCall, Stream = unknown>( const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. - const specificOpts = options.optionsToTriggerAnnotations(opts); + const specificOpts = options.optionsToTriggerAnnotations(opts, "https"); return { platform: "gcfv2", ...baseOpts, @@ -505,7 +508,7 @@ export function onCall, Stream = unknown>( const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToEndpoint handles both cases. - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "https"); func.__endpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), platform: "gcfv2", diff --git a/src/v2/providers/identity.ts b/src/v2/providers/identity.ts index 2e46b6779..248eadac3 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -325,7 +325,7 @@ export function beforeOperation( /** Endpoint */ const baseOptsEndpoint = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOptsEndpoint = options.optionsToEndpoint(opts); + const specificOptsEndpoint = options.optionsToEndpoint(opts, "https"); func.__endpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), platform: "gcfv2", diff --git a/src/v2/providers/pubsub.ts b/src/v2/providers/pubsub.ts index bb151877b..8f2af9c2b 100644 --- a/src/v2/providers/pubsub.ts +++ b/src/v2/providers/pubsub.ts @@ -395,7 +395,7 @@ export function onMessagePublished( Object.defineProperty(func, "__trigger", { get: () => { const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); - const specificOpts = options.optionsToTriggerAnnotations(opts); + const specificOpts = options.optionsToTriggerAnnotations(opts, "event"); return { platform: "gcfv2", @@ -414,7 +414,7 @@ export function onMessagePublished( }); const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "event"); const endpoint: ManifestEndpoint = { ...initV2Endpoint(options.getGlobalOptions(), opts), diff --git a/src/v2/providers/remoteConfig.ts b/src/v2/providers/remoteConfig.ts index 7f9ffba22..041dbe3ed 100644 --- a/src/v2/providers/remoteConfig.ts +++ b/src/v2/providers/remoteConfig.ts @@ -153,7 +153,7 @@ export function onConfigUpdated( } const baseOpts = optionsToEndpoint(getGlobalOptions()); - const specificOpts = optionsToEndpoint(optsOrHandler); + const specificOpts = optionsToEndpoint(optsOrHandler, "event"); const func: any = wrapTraceContext( withInit((raw: CloudEvent) => { diff --git a/src/v2/providers/scheduler.ts b/src/v2/providers/scheduler.ts index 876ff7194..56a415533 100644 --- a/src/v2/providers/scheduler.ts +++ b/src/v2/providers/scheduler.ts @@ -225,7 +225,7 @@ export function onSchedule( const globalOpts = options.getGlobalOptions(); const baseOptsEndpoint = options.optionsToEndpoint(globalOpts); - const specificOptsEndpoint = options.optionsToEndpoint(separatedOpts.opts); + const specificOptsEndpoint = options.optionsToEndpoint(separatedOpts.opts, "event"); const ep: ManifestEndpoint = { ...initV2Endpoint(globalOpts, separatedOpts.opts), diff --git a/src/v2/providers/storage.ts b/src/v2/providers/storage.ts index 3e705f243..4719bae79 100644 --- a/src/v2/providers/storage.ts +++ b/src/v2/providers/storage.ts @@ -779,7 +779,7 @@ export function onOperation( Object.defineProperty(func, "__trigger", { get: () => { const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); - const specificOpts = options.optionsToTriggerAnnotations(opts); + const specificOpts = options.optionsToTriggerAnnotations(opts, "event"); return { platform: "gcfv2", @@ -807,7 +807,7 @@ export function onOperation( Object.defineProperty(func, "__endpoint", { get: () => { const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); - const specificOpts = options.optionsToEndpoint(opts); + const specificOpts = options.optionsToEndpoint(opts, "event"); const endpoint: ManifestEndpoint = { platform: "gcfv2", diff --git a/src/v2/providers/tasks.ts b/src/v2/providers/tasks.ts index 9bb0cb13f..511a0f7a2 100644 --- a/src/v2/providers/tasks.ts +++ b/src/v2/providers/tasks.ts @@ -241,7 +241,10 @@ export function onTaskDispatched( const baseOpts = options.optionsToTriggerAnnotations(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToTriggerAnnotations handles both cases. - const specificOpts = options.optionsToTriggerAnnotations(opts as options.GlobalOptions); + const specificOpts = options.optionsToTriggerAnnotations( + opts as options.GlobalOptions, + "task" + ); const taskQueueTrigger: Record = {}; copyIfPresent(taskQueueTrigger, opts, "retryConfig", "rateLimits"); convertIfPresent( @@ -268,7 +271,7 @@ export function onTaskDispatched( const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); // global options calls region a scalar and https allows it to be an array, // but optionsToManifestEndpoint handles both cases. - const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions); + const specificOpts = options.optionsToEndpoint(opts as options.GlobalOptions, "task"); func.__endpoint = { platform: "gcfv2", diff --git a/src/v2/providers/testLab.ts b/src/v2/providers/testLab.ts index 3b4e5a3c1..1bd1a046d 100644 --- a/src/v2/providers/testLab.ts +++ b/src/v2/providers/testLab.ts @@ -188,7 +188,7 @@ export function onTestMatrixCompleted( } const baseOpts = optionsToEndpoint(getGlobalOptions()); - const specificOpts = optionsToEndpoint(optsOrHandler); + const specificOpts = optionsToEndpoint(optsOrHandler, "event"); const func: any = (raw: CloudEvent) => { return wrapTraceContext(withInit(handler))(raw as CloudEvent); From 14e4b082d4a3be82a3bfdce1dc94add258d5de38 Mon Sep 17 00:00:00 2001 From: kyungseopk1m Date: Mon, 20 Apr 2026 22:25:40 +0900 Subject: [PATCH 2/3] docs: reference PR #1874 in CHANGELOG entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e845eb43e..24b8fb0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,2 @@ -- Validate `timeoutSeconds` per v2 trigger type (540s for events, 3600s for HTTPS/callable, 1800s for task queues) so misconfigured values fail at function-definition or manifest-extraction time instead of at deploy time. +- Validate `timeoutSeconds` per v2 trigger type (540s for events, 3600s for HTTPS/callable, 1800s for task queues) so misconfigured values fail at function-definition or manifest-extraction time instead of at deploy time. (#1874) From 750b2e91de47833e553abcb8544daacfacb5a87f Mon Sep 17 00:00:00 2001 From: kyungseopk1m Date: Wed, 22 Apr 2026 18:48:50 +0900 Subject: [PATCH 3/3] style: format CHANGELOG.md with prettier --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24b8fb0c0..cb4475fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1 @@ - Validate `timeoutSeconds` per v2 trigger type (540s for events, 3600s for HTTPS/callable, 1800s for task queues) so misconfigured values fail at function-definition or manifest-extraction time instead of at deploy time. (#1874) -