Skip to content

feat(notifications): replace webhook KeyMap with LiquidJS templating - #2039

Open
mmenanno wants to merge 4 commits into
seerr-team:developfrom
mmenanno:feature/refactor-webhook-handlebars
Open

mmenanno wants to merge 4 commits into
seerr-team:developfrom
mmenanno:feature/refactor-webhook-handlebars

Conversation

@mmenanno

@mmenanno mmenanno commented Oct 13, 2025 •

Copy link
Copy Markdown

Description

This PR refactors the webhook notification agent to use a shared LiquidJS-based templating engine instead of the bespoke KeyMap + parseKeys substitution system, providing a foundation for standardized templating across all notification agents.

This replaces the original Handlebars approach from earlier in this PR, following the maintainer discussion (review thread): Liquid is a better fit for an end-user template engine because it ships the logic/filters users need out of the box, so we don't have to implement and maintain custom helpers.

This is part of the larger effort to enable custom templates across notification agents; it lays the groundwork for extending template support to all agents in future PRs. No specific issue to link.

Key Changes

  • New TemplateEngine (server/lib/notifications/templateEngine.ts) wrapping a shared LiquidJS engine.
  • No custom helpers needed — Liquid provides conditionals, comparisons (==, >, …), and filters (default, upcase, downcase, capitalize, truncate, json, url_encode) natively. This also resolves several review points for free: numeric comparison coercion, default on empty strings, and no HTML-escaping (apostrophes/quotes render verbatim).
  • Circular-safe json filter — the built-in json filter is overridden so circular TypeORM relations (e.g. Media ↔ Issue) can't crash rendering.
  • buildContext exposes the same scalar variables as the old KeyMap (including discordIds, media_imdbid, jellyfinMediaId, plexRatingKey, requestedBy_jellyfinUserId) plus the full media/request/issue/comment/extra values for conditionals.
  • Simpler payload build — the stored template is rendered to a string and handed to JSON.parse; the previous regex "cleanup" of the rendered payload was removed.
  • Relaxed validation (client + server) — the payload is now a Liquid template that needn't be valid JSON until rendered, so the strict JSON.parse checks on save/test were removed. Templates are validated by using the Test button.
  • customHeaders support retained.
  • Unit tests added for TemplateEngine (escaping, optional-section null handling, circular-safe json, and variable mapping).

New Templating Capabilities

Webhook payloads and URLs can now use Liquid:

  • Conditionals: {% if media %}…{% else %}…{% endif %}, {% if media_status == "AVAILABLE" %}…{% endif %}
  • Comparisons: {% if pendingRequestsCount > 10 %}…{% endif %}
  • Filters: {{ notifyuser_username | capitalize }}, {{ media_type | upcase }}, {{ subject | truncate: 50 }}, {{ subject | json }}, {{ value | url_encode }}
  • Defaults: {{ notifyuser_username | default: "Unknown User" }}

The default payload now uses {% if %}…{% else %}null{% endif %} so optional sections (media/request/issue/comment) render as null when absent (matching prior behavior) while staying valid JSON, and emits free-text values (titles, overviews, display names, comments) through the json filter so quotes/newlines can't break the surrounding JSON.

Compatibility Notes

  • Simple {{ variable }} substitution continues to work unchanged.
  • This is not a drop-in for every existing custom payload: templates are now Liquid, so the old "magic" object keys ("{{media}}", "{{request}}", …) should be rewritten as {% if %} blocks (the Reset to Default button produces an up-to-date example). Filter names follow Liquid (upcase/downcase rather than upper/lower).
  • URL variable substitution renders raw; use the url_encode filter where encoding is needed.

Review Feedback Addressed

  • Switched the engine from Handlebars to LiquidJS (gauthier-th / M0NsTeRRR).
  • Removed the regex JSON "cleanup"; the render output is parsed directly (M0NsTeRRR).
  • A non-JSON template that renders to valid JSON is now allowed end-to-end (relaxed client + server validation) (M0NsTeRRR).
  • Removed the frontend payload-validation test that could no longer verify templates (M0NsTeRRR).
  • Dropped the notifyUser → requester fallback so notifyuser_* maps to notifyUser only, matching the previous KeyMap (M0NsTeRRR).
  • Restored an escaping path for free-text values, and url_encode is available for URL templating.

