Skip to content

fix: normalize webhook payload encoding to support raw JSON inputs#2641

Merged
fallenbagel merged 4 commits intoseerr-team:developfrom
aldoeliacim:fix/webhook-double-json-parse
Apr 16, 2026
Merged

fix: normalize webhook payload encoding to support raw JSON inputs#2641
fallenbagel merged 4 commits intoseerr-team:developfrom
aldoeliacim:fix/webhook-double-json-parse

Conversation

@aldoeliacim
Copy link
Copy Markdown
Contributor

@aldoeliacim aldoeliacim commented Mar 6, 2026

Description

Webhook notifications fail with "[object Object]" is not valid JSON for direct API callers that save a webhook payload as raw JSON text.

buildPayload() in webhook.ts expects a double-encoded payload (base64 -> JSON string -> JSON object):

const parsedJSON = JSON.parse(JSON.parse(payloadString));

But the save routes (POST /webhook and POST /webhook/test) only single-encoded the payload:

jsonPayload: Buffer.from(req.body.options.jsonPayload).toString("base64")

For direct API callers, that stores raw JSON in base64. On delivery, the first JSON.parse produces an object and the second parse fails with "[object Object]" is not valid JSON.

The UI was previously compensating client-side by calling JSON.stringify(values.jsonPayload) before sending, which is why the UI flow already worked. This PR moves that normalization to the server so both UI and direct API callers follow the same path.

This change:

How Has This Been Tested?

  1. Save a custom webhook payload through the API using raw JSON text
  2. Trigger a test notification -> succeeds (HTTP 204)
  3. Save and test the same payload through the UI -> still succeeds after removing the frontend stringify
  4. Trigger a real notification (e.g. media request) -> webhook fires correctly
  5. Verified the default payload still works (fresh install)

Screenshots / Logs (if applicable)

N/A

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

AI Disclosure

This PR was authored by @aldoeliacim with the help of Claude Code. The bug was discovered while debugging a production Jellyseerr v3.1.0 webhook integration.

Summary by CodeRabbit

Bug Fixes

  • Fixed webhook payload encoding to ensure proper formatting and delivery of webhook data to external endpoints.

The webhook `buildPayload` method correctly double-parses the stored
jsonPayload (`JSON.parse(JSON.parse(payloadString))`), expecting a
base64-encoded JSON string that wraps the actual JSON template.

However, the POST /webhook and POST /webhook/test routes only
single-encode the payload:
  Buffer.from(req.body.options.jsonPayload).toString("base64")

This produces a base64 string that decodes directly to the JSON object
string. When buildPayload double-parses it, the first parse yields a
JS object, and the second parse calls toString() on that object,
producing "[object Object]" which is not valid JSON.

The fix wraps the payload with JSON.stringify() before base64-encoding,
matching the double-encoded format that buildPayload expects:
  Buffer.from(JSON.stringify(req.body.options.jsonPayload)).toString("base64")

This is consistent with the default jsonPayload in settings/index.ts,
which is already stored in this double-encoded format.

Note: the default payload works because it was hardcoded as a
double-encoded base64 string. Only payloads saved via the API
(i.e., any user customization) trigger this bug.
Copilot AI review requested due to automatic review settings March 6, 2026 07:06
@aldoeliacim aldoeliacim requested a review from a team as a code owner March 6, 2026 07:06
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

Server routes now JSON.stringify() the webhook JSON payload before base64-encoding and storing; the Settings UI no longer stringifies the payload before sending, so the server receives the raw value and normalizes encoding expected by the webhook agent.

Changes

Cohort / File(s) Summary
Server: webhook save endpoints
server/routes/settings/notifications.ts
Wrap req.body.options.jsonPayload with JSON.stringify() before Buffer.from(...).toString('base64') in POST /webhook and POST /webhook/test, aligning storage format with consumer expectations.
Client: webhook UI
src/components/Settings/Notifications/NotificationsWebhook/index.tsx
Removed JSON.stringify when constructing options.jsonPayload for onSubmit and testSettings; UI now sends raw payload so server performs stringify+base64 encoding.

Sequence Diagram(s)

