Summary
Etherpad assumes it is the root of its origin (/). When it sits behind a reverse proxy that adds a path prefix — Home Assistant ingress, Nginx location /etherpad/, Cloudflare Worker routes, etc. — plugin assets, admin routes and a few hard-coded URLs break in subtle, partial ways.
Reopening the topic from #2025 (closed wontfix, 2020) with concrete repros from a real Home Assistant install — happy to draft a PR if there's appetite this time.
Environment
- Etherpad 3.1.0 (running via the official
etherpad/etherpad:3.1.0 Docker image, wrapped in the new ether/home-assistant-addon-etherpad addon).
- Home Assistant OS (HAOS) supervisor on aarch64.
- HA serves Etherpad under
https://<ha-host>:8123/api/hassio_ingress/<RANDOM_TOKEN>/.
- HA's ingress proxy is plain HTTP-to-backend; it sets
X-Forwarded-Proto, X-Forwarded-Host and a X-Ingress-Path: /api/hassio_ingress/<TOKEN> header.
trustProxy: true.
Symptoms
1. Plugin assets 404 in the inner ace iframe
ep_headings2, ep_align, etc. install fine. Their dropdown buttons render in the toolbar. But applying a heading changes nothing visually — the changeset is stored server-side, the attribute is in the pad data, the inner ace iframe just never picks up the CSS that styles it.
Root cause: the inner iframe's <link rel=\"stylesheet\" href=\"/static/plugins/ep_headings2/...\"> resolves against https://<ha-host>:8123/static/... instead of https://<ha-host>:8123/api/hassio_ingress/<TOKEN>/static/.... Network tab shows 404. Same shape for any plugin whose CSS/JS is referenced with a leading /.
If the user switches to the direct port URL (no prefix), the same pad renders the headings correctly — proving the data round-trip is fine and the bug is purely the asset URL.
2. /admin and /admin/plugins unreachable when the URL bar is hidden
HA's ingress panel hides the iframe's URL bar (the iframe is full-bleed inside an HA panel). There is no link in the Etherpad index page that points to /admin, and the user can't type a new path into the iframe.
So once Etherpad is reached via ingress, the admin UI is effectively invisible — even if the user has the admin password set.
3. Misc absolute URLs
<link rel=\"manifest\" href=\"/manifest.json\">, <meta property=\"og:image\" content=\"http://0.0.0.0:9001/favicon.ico\">, social-meta tags, and a few favicon/touch-icon references all use absolute paths and emit http://0.0.0.0:9001/... strings even when publicURL is null. Browsers show favicon 404s; OG card previews show broken images when a pad is shared as a URL.
Why "just set publicURL" doesn't fix it
publicURL is a single static setting. HA's ingress prefix is a per-session random token, not a stable path. The right answer is for Etherpad to read the prefix at request time — typically from X-Forwarded-Prefix (HAProxy/Traefik convention) or X-Ingress-Path (HA's spelling) — and:
- Use it when rendering server-side HTML (replace leading
/ in plugin asset paths and admin links).
- Pass it as a
<base href> or via a small JS shim to the inner ace iframe so plugin-injected CSS resolves correctly.
- Strip leading slashes from
<meta> URLs when trustProxy: true and the request looks proxied.
There's prior art: nextcloud-snap and bitwarden_rs solved this with X-Forwarded-Prefix support. The Express ecosystem has express-url-rewrite middleware patterns for this exact case.
Workaround we ship today
The HA addon documents "use the direct port for anything beyond writing a pad". That works but is a poor UX:
- Plugin features partly work (toolbar yes, rendering no), so the failure mode is confusing.
- WebRTC plugins fail for a related-but-different reason (HA iframe
allow= policy, filed separately upstream of HA).
- Admin tooling is functionally unavailable through ingress.
Proposed scope
Minimum-viable: honor X-Forwarded-Prefix / X-Ingress-Path when trustProxy is true. Inject as <base> into the pad template; rewrite plugin-asset hrefs server-side; ensure plugin loader uses the same prefix. Doesn't require deprecating publicURL.
Will gladly draft a PR if a maintainer signals 👍 — would help every Home Assistant user, every Nginx subpath user, every Cloudflare Worker route user.
Refs: #2025, #7137. CC @JohnMcLear.
Summary
Etherpad assumes it is the root of its origin (
/). When it sits behind a reverse proxy that adds a path prefix — Home Assistant ingress, Nginxlocation /etherpad/, Cloudflare Worker routes, etc. — plugin assets, admin routes and a few hard-coded URLs break in subtle, partial ways.Reopening the topic from #2025 (closed wontfix, 2020) with concrete repros from a real Home Assistant install — happy to draft a PR if there's appetite this time.
Environment
etherpad/etherpad:3.1.0Docker image, wrapped in the newether/home-assistant-addon-etherpadaddon).https://<ha-host>:8123/api/hassio_ingress/<RANDOM_TOKEN>/.X-Forwarded-Proto,X-Forwarded-Hostand aX-Ingress-Path: /api/hassio_ingress/<TOKEN>header.trustProxy: true.Symptoms
1. Plugin assets 404 in the inner ace iframe
ep_headings2,ep_align, etc. install fine. Their dropdown buttons render in the toolbar. But applying a heading changes nothing visually — the changeset is stored server-side, the attribute is in the pad data, the inner ace iframe just never picks up the CSS that styles it.Root cause: the inner iframe's
<link rel=\"stylesheet\" href=\"/static/plugins/ep_headings2/...\">resolves againsthttps://<ha-host>:8123/static/...instead ofhttps://<ha-host>:8123/api/hassio_ingress/<TOKEN>/static/.... Network tab shows 404. Same shape for any plugin whose CSS/JS is referenced with a leading/.If the user switches to the direct port URL (no prefix), the same pad renders the headings correctly — proving the data round-trip is fine and the bug is purely the asset URL.
2. /admin and /admin/plugins unreachable when the URL bar is hidden
HA's ingress panel hides the iframe's URL bar (the iframe is full-bleed inside an HA panel). There is no link in the Etherpad index page that points to
/admin, and the user can't type a new path into the iframe.So once Etherpad is reached via ingress, the admin UI is effectively invisible — even if the user has the admin password set.
3. Misc absolute URLs
<link rel=\"manifest\" href=\"/manifest.json\">,<meta property=\"og:image\" content=\"http://0.0.0.0:9001/favicon.ico\">, social-meta tags, and a few favicon/touch-icon references all use absolute paths and emithttp://0.0.0.0:9001/...strings even whenpublicURLis null. Browsers show favicon 404s; OG card previews show broken images when a pad is shared as a URL.Why "just set publicURL" doesn't fix it
publicURLis a single static setting. HA's ingress prefix is a per-session random token, not a stable path. The right answer is for Etherpad to read the prefix at request time — typically fromX-Forwarded-Prefix(HAProxy/Traefik convention) orX-Ingress-Path(HA's spelling) — and:/in plugin asset paths and admin links).<base href>or via a small JS shim to the inner ace iframe so plugin-injected CSS resolves correctly.<meta>URLs whentrustProxy: trueand the request looks proxied.There's prior art:
nextcloud-snapandbitwarden_rssolved this withX-Forwarded-Prefixsupport. The Express ecosystem hasexpress-url-rewritemiddleware patterns for this exact case.Workaround we ship today
The HA addon documents "use the direct port for anything beyond writing a pad". That works but is a poor UX:
allow=policy, filed separately upstream of HA).Proposed scope
Minimum-viable: honor
X-Forwarded-Prefix/X-Ingress-PathwhentrustProxyis true. Inject as<base>into the pad template; rewrite plugin-asset hrefs server-side; ensure plugin loader uses the same prefix. Doesn't require deprecatingpublicURL.Will gladly draft a PR if a maintainer signals 👍 — would help every Home Assistant user, every Nginx subpath user, every Cloudflare Worker route user.
Refs: #2025, #7137. CC @JohnMcLear.