How Has This Been Tested?

Tested locally with pnpm dev against a webhook.site endpoint, plus automated unit tests.

Manual:

  1. Configure the webhook agent against the test endpoint, enable notification types, and Save.
  2. Click Test and confirm the webhook is received with a valid JSON body and variables substituted.
  3. Trigger real notifications (request, auto-approve, issue, comment) and confirm optional sections render as objects when relevant and null otherwise.
  4. Edge cases verified: a media title containing quotes, a multi-line issue/comment message, and templates using conditionals/filters — all produce valid JSON.

Automated: pnpm test server/lib/notifications/templateEngine.test.ts (escaping, optional-section null handling, circular-safe json, and buildContext variable mapping). pnpm build, pnpm typecheck, and pnpm lint all pass.

Screenshots / Logs (if applicable)

N/A — backend refactor (frontend default payload/validation updated, UI unchanged).

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy) — AI (Claude Code) used for implementation assistance
  • I have updated the documentation accordingly. — docs for the new Liquid template syntax to follow
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract — no new translation keys added
  • Database migration (if required) — not required (uses existing settings system)

Summary by CodeRabbit

  • New Features

    • Webhook notifications now support Liquid templates for JSON payloads and webhook URLs.
    • Templates can use notification details, related media, requests, issues, comments, and extra data.
  • Bug Fixes

    • Improved validation for template-based and standard JSON payloads.
    • Webhook settings are now handled more consistently during retrieval, updates, and upgrades.
  • Documentation

    • Expanded webhook documentation with Liquid examples, supported filters, conditionals, error handling, and available template objects.

@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

@M0NsTeRRR

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

Webhook notifications now render Liquid templates from stored payload text. Settings, routes, validation, documentation, and tests support raw template payloads and rendered JSON webhook bodies.

Changes: Webhook Template Engine

Layer / File(s) Summary
Template engine and tests
package.json, server/lib/notifications/templateEngine.ts, server/lib/notifications/templateEngine.test.ts
Adds liquidjs, builds notification template contexts, renders Liquid templates, and tests serialization, defaults, enums, and nested entities.
Webhook agent rendering
server/lib/notifications/agents/webhook.ts, server/lib/notifications/agents/webhook.test.ts
Renders webhook URLs and payloads through TemplateEngine, parses rendered JSON once, handles failures, and posts the computed body.
Settings default and migration
server/lib/settings/index.ts, server/lib/settings/migrations/0009_migrate_webhook_payload_encoding.ts
Defines and base64-encodes the default Liquid payload template, and migrates legacy encoded JSON string payloads.
Routes, validation, and documentation
server/routes/settings/notifications.ts, src/components/Settings/Notifications/NotificationsWebhook/index.tsx, docs/using-seerr/notifications/webhook.md, gen-docs/docusaurus.config.ts
Stores webhook payloads as raw text, accepts JSON or template syntax, and documents Liquid rendering, objects, filters, and syntax highlighting.

Estimated code review effort: 4 (Complex) | ~50 minutes

Possibly related PRs

  • seerr-team/seerr#2641: Changes the same webhook payload encoding and parsing paths for raw Liquid template handling.

Suggested reviewers: gauthier-th, fallenbagel

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: replacing the webhook KeyMap system with LiquidJS templating.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit writes Liquid with care,
Templates bloom in webhook air.
JSON hops from text to flight,
URLs render clean and bright.
🐰✨

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
server/lib/notifications/templateEngine.ts (2)

28-39: Numeric comparison helpers assume numeric inputs without validation.

The lt, gt, lte, gte helpers cast inputs to number but don't validate. Comparing non-numeric values (strings, objects) produces NaN comparisons that always return false, which could be confusing in templates.

🔧 Optional: Add type checking or documentation

Either add runtime validation:

 Handlebars.registerHelper('lt', (a: unknown, b: unknown) => {
+  if (typeof a !== 'number' || typeof b !== 'number') return false;
   return (a as number) < (b as number);
 });

Or document that these helpers are intended for numeric comparisons only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/notifications/templateEngine.ts` around lines 28 - 39, The numeric
comparison Handlebars helpers (lt, gt, lte, gte) currently cast inputs to number
without validation, causing NaN comparisons to always return false; update each
helper (Handlebars.registerHelper('lt'|'gt'|'lte'|'gte')) to coerce inputs
safely (e.g., use Number(...) or parseFloat(...)) and check isNaN on both
operands, then either return false or throw/log a descriptive error when values
are not numeric, otherwise perform the numeric comparison; ensure the change is
applied to all four helper registrations for consistent behavior and consider
adding a short comment documenting that these helpers expect numeric inputs.

98-192: Consider exposing additional payload fields to templates.

The NotificationPayload interface includes pendingRequestsCount and isAdmin fields that aren't mapped to the template context. These could be useful for conditional template content (e.g., showing pending count in admin notifications).

➕ Optional: Add missing context variables
       comment: payload.comment ?? null,
+
+      // Additional payload fields
+      pending_requests_count: payload.pendingRequestsCount ?? 0,
+      is_admin: payload.isAdmin ?? false,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/notifications/templateEngine.ts` around lines 98 - 192, The
buildContext method is missing mappings for
NotificationPayload.pendingRequestsCount and NotificationPayload.isAdmin; update
the static buildContext function to add context keys (e.g., pendingRequestsCount
and isAdmin or notifyuser_isAdmin) populated from payload.pendingRequestsCount
(number) and payload.isAdmin (boolean) so templates can use them, and ensure the
values are included alongside the other fields in the context object returned by
buildContext.
src/components/Settings/Notifications/NotificationsWebhook/index.tsx (1)

137-158: Validation allows syntactically invalid templates to be saved.

The dual-mode validation correctly detects Handlebars syntax, but templates with structural issues (unclosed blocks, mismatched braces, invalid JSON structure after rendering) will only fail at webhook send time with errors logged server-side. Users won't see validation feedback until they test.

This is acknowledged in the past review discussion—full frontend Handlebars validation would significantly increase bundle size. The current approach is a reasonable trade-off given that constraint, but consider adding a note in the UI or documentation that template syntax errors will only appear in server logs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/Notifications/NotificationsWebhook/index.tsx` around
lines 137 - 158, The validator for 'validate-json-or-template' in
NotificationsWebhook/index.tsx currently accepts any string containing
Handlebars markers even if the template has structural errors; update the UI to
inform users that Handlebars/template syntax errors are only validated
server-side by (a) adding or updating an intl message (used via
intl.formatMessage) that clearly states "Template syntax errors (unclosed
blocks, mismatched braces) are only reported by the server" and (b) rendering
that message near the payload input or validation hint in the
NotificationsWebhook component so users see it at save time; keep the existing
validator logic unchanged but surface the doc/notice via the same component that
uses validate-json-or-template.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/notifications/agents/webhook.ts`:
- Around line 37-44: The current JSON cleanup on renderedString (in
server/lib/notifications/agents/webhook.ts) uses regexes that can corrupt string
values; change the flow to first attempt a strict JSON.parse of renderedString
and only run the destructive regex cleanup as a recovery path if parsing fails,
then re-attempt JSON.parse and throw/log a clear error if it still fails; locate
the code that performs the replacements on renderedString and wrap them behind
the "parse first, cleanup on failure, parse again" logic so valid payloads are
never modified by the regexes.

