Skip to content

fix(common): preserve DEFAULT_PROJECT_ID_TOKEN in joinURIComponents() - #9315

Open
akiliscodes wants to merge 3 commits into
googleapis:mainfrom
akiliscodes:fix/preserve-project-id-token-in-join-uri-components
Open

akiliscodes wants to merge 3 commits into
googleapis:mainfrom
akiliscodes:fix/preserve-project-id-token-in-join-uri-components

Conversation

@akiliscodes

Copy link
Copy Markdown

Summary

Preserve DEFAULT_PROJECT_ID_TOKEN when building request URIs with joinURIComponents() so lazy project ID resolution can still replace the placeholder before the request is sent.

Problem

#9188 hardened joinURIComponents() against path traversal by encoding path components with encodeURIPath().

This also causes the internal DEFAULT_PROJECT_ID_TOKEN placeholder ({{projectId}}) to be encoded as %7B%7BprojectId%7D%7D.

Project ID substitution happens later, when decorateRequest() calls replaceProjectIdToken(). Because that function looks for the literal {{projectId}} token, it can no longer recognize the encoded placeholder.

As a result, clients relying on lazy project ID resolution can send the encoded placeholder to the backend instead of the resolved project ID, causing the request to fail.

Reported in #9256.

Fix

Update joinURIComponents() to preserve DEFAULT_PROJECT_ID_TOKEN when a trimmed path component exactly matches the library-owned token.

Only this exact internal constant bypasses encoding. All other path components continue through encodeURIPath(), preserving the path-traversal protection introduced in #9188.

This allows the existing request flow to work as intended:

joinURIComponents()
  → preserves {{projectId}}

decorateRequest()
  → replaceProjectIdToken()
  → substitutes the resolved project ID

Testing

Added coverage for both the regression and the existing URI-encoding behavior:

  • test/util.ts
    • verifies that DEFAULT_PROJECT_ID_TOKEN survives joinURIComponents() and can subsequently be replaced by replaceProjectIdToken()
    • verifies that token-like user-controlled segments such as {{../../admin}} do not bypass encoding/path-traversal protection
    • verifies that regular path components containing special characters are still encoded
  • test/service.ts
    • verifies that Service.request_() preserves the literal {{projectId}} placeholder until the request-decoration phase when no explicit project ID is provided

Checklist

Fixes #9256 🦕

@google-cla

google-cla Bot commented Sep 13, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@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 addresses a regression where the DEFAULT_PROJECT_ID_TOKEN was encoded, breaking lazy/ADC project-ID resolution. The changes update joinURIComponents to bypass encoding for this token and add corresponding unit tests. The review feedback points out two critical issues: a missing import of replaceProjectIdToken in the test file, and a potential circular dependency if DEFAULT_PROJECT_ID_TOKEN is imported from service into util. The reviewer suggests moving the token definition to util.ts to resolve the circular dependency cleanly.

Comment thread core/common/test/util.ts
ParsedHttpRespMessage,
ParsedHttpResponseBody,
Util,
joinURIComponents,

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.

high

The test uses replaceProjectIdToken on line 1937, but it is not imported from ../src/util. This will cause a compilation error. Please add replaceProjectIdToken to the imports.

  joinURIComponents,
  replaceProjectIdToken,

Comment thread core/common/src/util.ts
const trimSlashesRegex = /^\/*|\/*$/g;
const trimmed = uriComponent.replace(trimSlashesRegex, '');
const trimmed = uriComponent.replace(/^\/*|\/*$/g, '');
if (trimmed === DEFAULT_PROJECT_ID_TOKEN) return trimmed;

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.

high

Using DEFAULT_PROJECT_ID_TOKEN here will cause a compilation error if it is not imported. However, importing it from ./service into ./util introduces a circular dependency (service -> util -> service), which can lead to runtime issues (e.g., DEFAULT_PROJECT_ID_TOKEN being evaluated as undefined during module initialization).

To resolve this cleanly, consider defining DEFAULT_PROJECT_ID_TOKEN in src/util.ts and exporting it, then re-exporting it from src/service.ts to maintain backward compatibility.

encodeURIPath encodes '{{projectId}}' as '%7B%7BprojectId%7D%7D',
which prevents replaceProjectIdToken() from finding and substituting
the placeholder later in the request lifecycle (decorateRequest →
makeAuthenticatedRequest).

Skip encoding when the trimmed component equals DEFAULT_PROJECT_ID_TOKEN
so that ADC / lazy project-ID resolution continues to work for clients
that do not supply an explicit projectId (e.g. new BigQuery()).

Regression introduced by googleapis#9188.
Fixes googleapis#9256.
@akiliscodes
akiliscodes force-pushed the fix/preserve-project-id-token-in-join-uri-components branch from ba0c03b to 8e247fb Compare September 13, 2026 20:44
@akiliscodes
akiliscodes marked this pull request as ready for review September 13, 2026 20:54
@akiliscodes
akiliscodes requested a review from a team as a code owner September 13, 2026 20:54
@github-actions
github-actions Bot requested a review from shivanee-p September 13, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant