Skip to content

fix(cors): defer parameter resolution in https callables and requests - #1903

Merged
inlined merged 9 commits into
masterfrom
fix-cors-param-resolution
Jul 1, 2026
Merged

fix(cors): defer parameter resolution in https callables and requests#1903
inlined merged 9 commits into
masterfrom
fix-cors-param-resolution

Conversation

@inlined

@inlined inlined commented Jun 13, 2026

Copy link
Copy Markdown
Member

Description

Fixes an issue where passing a parameterized CORS origin (Expression) to HTTPS functions attempts to evaluate the parameter at global module definition time rather than request runtime. This also updates V1 https.onRequest to accept an options object (HttpsOptions) supporting custom cors configuration, aligning with V2.

Scenarios Tested

  • Added unit tests in spec/common/providers/https.spec.ts verifying that resolveCorsOrigin successfully extracts runtime values from StringParam and ListParam Expressions.
  • Added unit tests in spec/v1/providers/https.spec.ts verifying V1 onRequest correctly enforces custom CORS options and does not crash when passed an Expression.
  • Added unit tests in spec/v2/providers/https.spec.ts verifying passing Expressions to onRequest and onCall CORS options does not cause definition-time crashes.
  • Ran the full npm test suite to ensure no regressions.

@inlined
inlined requested a review from wandamora June 13, 2026 01:10

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds support for configuring CORS options using parameter expressions (such as defineString and defineList) in both v1 and v2 HTTPS and callable functions. It introduces a resolveCorsOrigin helper to dynamically resolve these expressions per request and includes comprehensive unit tests. The review feedback highlights a critical bug in src/v1/function-builder.ts where arguments is incorrectly used inside an arrow function, which will cause runtime failures. Additionally, it is recommended to use typeof checks instead of arguments.length in src/v1/providers/https.ts for better robustness and consistency.