In `@server/lib/notifications/templateEngine.ts`:
- Around line 76-80: The 'json' Handlebars helper (registered via
Handlebars.registerHelper('json', ...)) uses JSON.stringify directly and can
throw on circular references because full entities (media, request, issue,
comment) are in the template context; update the helper to safely stringify by
wrapping JSON.stringify in a try-catch and returning a safe fallback (e.g.,
"[Circular]" or "{}") on error, or replace the implementation to use a
circular-safe stringifier (e.g., fast-safe-stringify or a custom replacer that
tracks seen objects) so templates never crash when encountering bidirectional
ORM relations.

---

Nitpick comments:
In `@server/lib/notifications/templateEngine.ts`:
- Around line 28-39: The numeric comparison Handlebars helpers (lt, gt, lte,
gte) currently cast inputs to number without validation, causing NaN comparisons
to always return false; update each helper
(Handlebars.registerHelper('lt'|'gt'|'lte'|'gte')) to coerce inputs safely
(e.g., use Number(...) or parseFloat(...)) and check isNaN on both operands,
then either return false or throw/log a descriptive error when values are not
numeric, otherwise perform the numeric comparison; ensure the change is applied
to all four helper registrations for consistent behavior and consider adding a
short comment documenting that these helpers expect numeric inputs.
- Around line 98-192: The buildContext method is missing mappings for
NotificationPayload.pendingRequestsCount and NotificationPayload.isAdmin; update
the static buildContext function to add context keys (e.g., pendingRequestsCount
and isAdmin or notifyuser_isAdmin) populated from payload.pendingRequestsCount
(number) and payload.isAdmin (boolean) so templates can use them, and ensure the
values are included alongside the other fields in the context object returned by
buildContext.

In `@src/components/Settings/Notifications/NotificationsWebhook/index.tsx`:
- Around line 137-158: The validator for 'validate-json-or-template' in
NotificationsWebhook/index.tsx currently accepts any string containing
Handlebars markers even if the template has structural errors; update the UI to
inform users that Handlebars/template syntax errors are only validated
server-side by (a) adding or updating an intl message (used via
intl.formatMessage) that clearly states "Template syntax errors (unclosed
blocks, mismatched braces) are only reported by the server" and (b) rendering
that message near the payload input or validation hint in the
NotificationsWebhook component so users see it at save time; keep the existing
validator logic unchanged but surface the doc/notice via the same component that
uses validate-json-or-template.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf513910-243d-4b52-b2c1-891a296bd5bd

📥 Commits

Reviewing files that changed from the base of the PR and between a8c7e35 and 4644d24.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • package.json
  • server/lib/notifications/agents/webhook.ts
  • server/lib/notifications/templateEngine.ts
  • src/components/Settings/Notifications/NotificationsWebhook/index.tsx

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the webhook notification agent’s variable substitution from a custom KeyMap system to a centralized Handlebars-based templating engine, enabling richer template features in webhook URLs and JSON payloads.

Changes:

  • Added a new TemplateEngine (Handlebars + custom helpers) to render notification templates.
  • Updated the webhook agent to render payloads/URLs via TemplateEngine and added post-render JSON cleanup/fallback parsing.
  • Relaxed frontend webhook payload validation to allow Handlebars syntax while still validating strict JSON when no template syntax is detected.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/components/Settings/Notifications/NotificationsWebhook/index.tsx Adjusts Yup validation to permit Handlebars syntax in the JSON payload editor.
server/lib/notifications/templateEngine.ts Introduces Handlebars rendering + helper registration and context building for notifications.
server/lib/notifications/agents/webhook.ts Replaces KeyMap substitution with TemplateEngine rendering for payload and URL.
package.json Adds handlebars dependency.
pnpm-lock.yaml Lockfile updates for handlebars and related dependency graph changes.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@M0NsTeRRR M0NsTeRRR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,
Sorry for the delay here’s some feedback before I test it! :)

@mmenanno
mmenanno force-pushed the feature/refactor-webhook-handlebars branch from 4644d24 to 7dda3e7 Compare June 20, 2026 23:19
@github-actions github-actions Bot added blocked:template and removed merge conflict Cannot merge due to merge conflicts labels Jun 20, 2026
@mmenanno mmenanno changed the title refactor(notifications): replace webhook KeyMap with Handlebars templating refactor(notifications): replace webhook KeyMap with LiquidJS templating Jun 20, 2026

@M0NsTeRRR M0NsTeRRR left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey,

An other review before testing out your PR let me know if something isn't clear or does not make sense.

@Rombond

Rombond commented Jul 4, 2026

Copy link
Copy Markdown

Hey guys,

Any timeline for merging and releasing this PR?
Several other PRs were closed in favor of this one, and it's been open for a while.
Which release version or milestone is this targeting? 3.4.0 ?

@fallenbagel

Copy link
Copy Markdown
Member

Hey guys,

Any timeline for merging and releasing this PR?
Several other PRs were closed in favor of this one, and it's been open for a while.
Which release version or milestone is this targeting? 3.4.0 ?

No ETA. If this is already reviewed, then it will be included in 3.4.0 and you can find it in the roadmap issue that links to the milestones.

@mmenanno

mmenanno commented Jul 4, 2026

Copy link
Copy Markdown
Author

Hey guys,

Any timeline for merging and releasing this PR? Several other PRs were closed in favor of this one, and it's been open for a while. Which release version or milestone is this targeting? 3.4.0 ?

This PR recently became active again, during the seerr transition this PR was waiting on a review for a bit. I'm currently working on the feedback I got on the PR yesterday and should have that up soon. So there is active effort on this currently to get it over the finish line

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/routes/settings/notifications.ts (1)

