Skip to content

fix(react-client): prefix configured asset URLs with the app base path - #3064

Open
Bierbarbar wants to merge 1 commit into
Chainlit:mainfrom
Bierbarbar:fix/asset-url-root-path-prefix
Open

Bierbarbar wants to merge 1 commit into
Chainlit:mainfrom
Bierbarbar:fix/asset-url-root-path-prefix

Conversation

@Bierbarbar

@Bierbarbar Bierbarbar commented Sep 30, 2026 •

Copy link
Copy Markdown

Summary

logo_file_url, default_avatar_file_url, ChatProfile.icon, and login_page_image are returned verbatim by the frontend whenever they're configured as a /public/...-relative path, bypassing apiClient.buildEndpoint() entirely. This breaks under a reverse-proxy subpath deployment (e.g. https://host/my-app/...): the browser requests the unprefixed path against the domain root, which never reaches the app.

Several sibling call sites already guard against exactly this with the same pattern:

value.startsWith('/public') ? apiClient.buildEndpoint(value) : value

(see WelcomeScreen.tsx, Starter.tsx, ChatProfiles.tsx, ModePicker.tsx, ButtonLink.tsx) — buildEndpoint() is what correctly prepends httpEndpoint (window.origin + basename), so an already-absolute external URL is left untouched while a local /public/... path gets the app's base path prepended.

This PR applies the same, already-established guard to the three places that were missing it:

  • ChainlitAPI.getLogoEndpoint() (libs/react-client/src/api/index.tsx) — returned configuredLogoUrl raw
  • Avatar.tsx — returned default_avatar_file_url and selectedChatProfile.icon raw
  • Login.tsx — returned login_page_image raw

No new abstraction, no behavior change for apps not deployed behind a subpath (an unprefixed httpEndpoint is a no-op prefix).

Test plan

  • Added libs/react-client/src/api/index.spec.ts covering getLogoEndpoint: prefixes a /public/... path, leaves an external URL untouched, and the unconfigured fallback still works. Matches the existing unit-test convention in this package (state.spec.ts) — the project otherwise relies on Cypress E2E rather than component unit tests, and adding an E2E fixture for this would have required changes to the shared test runner (no per-scenario env var support today), which felt like scope creep for a 3-site fix.
  • pnpm lint, pnpm format-check, pnpm type-check (whole repo) all pass.
  • pnpm test in libs/react-client: 19/19 passing.

Reopens #3036, which was auto-closed by the stale bot for inactivity (no unresolved review feedback).

🤖 Generated with Claude Code


Summary by cubic

Fixes configured asset URLs (logo_file_url, default_avatar_file_url, ChatProfile.icon, login_page_image) so /public/... paths get the app base path prepended, preventing 404s under reverse-proxy subpath deployments. External URLs are left untouched.

  • Applies the same buildEndpoint() guard already used by sibling call sites to getLogoEndpoint(), Avatar.tsx, and Login.tsx.
  • Adds unit tests for getLogoEndpoint covering prefixed, external, and fallback cases.

Written for commit d65fc9e. Summary will update on new commits.

Review in cubic

logo_file_url, default_avatar_file_url, ChatProfile.icon, and
login_page_image were returned verbatim when set to a /public/... path,
bypassing apiClient.buildEndpoint() and breaking under a reverse-proxy
subpath (window.origin + basename never gets prepended). Several
sibling call sites (WelcomeScreen, Starter, ChatProfiles) already guard
with `startsWith('/public') ? buildEndpoint(url) : url` — this applies
the same guard to the three places that were missing it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="libs/react-client/src/api/index.tsx">

<violation number="1" location="libs/react-client/src/api/index.tsx:353">
P3: `startsWith('/public')` matches any path whose first segment merely starts with those characters (e.g. `/public_html/logo.png`, `/Public/logo.png`), which then gets rewritten through `buildEndpoint()` while sibling paths are left untouched. Use the documented `/public/` boundary, and consider extracting a shared helper since the identical check is duplicated in `frontend/src/components/chat/Messages/Message/Avatar.tsx` and `frontend/src/pages/Login.tsx`.</violation>
</file>

