From 05ab9639518fb17dac764be8de077769e85eb9dd Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 20 Sep 2026 07:51:08 +0200 Subject: [PATCH] fix(issues): the preview must not ask the tracker for a repository context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer's preview did not fail a call — it killed the Gitea process, and with it the git server and the container registry. `POST /api/v1/markdown` with `{"Mode": "gfm", "Context": ""}` makes Gitea resolve every hex-shaped word as a commit link, and on the API's render path the commit checker it asks is nil: panic: runtime error: invalid memory address or nil pointer dereference renderhelper.(*commitChecker).IsCommitIDExisting(0x0) <- markup.hashCurrentPatternProcessor (html_commit.go:217) Every report carries hex — a tab id, a request id — so this was not an edge case but the only case: the preview answered "the tracker isn't answering" every time, because the tracker had just died. Measured on throwaway instances, not argued. Same body, same call: gfm + Context + "5645b1c0ae01" -> 1.26.4 CRASH, 1.27.1 CRASH, 1.27.3 200 gfm + Context, no hex -> 200 gfm, no Context -> 200, and the HTML keeps its tables, task lists and chroma-highlighted code blocks Upstream fixed it in 1.27.2 ("fix: avoid markup render panic", #38698), but a self-hoster runs whatever version they run, so the payload is what gets fixed. Sending was never affected: the created issue's own page renders the same body fine (measured) — a real repo context has a real commit checker. The only loss is that a hash or a `#123` is not yet a link in the preview; the issue page links it. The test pins the payload dict, because the defect is in what we send. --- backend/app/issues/gitea.py | 21 +++++++++++++++++---- backend/tests/test_debug_sessions.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) 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")