302-309: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Mark options.jsonPayload as required
WebhookSettings only declares jsonPayload as a string, so /webhook and /webhook/test can still hit Buffer.from(req.body.options.jsonPayload) with missing input and return a 500. Add the field to required or guard before encoding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/routes/settings/notifications.ts` around lines 302 - 309, The webhook
settings update path is allowing settings.notifications.agents.webhook to encode
req.body.options.jsonPayload without guaranteeing it is present. Update the
/webhook and /webhook/test request validation to require options.jsonPayload, or
add a guard in the webhook handler before Buffer.from is called so missing input
is rejected with a client error instead of causing a 500. Use the webhook
settings update logic and the webhook/test route handlers to locate the fix.

Source: Learnings

🧹 Nitpick comments (1)
server/lib/notifications/agents/webhook.ts (1)

63-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fail-closed error handling for URL/payload rendering looks correct.

This addresses the earlier reviewer concern about not proceeding after a template render failure ("I don't think we should move forward if there's a problem"), and the request to catch/log template errors clearly.

The two try/catch blocks (URL render, payload build) are structurally identical (render → catch → log with same fields → return false). Consider extracting a small helper to reduce duplication, e.g.:

+function renderOrFail<T>(
+  fn: () => T,
+  errorLabel: string,
+  type: Notification,
+  payload: NotificationPayload
+): { ok: true; value: T } | { ok: false } {
+  try {
+    return { ok: true, value: fn() };
+  } catch (error) {
+    logger.error(errorLabel, {
+      label: 'Notifications',
+      type: Notification[type],
+      subject: payload.subject,
+      errorMessage: error instanceof Error ? error.message : 'Unknown error',
+    });
+    return { ok: false };
+  }
+}

Optional, purely a DRY cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/lib/notifications/agents/webhook.ts` around lines 63 - 93, The webhook
template error handling in webhook.ts already fail-closes correctly, but the
duplicated try/catch blocks around TemplateEngine.render and this.buildPayload
should be refactored into a small shared helper. Extract the common
“render/catch/log/return false” flow into a reusable function or method, and
keep the existing logger.error fields (label, type, subject, errorMessage)
unchanged so the webhook URL and payload paths use the same centralized logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/using-seerr/notifications/webhook.md`:
- Around line 50-52: Add a language identifier to the fenced code blocks in the
webhook markdown examples so markdownlint no longer reports MD040. Update the
affected code fences in the notification webhook docs to use a specific language
(for example, liquid) while keeping the existing content unchanged; the relevant
snippets are the templated blocks containing media_tmdbid and the media_status
conditional.

---

Outside diff comments:
In `@server/routes/settings/notifications.ts`:
- Around line 302-309: The webhook settings update path is allowing
settings.notifications.agents.webhook to encode req.body.options.jsonPayload
without guaranteeing it is present. Update the /webhook and /webhook/test
request validation to require options.jsonPayload, or add a guard in the webhook
handler before Buffer.from is called so missing input is rejected with a client
error instead of causing a 500. Use the webhook settings update logic and the
webhook/test route handlers to locate the fix.

---

Nitpick comments:
In `@server/lib/notifications/agents/webhook.ts`:
- Around line 63-93: The webhook template error handling in webhook.ts already
fail-closes correctly, but the duplicated try/catch blocks around
TemplateEngine.render and this.buildPayload should be refactored into a small
shared helper. Extract the common “render/catch/log/return false” flow into a
reusable function or method, and keep the existing logger.error fields (label,
type, subject, errorMessage) unchanged so the webhook URL and payload paths use
the same centralized logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05a64d18-3391-4525-86a9-bf8426532472

📥 Commits

Reviewing files that changed from the base of the PR and between 7dda3e7 and 6f333d1.

📒 Files selected for processing (6)
  • docs/using-seerr/notifications/webhook.md
  • server/lib/notifications/agents/webhook.ts
  • server/lib/notifications/templateEngine.test.ts
  • server/lib/settings/index.ts
  • server/lib/settings/migrations/0009_migrate_webhook_payload_encoding.ts
  • server/routes/settings/notifications.ts
✅ Files skipped from review due to trivial changes (1)
  • server/lib/settings/migrations/0009_migrate_webhook_payload_encoding.ts

@@ -0,0 +1,41 @@
import type { AllSettings } from '@server/lib/settings';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What is the purpose of this migration? Your AI seems to have hallucinated again. Please reread what was done here. Is this migration needed?
  2. Please read CONTRIBUTING.md this is not the way to generate db migration file if you need it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration unwraps a storage-format change in the save route, not anything about the removed frontend validation: on develop, POST /webhook stores Buffer.from(JSON.stringify(req.body.options.jsonPayload)), while this PR stores the raw text. Without it, existing installs decode to a quoted JSON string literal and the agent would post a string body instead of the rendered object.

On the second point this is a settings.json migration following the existing server/lib/settings/migrations/0001–0008 pattern (auto-run by migrator.ts), not a TypeORM db migration, so the CONTRIBUTING.md migration:generate flow doesn’t apply. I’ve expanded the file’s header comment to make this explicit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration unwraps a storage-format change in the save route, not anything about the removed frontend validation: on develop, POST /webhook stores Buffer.from(JSON.stringify(req.body.options.jsonPayload)), while this PR stores the raw text. Without it, existing installs decode to a quoted JSON string literal and the agent would post a string body instead of the rendered object.

Wouldn't raw text make settings.json way too long. Why aren't we storing like how we did before?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, "raw text" was poor wording on my part. It’s still base64 in settings.json, same as before. The only difference is that the PR drops the extra JSON.stringify layer:

  • develop: base64(JSON.stringify(template)), which is why the agent had to do JSON.parse(JSON.parse(...))
  • this PR: base64(template)

So the stored value actually gets slightly shorter: the default payload is 2700 chars versus 2936 before, because the escaped quotes and \ns are gone. The migration only exists to remove that extra layer from existing installs.

That said, if you’d rather not have a migration, I can keep the old base64(JSON.stringify(...)) format and unwrap it when reading. That leaves the storage format untouched and lets me delete 0009 entirely. Happy to go either way.

@mmenanno
mmenanno requested a review from M0NsTeRRR August 6, 2026 05:19
@mmenanno

mmenanno commented Sep 2, 2026 •

Copy link
Copy Markdown
Author

Hey @M0NsTeRRR maybe you are just busy but thought I'd mention this is read for review in case the last notification didn't come through. I noticed the pr still has the pending author's response label but I think I've responded to everything

@M0NsTeRRR

Copy link
Copy Markdown
Member

Hey, yes, we’re currently focusing on bug fixes, but your PR will continue to be reviewed once we’re done with these. :)

@J-J-E

J-J-E commented Oct 16, 2026

Copy link
Copy Markdown

@M0NsTeRRR Any idea when this might get picked up for review?

@fallenbagel fallenbagel changed the title refactor(notifications): replace webhook KeyMap with LiquidJS templating feat(notifications): replace webhook KeyMap with LiquidJS templating Oct 16, 2026
@M0NsTeRRR

M0NsTeRRR commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

As you can see an other maintainer added a comment #2039 (comment) and he is waiting for an answer from @mmenanno

@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Oct 28, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

Replace the bespoke KeyMap substitution in the webhook agent with a shared
LiquidJS-based TemplateEngine, laying groundwork for templating across all
notification agents.

- Add TemplateEngine (server/lib/notifications/templateEngine.ts) wrapping
  LiquidJS. Liquid provides conditionals, comparisons, and filters (default,
  upcase, truncate, json, url_encode, …) natively, so no custom helpers are
  needed. Liquid does not HTML-escape output, so apostrophes etc. render
  verbatim. The built-in json filter is overridden to be circular-safe and to
  log rather than drop data silently.
- buildContext exposes the same scalar variables as the previous KeyMap
  (including discordIds, media_imdbid, jellyfinMediaId, plexRatingKey and
  requestedBy_jellyfinUserId) plus the full media/request/issue/comment/extra
  values for conditionals.
- webhook agent renders the stored template to a string and lets JSON.parse
  validate the result; no regex payload cleanup. customHeaders support is
  retained.
- Relax server-side and client-side payload validation: the payload is now a
  Liquid template that need not be valid JSON until rendered. The default
  payload uses {% if %}…{% else %}null{% endif %} blocks so optional sections
  stay null when absent (matching prior behavior), and emits free-text values
  (titles, overviews, display names, comments) through the json filter so
  characters like quotes and newlines cannot break the surrounding JSON.
- URL variable substitution renders raw; use the url_encode filter where
  encoding is needed.
- Add unit tests for TemplateEngine (escaping, optional-section null handling,
  circular-safe json filter, and buildContext variable mapping).
…ad storage

Addresses review feedback on the LiquidJS webhook refactor:

- Fail closed when a webhook URL or payload template fails to render,
  logging a clear error instead of POSTing to an unrendered URL.
- Store the payload as a raw base64 template (drop the JSON.stringify/parse
  envelope) and add migration 0009 to re-encode existing settings.
- Fix the server-side default payload to the LiquidJS template so fresh
  installs render valid JSON.
- Document LiquidJS template syntax and link the NotificationPayload
  interface; mark the flat variables as deprecated.
- Add a test covering undefined template variables.
Adds a language identifier to the two bare fenced code blocks in the
webhook notification guide to satisfy markdownlint MD040, and registers
liquid as an additional Prism language so Docusaurus highlights them
without emitting an unknown-language warning.
- Make TemplateEngine.render async via parseAndRender, awaited in the
  already-async WebhookAgent.send
- Test WebhookAgent.send outcomes (rendered payload posted, render/parse
  failures, URL template failure, post failure) with mocked axios instead
  of exercising LiquidJS engine behavior
- Trim templateEngine tests to our own code: buildContext mapping and the
  circular-safe json filter
- Clarify the 0009 settings migration comment with the storage-format
  change that makes it necessary
- Link the intro-to-Liquid tutorial from the webhook docs
@mmenanno
mmenanno force-pushed the feature/refactor-webhook-handlebars branch from 73387c6 to 4483ec8 Compare October 29, 2026 05:52
@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Oct 29, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Notification content customization

7 participants