Skip to content

[Bug]: Multiple URLs missing basePath prefix in subpath deployments #319

@dealerweb

Description

@dealerweb

Description

When deploying under a subpath (e.g. NEXT_PUBLIC_BASE_PATH=/webmail), Next.js prefixes <Link> and next/image automatically — but many hand-written URLs in the codebase bypass this and hit the origin root instead of the deployment subpath. Result: broken requests in production whenever the app isn't mounted at /.

There's already an opt-in apiFetch() helper in lib/browser-navigation.ts, but several call sites forgot to use it, and it can't help with <img src> / <link href> / window.location.* anyway because the browser resolves those itself.

Affected files

Raw fetch() / XMLHttpRequest not using apiFetch:

  • app/(main)/[locale]/auth/callback/page.tsx:99 — SSO completion
  • lib/plugin-sandbox/bundle-signing.ts:29 — plugin signing pubkey
  • lib/webdav/client.ts:35,121 — WebDAV proxy fetch + XMLHttpRequest.open

<img src> with /api/... or config values containing /api/...:

  • components/ui/avatar.tsx:239 (sender favicon)
  • app/(main)/admin/layout.tsx:280,332,362,394 (admin logo ×4)
  • app/(main)/admin/login/page.tsx:51
  • app/(main)/[locale]/login/page.tsx:725,875
  • components/layout/navigation-rail.tsx:451
  • components/pwa-install-prompt.tsx:69,78
  • app/(main)/setup/page.tsx:1332 (wizard upload preview)

<img src> with hard-coded /branding/...:

  • components/email/email-viewer.tsx:3216
  • components/settings/about-data-settings.tsx:126,131
  • components/settings/files-settings.tsx:98,128

Next.js metadata.icons (renders as <link rel="icon">):

  • app/(main)/layout.tsx:42icons: { icon: faviconUrl }

window.location.* bypassing the router:

  • app/(main)/setup/page.tsx:262window.location.assign('/admin/login')
  • app/(main)/[locale]/pro/page.tsx:175window.location.replace("/")

Suggested fix

Add a single helper module and patch window.fetch / XMLHttpRequest once at boot via the root layout. Helper is idempotent, so wrapping values that are already prefixed or external (https://...) is safe — no DB migration needed.

// lib/api-prefix.ts (new file)
export const API_PREFIX = (process.env.NEXT_PUBLIC_BASE_PATH ?? '').replace(/\/+$/, '');

export function withApiPath(path: string): string {
  if (!API_PREFIX || !path) return path;
  if (path.charCodeAt(0) !== 47) return path;        // not absolute
  if (path.charCodeAt(1) === 47) return path;        // protocol-relative
  if (path === API_PREFIX || path.startsWith(API_PREFIX + '/')) return path;
  return API_PREFIX + path;
}

export function getApiPrefixInstallScript(): string {
  const P = JSON.stringify(API_PREFIX);
  return `(function(){var P=${P};if(!P||window.__apiPrefixInstalled)return;window.__apiPrefixInstalled=true;
function fix(u){if(typeof u!=='string'||u.charCodeAt(0)!==47||u.charCodeAt(1)===47||u===P||u.indexOf(P+'/')===0)return u;return P+u;}
var of=window.fetch;if(of)window.fetch=function(i,init){if(typeof i==='string')i=fix(i);return of.call(this,i,init);};
var X=window.XMLHttpRequest;if(X&&X.prototype.open){var oo=X.prototype.open;X.prototype.open=function(m,u){var a=[].slice.call(arguments);a[1]=fix(u);return oo.apply(this,a);};}})();`;
}

Wire into both root layouts (app/(main)/layout.tsx and app/(sandbox)/layout.tsx) inside <head>:

<script
  nonce={nonce}
  suppressHydrationWarning
  dangerouslySetInnerHTML={{ __html: getApiPrefixInstallScript() }}
/>

→ This single change fixes all three "raw fetch" sites above without any call-site edits.

At every JSX / window.location.* site from the lists above, wrap the URL:

<img src={withApiPath(logoUrl)} />
icons: { icon: withApiPath(faviconUrl) }
window.location.assign(withApiPath('/admin/login'));

Steps to Reproduce

  1. Set NEXT_PUBLIC_BASE_PATH=/webmail in .env.local
  2. npm run build and deploy behind a reverse proxy mounting the app at https://host/webmail/
  3. Open https://host/webmail/de/login and check DevTools → Network

Expected Behavior

All same-origin requests, image sources, <link> hrefs, and window.location navigations are prefixed with /webmail. E.g. GET /webmail/api/admin/branding/loginLogoDarkUrl.png.

Actual Behavior

Multiple URLs hit the origin root without the prefix and return 404:

  • GET /api/admin/branding/loginLogoDarkUrl.png → 404
  • GET /api/admin/branding/faviconUrl.png → 404
  • GET /api/favicon?domain=… → 404
  • GET /api/plugin-signing-pubkey → 404 (signed plugins fail to verify)
  • GET /api/webdav → 404 (Files page broken)
  • <link rel="icon"> points to /branding/Bulwark_Favicon.svg without the prefix
  • Setup wizard's "Finish" navigates to /admin/login instead of /webmail/admin/login
  • Leaving Pro mode on mobile redirects to / instead of /webmail/

Bulwark Version

1.7.0

Stalwart Mail Server Version

0.16.5

Browser

Chrome / Chromium

Operating System

Windows

Screenshots / Screen Recording

No response

Relevant Logs or Error Output

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions