diff --git a/backend/app/issues/gitea.py b/backend/app/issues/gitea.py index aeb24492..6d462296 100644 --- a/backend/app/issues/gitea.py +++ b/backend/app/issues/gitea.py @@ -3,7 +3,7 @@ before a line of this was written (plans/e16-debug-issue.md, S0). Configured by an admin (sysconfig `issues_gitea_url` / `_repo` / `_token`, the token encrypted at rest). The repository must be PRIVATE: an issue carries its reporter's log — page titles, filter -values, at Teljes what they typed into search — and a public repository would publish it. That is +values, at Teljes (TRACE) what they typed into search — and a public repository would publish it. That is checked on every send, not only in the admin's "test connection", because a repository can be made public after the fact. @@ -154,9 +154,22 @@ def check(db: Session) -> dict: def preview(db: Session, text: str) -> str: - """The body as Gitea itself will render it (its markdown flavour, its sanitiser).""" - with _client(_settings(db)) as (client, repo): - r = _call(client, "POST", "/markdown", json={"Text": text, "Mode": "gfm", "Context": repo}) + """The body as Gitea itself will render it (its markdown flavour, its sanitiser). + + **No `Context`, deliberately — a repository context here does not fail the call, it kills the + tracker.** With one, Gitea's post-processor turns every hex-shaped word into a commit link, and + on the API's render path the commit checker it asks is nil: the whole Gitea process segfaults + and restarts, taking the git server (and, here, the container registry) down with it. Every + report carries hex — a tab id, a request id — so this was not an edge case but the only case. + Measured on throwaway instances (2026-09-20): `{"Mode": "gfm", "Context": ""}` with + `5645b1c0ae01` in the text crashes 1.26.4 and 1.27.1 and is fixed in 1.27.3 — but a self-hoster + runs whatever version they run, so the payload stays safe here rather than relying on theirs. + Dropping `Context` is not a downgrade of the preview: measured against the same body, the HTML + keeps its tables, its task lists and its highlighted code blocks. It only stops resolving + repo-relative links (a hash, a `#123`), which the issue page itself still resolves. + """ + with _client(_settings(db)) as (client, _repo): + r = _call(client, "POST", "/markdown", json={"Text": text, "Mode": "gfm"}) return r.text diff --git a/backend/tests/test_debug_sessions.py b/backend/tests/test_debug_sessions.py index 5c1730b0..6a791922 100644 --- a/backend/tests/test_debug_sessions.py +++ b/backend/tests/test_debug_sessions.py @@ -786,6 +786,7 @@ class FakeGitea: self.redirect = False # an http:// address behind a proxy that sends it to https self.garbage_asset = False # a 2xx that is not the API's answer (a proxy's page) self.patched_body: str | None = None + self.markdown_payloads: list[dict] = [] # what the preview asked the tracker to render def handler(self, request: httpx.Request) -> httpx.Response: path = request.url.path.removeprefix("/api/v1") @@ -826,6 +827,7 @@ class FakeGitea: self.patched_body = json.loads(request.content)["body"] return httpx.Response(201, json={"number": 7}) if request.method == "POST" and path == "/markdown": + self.markdown_payloads.append(json.loads(request.content)) return httpx.Response(200, text="

Steps

") return httpx.Response(404, json={"message": "not found"}) @@ -1180,6 +1182,20 @@ def test_the_preview_is_the_trackers_own_rendering(gitea_fake, db, make_user, as assert stranger.post(f"/api/debug-sessions/{sid}/issue").status_code == 404 +def test_the_preview_never_asks_the_tracker_for_a_repository_context( + gitea_fake, db, make_user, as_user +): + """A `Context` makes Gitea hunt for commit links, and on the API's render path that hunt + dereferences a nil checker: the tracker's whole process segfaults. Every report carries a + hex-shaped id, so the payload — not the text — is what has to stay safe (measured against + 1.26.4 / 1.27.1, fixed upstream in 1.27.3; a self-hoster's version is not ours to pick).""" + owner = as_user(make_user()) + sid = _start(owner)["id"] + text = "| tab | 5645b1c0ae01 |" + assert owner.post(f"/api/debug-sessions/{sid}/preview", json={"text": text}).status_code == 200 + assert gitea_fake.markdown_payloads == [{"Text": text, "Mode": "gfm"}] + + def test_the_admin_connection_test_names_what_is_wrong(gitea_fake, db, make_user, as_user): admin = as_user(make_user(role="admin")) r = admin.post("/api/admin/config/test-issues")