From 5e5abc92f472d764f232316d41f33e9159b81056 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 14 Oct 2026 04:34:55 +0200 Subject: [PATCH] fix(server): avoid excessive backtracking in webhook templates (#19160) Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com> --- .../scheduledTasks/webhookTemplate.test.ts | 19 +++++++++++++++++++ .../src/scheduledTasks/webhookTemplate.ts | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/server/src/scheduledTasks/webhookTemplate.test.ts b/apps/server/src/scheduledTasks/webhookTemplate.test.ts index 8b6f11e91f9f020bc3f9f7e3d38cd136899e54fb..f86a433c92e501d7e016b9f03f54355ac22a468c 100644 --- a/apps/server/src/scheduledTasks/webhookTemplate.test.ts +++ b/apps/server/src/scheduledTasks/webhookTemplate.test.ts @@ -52,6 +52,25 @@ describe("renderWebhookPrompt", () => { assert.deepEqual(rendered.missing, ["body.nope", "headers.x-missing", "unknown"]); }); + it("trims placeholder whitespace before resolving and reporting missing fields", () => { + const rendered = renderWebhookPrompt( + "{{\tbody.action\n}}|{{ body.nope }}|{{\nbody.nope\t}}|{{ \t\n }}", + githubPullRequest, + ); + assert.equal(rendered.prompt, "opened|||"); + assert.deepEqual(rendered.missing, ["body.nope", ""]); + }); + + it("preserves unclosed whitespace-heavy placeholders and resolves later valid ones", () => { + const unfinished = `Notify {{${" ".repeat(1024)}body`; + const rendered = renderWebhookPrompt( + `${unfinished}\n{{body.pull_request.number}}`, + githubPullRequest, + ); + assert.equal(rendered.prompt, `${unfinished}\n45`); + assert.deepEqual(rendered.missing, []); + }); + it("addresses form-encoded bodies and leaves other bodies to {{body}}", () => { const form = renderWebhookPrompt("{{body.text}} by {{body.user_name}}", { ...githubPullRequest, diff --git a/apps/server/src/scheduledTasks/webhookTemplate.ts b/apps/server/src/scheduledTasks/webhookTemplate.ts index 74c0f08ae7781dab7fb4f886c897988bd53f1b30..a0540e4a1766a1ee36dbbb44e8cf34a60ac6ac96 100644 --- a/apps/server/src/scheduledTasks/webhookTemplate.ts +++ b/apps/server/src/scheduledTasks/webhookTemplate.ts @@ -69,7 +69,8 @@ export function redactQuery(query: string): string { .join("&"); } -const PLACEHOLDER = /\{\{\s*([^{}]*?)\s*\}\}/g; +// Trim after matching so unclosed placeholders cannot backtrack over whitespace partitions. +const PLACEHOLDER = /\{\{([^{}]*)\}\}/g; function parseBody(request: WebhookRequest): unknown { const contentType = (request.headers["content-type"] ?? "").toLowerCase(); @@ -144,7 +145,8 @@ export function renderWebhookPrompt( } }; - const prompt = template.replace(PLACEHOLDER, (_match, expression: string) => { + const prompt = template.replace(PLACEHOLDER, (_match, rawExpression: string) => { + const expression = rawExpression.trim(); const value = resolve(expression); if (value === undefined || value === null) { if (!missing.includes(expression)) missing.push(expression); -- 2.54.0