Comment on lines +345 to +361
onRequest: (
optsOrHandler:
| https.HttpsOptions
| ((req: https.Request, resp: express.Response) => void | Promise<void>),
handler?: (req: https.Request, resp: express.Response) => void | Promise<void>
) => {
let opts: https.HttpsOptions;
let userHandler: (req: https.Request, resp: express.Response) => void | Promise<void>;
if (arguments.length === 1) {
opts = {};
userHandler = optsOrHandler as any;
} else {
opts = optsOrHandler as https.HttpsOptions;
userHandler = handler!;
}
return https._onRequestWithOptions(userHandler, { ...this.options, ...opts } as any);
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using arguments inside an arrow function is a critical bug. Arrow functions do not bind their own arguments object; instead, they inherit it from the enclosing scope (in this case, the get https() getter, which has 0 arguments). Consequently, arguments.length === 1 will always evaluate to false, causing the function to incorrectly treat a single handler argument as options and leaving the actual handler undefined, leading to runtime crashes when requests are received.

To fix this, check typeof optsOrHandler === "function" instead of relying on arguments.length.

Suggested change
onRequest: (
optsOrHandler:
| https.HttpsOptions
| ((req: https.Request, resp: express.Response) => void | Promise<void>),
handler?: (req: https.Request, resp: express.Response) => void | Promise<void>
) => {
let opts: https.HttpsOptions;
let userHandler: (req: https.Request, resp: express.Response) => void | Promise<void>;
if (arguments.length === 1) {
opts = {};
userHandler = optsOrHandler as any;
} else {
opts = optsOrHandler as https.HttpsOptions;
userHandler = handler!;
}
return https._onRequestWithOptions(userHandler, { ...this.options, ...opts } as any);
},
onRequest: (
optsOrHandler:
| https.HttpsOptions
| ((req: https.Request, resp: express.Response) => void | Promise<void>),
handler?: (req: https.Request, resp: express.Response) => void | Promise<void>
) => {
let opts: https.HttpsOptions;
let userHandler: (req: https.Request, resp: express.Response) => void | Promise<void>;
if (typeof optsOrHandler === "function") {
opts = {};
userHandler = optsOrHandler;
} else {
opts = optsOrHandler as https.HttpsOptions;
userHandler = handler!;
}
return https._onRequestWithOptions(userHandler, { ...this.options, ...opts } as any);
},

Comment thread src/v1/providers/https.ts
Comment on lines 61 to 77
export function onRequest(
handler: (req: Request, resp: express.Response) => void | Promise<void>
optsOrHandler:
| HttpsOptions
| ((req: Request, resp: express.Response) => void | Promise<void>),
handler?: (req: Request, resp: express.Response) => void | Promise<void>
): HttpsFunction {
return _onRequestWithOptions(handler, {});
let opts: HttpsOptions;
let userHandler: (req: Request, resp: express.Response) => void | Promise<void>;
if (arguments.length === 1) {
opts = {};
userHandler = optsOrHandler as any;
} else {
opts = optsOrHandler as HttpsOptions;
userHandler = handler!;
}
return _onRequestWithOptions(userHandler, opts as DeploymentOptions);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and robustness, it is highly recommended to check typeof optsOrHandler === "function" instead of relying on arguments.length. This avoids potential issues if the function is refactored into an arrow function in the future and aligns with modern TypeScript practices.

Suggested change
export function onRequest(
handler: (req: Request, resp: express.Response) => void | Promise<void>
optsOrHandler:
| HttpsOptions
| ((req: Request, resp: express.Response) => void | Promise<void>),
handler?: (req: Request, resp: express.Response) => void | Promise<void>
): HttpsFunction {
return _onRequestWithOptions(handler, {});
let opts: HttpsOptions;
let userHandler: (req: Request, resp: express.Response) => void | Promise<void>;
if (arguments.length === 1) {
opts = {};
userHandler = optsOrHandler as any;
} else {
opts = optsOrHandler as HttpsOptions;
userHandler = handler!;
}
return _onRequestWithOptions(userHandler, opts as DeploymentOptions);
}
export function onRequest(
optsOrHandler:
| HttpsOptions
| ((req: Request, resp: express.Response) => void | Promise<void>),
handler?: (req: Request, resp: express.Response) => void | Promise<void>
): HttpsFunction {
let opts: HttpsOptions;
let userHandler: (req: Request, resp: express.Response) => void | Promise<void>;
if (typeof optsOrHandler === "function") {
opts = {};
userHandler = optsOrHandler;
} else {
opts = optsOrHandler as HttpsOptions;
userHandler = handler!;
}
return _onRequestWithOptions(userHandler, opts as DeploymentOptions);
}

Comment thread src/common/providers/https.ts Dismissed
Comment thread src/v1/providers/https.ts Dismissed
Comment thread src/v2/providers/https.ts Dismissed
Comment thread src/v2/providers/https.ts Dismissed
@wandamora

Copy link
Copy Markdown
Contributor

LGTM, Jetski suggested some edge cases you can add to your testing. Maybe it's useful.

  1. Expression Resolution Failure / Missing Environment Variables:

    Scenario: What happens when a StringParam is used for CORS, but the corresponding environment variable is completely missing or empty at runtime?
    Test: Assert that resolveCorsOrigin handles this gracefully (e.g., throws a clear error, falls back to blocking CORS, or uses a default value) rather than letting the function crash silently.

  2. CORS Option with Empty Array/List Expression:

    Scenario: A ListParam is resolved, but it evaluates to an empty list [].
    Test: Verify that an empty list doesn't cause runtime exceptions in the cors library and behaves as expected (typically denying all origins).

@inlined

inlined commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

1 Should be impossible but neither hurt to test against.

@inlined
inlined force-pushed the fix-cors-param-resolution branch from 60b6849 to c56a7ae Compare June 16, 2026 17:15
### Description
Fixes an issue where passing a parameterized CORS origin (`Expression`) to HTTPS functions attempts to evaluate the parameter at global module definition time rather than request runtime.
This updates `resolveCorsOrigin` to execute per-request, gracefully catching missing environment variables or empty parameters and disabling CORS (`origin: false`) as a secure fallback.
Additionally, this updates V1 `https.onRequest` to accept an options object (`HttpsOptions`) supporting custom `cors` configuration, aligning with V2.

### Scenarios Tested
- Added unit tests in `spec/common/providers/https.spec.ts` verifying `resolveCorsOrigin` successfully extracts runtime values from `StringParam` and `ListParam` Expressions, and safely defaults to `false` when params are missing or evaluate to empty lists/strings.
- Added unit tests in `spec/v1/providers/https.spec.ts` verifying V1 `onRequest` correctly enforces custom CORS options and does not crash when passed an Expression.
- Added unit tests in `spec/v2/providers/https.spec.ts` verifying passing Expressions to `onRequest` and `onCall` CORS options does not cause definition-time crashes.
- Ran the full `npm test` suite to confirm zero regressions.

### Sample Commands
`npm test`
@inlined
inlined force-pushed the fix-cors-param-resolution branch from b8b8fdf to a7e12d0 Compare June 16, 2026 18:42
@inlined
inlined enabled auto-merge June 16, 2026 19:33
@inlined
inlined added this pull request to the merge queue Jun 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Jun 30, 2026
@inlined
inlined merged commit 9837ad0 into master Jul 1, 2026
31 checks passed
@inlined
inlined deleted the fix-cors-param-resolution branch July 1, 2026 19:04
inlined added a commit that referenced this pull request Jul 16, 2026
…1929)

# Description
Fixes Cloud Build substitution errors when running prerelease/force
deploys.
Also had to change a GitHub repository setting to make sure the commit
message is the PR description. The previous relnotes were lost and are
added back here.

# Release Notes
relnote: fix: Remove false warning when using Expression in cors option
(#1802)
relnote: feat: Add requiresRole developer API for declarative security
support and automatic Manifest extraction (#1908)
relnote: Validate literal `timeoutSeconds` values per v2 trigger type
(0-540s for events, 0-3600s for HTTPS/callable, 0-1800s for task queues,
0-7s for identity functions) so misconfigured values fail at
function-definition or manifest-extraction time instead of at deploy
time. (#1877)
relnote: feat: Add requiresAPI function to allow declaring Google Cloud
API dependencies in code. (#1900)
relnote: fix(v1): Call onInit for schedule.onRun functions (#1801)
relnote: feat: Add support to declare lifecycle hooks in functions.
(#1915)
relnote: fix(cors): Fix issue using Params to set CORS allowed hosts
(#1903)
relnote: fix(v2): Fix event data unpacking for auth event triggers
(#1923)
relnote: feat: Add "v2/lifecycle" and "lifecycle" import paths for
lifecycle hooks (#1926)
relnote: chore: revamp deploy pipeline to be stateless. Changes must now
include relnotes
relnote: chore: move the last encrypted keys into Google Cloud Secrets
Manager
@sceee

sceee commented Jul 28, 2026

Copy link
Copy Markdown

Hi @inlined thanks for merging this - just wanted to quickly double check as it seems to be like this - the correct way of setting a RegExp value from the .env is to use defineString, right?

E.g.
.env file:

ALLOWED_CORS_ORIGINS="^http:\/\/.+\.example\.com$"

Code:

export const corsOrigins = defineString('ALLOWED_CORS_ORIGINS')

export const myFn = onCall<XYZ, Promise<ABC>>(
  {
    cors: corsOrigins,
  },
  async (request) => {
      // Handle...
  },
)

So defineString correctly interprets the value as "string regex", right?

@sceee

sceee commented Jul 29, 2026

Copy link
Copy Markdown

Hi @inlined thanks for merging this - just wanted to quickly double check as it seems to be like this - the correct way of setting a RegExp value from the .env is to use defineString, right?

E.g. .env file:

ALLOWED_CORS_ORIGINS="^http:\/\/.+\.example\.com$"

Code:

export const corsOrigins = defineString('ALLOWED_CORS_ORIGINS')

export const myFn = onCall<XYZ, Promise<ABC>>(
  {
    cors: corsOrigins,
  },
  async (request) => {
      // Handle...
  },
)

So defineString correctly interprets the value as "string regex", right?

Hi @inlined , unfortunately, when testing this it turned out this setup breaks CORS because the string is not parsed as RegExp but just as string (which browsers can't interpred), so it's unfortunately not working as I intended to use it.
When trying to work around this limitation, I faced another issue which I created #1943 for.

Can you think of another solution that would work and would allow to dynamically define CORS origins based on the environment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants