rewrite asset paths baked into the page, including JSON-escaped ones

Gitea inlines a script naming its own worker:
'\/assets\/js\/eventsource.sharedworker.…js'. That is not an attribute and its
slashes are backslash-escaped, so neither the attribute rewrite nor appSubUrl
saw it. Unprefixed, the browser resolved it against the PLATFORM's origin, got
the SPA's index.html, and reported "Uncaught SyntaxError: Unexpected token '<'"
from a file that appeared to load — along with a dead SharedWorker and a
heatmap parsing HTML as JSON.

appSubUrl covers everything Gitea's code BUILDS at runtime. This covers what is
already in the page before that code runs.

Narrow on purpose, like dev-proxy's /_bun/ rule: a blanket rewrite of absolute
paths inside JavaScript corrupts strings that are not URLs. /assets/ is Gitea's
own asset root and specific enough to match safely.

The escaping has to match the string it lands in — \/api\/gitea\/_web, not
\\/api — or the JS stops parsing. Both forms are tested against a live sample.
This commit is contained in:
officer
2026-08-19 09:28:04 +00:00
parent 01e4210c62
commit 47e3983f7e
2 changed files with 54 additions and 2 deletions
+30 -1
View File
@@ -1,5 +1,12 @@
import { describe, expect, test } from 'bun:test';
import { rewriteHtml, rewriteLocation, rewriteSetCookie, rewriteAttributes, rewriteAppSubUrl } from './rewrite';
import {
rewriteHtml,
rewriteLocation,
rewriteSetCookie,
rewriteAttributes,
rewriteAppSubUrl,
rewriteAssetPaths,
} from './rewrite';
const BASE = '/api/gitea/_web';
const ORIGINS = ['http://localhost:3000', 'https://gitea.officer.dev'];
@@ -90,6 +97,28 @@ describe('redirects — the most visible failure, because the frame just leaves'
});
});
describe('asset paths baked into the page before Gitea\'s own code runs', () => {
test('the JSON-escaped form — the SharedWorker URL that broke the dashboard', () => {
// Verbatim from a live page. Not an attribute, and its slashes are backslash-escaped, so neither the
// attribute rewrite nor appSubUrl touches it. Unprefixed, the browser resolves it against the
// PLATFORM's origin, gets the SPA's index.html, and reports `Unexpected token '<'` from a file that
// looks like it loaded.
const real = String.raw`'\/assets\/js\/eventsource.sharedworker.DV1Z2uJv.js',`;
const out = rewriteAssetPaths(real, BASE);
expect(out).toContain(String.raw`'\/api\/gitea\/_web\/assets\/js\/eventsource`);
// The escaping has to match the string it lands in, or the JS stops parsing.
expect(out).not.toContain(String.raw`\\/api`);
});
test('the plain quoted form too', () => {
expect(rewriteAssetPaths(`x = "/assets/img/logo.svg"`, BASE)).toContain(`"${BASE}/assets/img/logo.svg"`);
});
test('leaves a path that is not an asset root alone', () => {
expect(rewriteAssetPaths(`x = "/user/login"`, BASE)).toBe(`x = "/user/login"`);
});
});
describe('the origin Gitea declares about itself', () => {
test('is discovered from window.config, so ROOT_URL needs no second setting', () => {
// The sidecar only knows where it FETCHES from — usually loopback. The public origin lives in the