<file name="frontend/src/pages/Login.tsx">

<violation number="1" location="frontend/src/pages/Login.tsx:110">
P3: The `startsWith('/public') ? apiClient.buildEndpoint(url) : url` resolution is now duplicated in at least three places in this PR (Login.tsx, Avatar.tsx, react-client getLogoEndpoint) on top of the pre-existing copies in ButtonLink.tsx, Markdown.tsx, ModePicker.tsx, Starter.tsx, WelcomeScreen.tsx and ChatProfiles.tsx. Since the react-client library already owns `buildEndpoint`, add one shared helper there (e.g. `buildAssetEndpoint(url)`) and call it from every site so the prefix rule stays in a single place; this also removes Login's 3-level nested ternary.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

getLogoEndpoint(theme: string, configuredLogoUrl?: string) {
if (configuredLogoUrl) return configuredLogoUrl;
if (configuredLogoUrl) {
return configuredLogoUrl.startsWith('/public')

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.

P3: startsWith('/public') matches any path whose first segment merely starts with those characters (e.g. /public_html/logo.png, /Public/logo.png), which then gets rewritten through buildEndpoint() while sibling paths are left untouched. Use the documented /public/ boundary, and consider extracting a shared helper since the identical check is duplicated in frontend/src/components/chat/Messages/Message/Avatar.tsx and frontend/src/pages/Login.tsx.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At libs/react-client/src/api/index.tsx, line 353:

<comment>`startsWith('/public')` matches any path whose first segment merely starts with those characters (e.g. `/public_html/logo.png`, `/Public/logo.png`), which then gets rewritten through `buildEndpoint()` while sibling paths are left untouched. Use the documented `/public/` boundary, and consider extracting a shared helper since the identical check is duplicated in `frontend/src/components/chat/Messages/Message/Avatar.tsx` and `frontend/src/pages/Login.tsx`.</comment>

<file context>
@@ -349,7 +349,11 @@ export class ChainlitAPI extends APIBase {
   getLogoEndpoint(theme: string, configuredLogoUrl?: string) {
-    if (configuredLogoUrl) return configuredLogoUrl;
+    if (configuredLogoUrl) {
+      return configuredLogoUrl.startsWith('/public')
+        ? this.buildEndpoint(configuredLogoUrl)
+        : configuredLogoUrl;
</file context>

src={
config?.ui?.login_page_image ||
apiClient.buildEndpoint('/favicon')
config?.ui?.login_page_image

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.

P3: The startsWith('/public') ? apiClient.buildEndpoint(url) : url resolution is now duplicated in at least three places in this PR (Login.tsx, Avatar.tsx, react-client getLogoEndpoint) on top of the pre-existing copies in ButtonLink.tsx, Markdown.tsx, ModePicker.tsx, Starter.tsx, WelcomeScreen.tsx and ChatProfiles.tsx. Since the react-client library already owns buildEndpoint, add one shared helper there (e.g. buildAssetEndpoint(url)) and call it from every site so the prefix rule stays in a single place; this also removes Login's 3-level nested ternary.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At frontend/src/pages/Login.tsx, line 110:

<comment>The `startsWith('/public') ? apiClient.buildEndpoint(url) : url` resolution is now duplicated in at least three places in this PR (Login.tsx, Avatar.tsx, react-client getLogoEndpoint) on top of the pre-existing copies in ButtonLink.tsx, Markdown.tsx, ModePicker.tsx, Starter.tsx, WelcomeScreen.tsx and ChatProfiles.tsx. Since the react-client library already owns `buildEndpoint`, add one shared helper there (e.g. `buildAssetEndpoint(url)`) and call it from every site so the prefix rule stays in a single place; this also removes Login's 3-level nested ternary.</comment>

<file context>
@@ -107,8 +107,11 @@ export default function Login() {
             src={
-              config?.ui?.login_page_image ||
-              apiClient.buildEndpoint('/favicon')
+              config?.ui?.login_page_image
+                ? config.ui.login_page_image.startsWith('/public')
+                  ? apiClient.buildEndpoint(config.ui.login_page_image)
</file context>

This branch has not been deployed

No deployments
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.

1 participant