sequenceDiagram
  participant UI as UI (Settings Webhook)
  participant Server as Server (routes/settings/notifications)
  participant DB as DB (stored config)
  participant Agent as Webhook Agent (buildPayload)
  participant Ext as External Webhook
  UI->>Server: POST webhook payload (raw object/string)
  Server->>Server: JSON.stringify(payload) -> base64 encode -> store
  Server->>DB: save encoded payload
  Note right of Agent: On notification trigger
  Agent->>DB: read encoded payload
  Agent->>Agent: base64 decode -> JSON.parse -> JSON.parse -> final object
  Agent->>Ext: POST final JSON payload
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped through code with a curious squeak,
Wrapped payloads in strings so webhooks won't freak,
UI sends the object, the server does the rest,
Double-parse no longer turns JSON to jest,
Now notifications hop along—neat and sleek! 🥕

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #2640: server-side JSON.stringify() added to both webhook save routes, frontend redundant stringify removed, and double-encoding normalized server-side for UI and API consistency.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the webhook double-JSON-parse issue: modifications only affect webhook payload encoding in server routes and frontend submission logic.
Title check ✅ Passed The title accurately describes the main change: fixing webhook payload encoding to normalize inputs and support raw JSON by having the server handle JSON.stringify before base64-encoding.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 attempts to fix issue #2640 where webhook notifications fail with "[object Object] is not valid JSON". The fix adds JSON.stringify() wrapping before base64-encoding the jsonPayload in both the save and test webhook routes, aiming to produce the double-encoded format that buildPayload() in webhook.ts expects.

Changes:

  • Wraps req.body.options.jsonPayload with JSON.stringify() before base64-encoding in the POST /webhook save route
  • Applies the same JSON.stringify() wrapping in the POST /webhook/test test route

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

You can also share your feedback on Copilot code review. Take the survey.

Comment thread server/routes/settings/notifications.ts
Comment thread server/routes/settings/notifications.ts
Copy link
Copy Markdown
Member

@gauthier-th gauthier-th left a comment

Choose a reason for hiding this comment

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

Please edit the PR description according to the PR template in https://github.com/seerr-team/seerr/blob/develop/.github/PULL_REQUEST_TEMPLATE.md

The UI's onSubmit and testSettings handlers were wrapping
jsonPayload with JSON.stringify() before sending to the API.
Combined with the server-side JSON.stringify added in the
previous commit, this produced triple-encoding (Level 3)
that buildPayload()'s double-parse couldn't handle.

Remove the frontend stringify so the server is the single
point of encoding normalization — both UI and direct API
callers now go through the same path.
@aldoeliacim
Copy link
Copy Markdown
Contributor Author

Updated the PR description to follow the template. Also addressed Copilot's review — the frontend was redundantly calling JSON.stringify before sending, which combined with the server-side fix would cause triple-encoding. Removed the frontend stringify in 0285fee so the server handles all encoding normalization.

@aldoeliacim aldoeliacim requested a review from gauthier-th March 6, 2026 14:56
@fallenbagel fallenbagel added this to the v3.2.0 milestone Mar 28, 2026
Copy link
Copy Markdown
Collaborator

@fallenbagel fallenbagel left a comment

Choose a reason for hiding this comment

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

The description saying this affects "any webhook configuration saved via the API or UI" is inaccurate. The UI is not affected. The frontend already wraps with JSON.stringify(values.jsonPayload) before sending (line 227 and line 294, so saving and testing webhooks through the UI works fine. I can confirm this on my seerr instances on both dev branch and main branch and has always worked as well.

It only affects direct API callers because the server doesn't JSON.stringify before encoding in base64, so the payload ends up single-encoded instead of the double-encoded format that the buildPayload()'s JSON.parse(JSON.parse(...)) expect. The UI just so happens to compensate for this on the client side and is why it works.

That said, the fix is valid for ensuring direct API callers work correctly, and moving the encoding responsibility to the server is cleaner too imo, however, please update the PR description and issue #2640 to accurately reflect the actual scope.

@aldoeliacim
Copy link
Copy Markdown
Contributor Author

aldoeliacim commented Apr 16, 2026

Updated the PR description and issue #2640 to clarify the original scope: the broken path was direct API callers sending raw jsonPayload text, while the UI had been compensating client-side by pre-stringifying before submit. This PR keeps the UI working but moves encoding normalization to the server so both UI and API callers now go through the same path.

@aldoeliacim aldoeliacim requested a review from fallenbagel April 16, 2026 06:34
Copy link
Copy Markdown
Collaborator

@fallenbagel fallenbagel left a comment

Choose a reason for hiding this comment

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

LGTM

@seerr-automation-bot seerr-automation-bot added this to the v3.2.1 milestone Apr 16, 2026
@fallenbagel fallenbagel changed the title fix: webhook notifications fail with "[object Object] is not valid JSON" fix: normalize webhook payload encoding to support raw JSON inputs Apr 16, 2026
@fallenbagel fallenbagel dismissed gauthier-th’s stale review April 16, 2026 08:13

Pr template has been updated

@fallenbagel fallenbagel merged commit 5b45806 into seerr-team:develop Apr 16, 2026
19 checks passed
lucianchauvin pushed a commit to lucianchauvin/jellyseerr that referenced this pull request Apr 20, 2026
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.

Webhook notifications fail with "[object Object]" is not valid JSON after saving via API

6 participants