Skip to content

fix: isolate tag fetch failures to prevent sync abort on network errors#2681

Open
gitdeathbot wants to merge 3 commits intoseerr-team:developfrom
gitdeath:develop
Open

fix: isolate tag fetch failures to prevent sync abort on network errors#2681
gitdeathbot wants to merge 3 commits intoseerr-team:developfrom
gitdeath:develop

Conversation

@gitdeathbot
Copy link
Copy Markdown

@gitdeathbot gitdeathbot commented Mar 11, 2026

Description

When tagRequests is enabled and Radarr/Sonarr is momentarily unreachable, getTags() would throw and escape the outer try-catch in sendToRadarr/sendToSonarr. This caused the error to propagate to the afterInsert subscriber catch, which only logged it - leaving the request stuck in APPROVED state with no Retry button and the media status permanently in PENDING with no externalServiceId written.

Wrap the tagRequests block in its own try-catch in both sendToRadarr and sendToSonarr. A tag fetch failure now logs a warning and allows the sync to continue - the media is still added to the arr, just without the user tag.

fixes #2664

How Has This Been Tested?

Wrapping existing code, as is, in a try/catch - this is not a functionality change.

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
    AI wrote the description, the commit notes, etc. AI was used to quickly parse the existing project to find the functions that needed to be wrapped in try/catch. AI did tab complete things.
  • 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)

Summary by CodeRabbit

  • Bug Fixes
    • Media request tagging failures no longer abort requests; requests continue without tags when tag fetch/create fails.
    • Compatibility added for legacy and current tag label formats so existing labels are recognized.
    • Improved warning/logging for tag-related issues to aid troubleshooting without stopping processing.
    • Child-season approvals are now reliably persisted after parent approval to prevent lost approvals.

When tagRequests is enabled and Radarr/Sonarr is momentarily unreachable,
getTags() would throw and escape the outer try-catch in sendToRadarr/sendToSonarr.
This caused the error to propagate to the afterInsert subscriber catch, which only
logged it - leaving the request stuck in APPROVED state with no Retry button and
the media status permanently in PENDING with no externalServiceId written.

Wrap the tagRequests block in its own try-catch in both sendToRadarr and
sendToSonarr. A tag fetch failure now logs a warning and allows the sync to
continue - the media is still added to the arr, just without the user tag.
@gitdeathbot gitdeathbot requested a review from a team as a code owner March 11, 2026 20:56
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Wraps Radarr and Sonarr tag fetch/create flows in guarded try/catch blocks, adds dual-format tag label lookup (legacy "<userId> - " and new "<userId>-"), preserves tag creation when missing but logs warnings on failures, and awaits persistence when approving child seasons.

Changes

Cohort / File(s) Summary
Subscriber: tag handling & approval flow
server/subscriber/MediaRequestSubscriber.ts
Adds try/catch around Radarr and Sonarr tag fetch/create sequences; attempts both legacy (<userId> - ) and new (<userId>-) tag label formats; continues workflow when tag operations fail (logs warnings); changes seasonRequestRepository.save(season) to await seasonRequestRepository.save(season) when approving child seasons.

Sequence Diagram(s)

sequenceDiagram
    participant Subscriber as MediaRequestSubscriber
    participant Radarr as Radarr API
    participant Sonarr as Sonarr API
    participant Repo as seasonRequestRepository

    Subscriber->>Radarr: getTags()
    alt tags fetched
        Radarr-->>Subscriber: tags list
        Subscriber->>Radarr: find tag ("userId - " / "userId-")
        alt tag exists
            Radarr-->>Subscriber: tagId
        else not found
            Subscriber->>Radarr: createTag(label)
            Radarr-->>Subscriber: new tagId or error
        end
    else fetch/create error
        Radarr-->>Subscriber: error (logged), continue without tags
    end

    Subscriber->>Sonarr: getTags()
    alt tags fetched
        Sonarr-->>Subscriber: tags list
        Subscriber->>Sonarr: find tag ("userId - " / "userId-")
        alt tag exists
            Sonarr-->>Subscriber: tagId
        else not found
            Subscriber->>Sonarr: createTag(label)
            Sonarr-->>Subscriber: new tagId or error
        end
    else fetch/create error
        Sonarr-->>Subscriber: error (logged), continue without tags
    end

    Subscriber->>Repo: approve child seasons (await save)
    Repo-->>Subscriber: save complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Suggested labels

preview

Suggested reviewers

  • fallenbagel
  • 0xSysR3ll

Poem

🐇 I hopped through tags both old and new,
Fixed the snags where fetches blew,
A cautious catch, a gentle warn,
Child seasons saved before the dawn,
Hooray — the workflow bounds anew!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The PR includes one change outside the primary tag-failure scope: converting seasonRequestRepository.save() to await seasonRequestRepository.save() in updateParentStatus, which addresses async persistence but is tangentially related. Clarify whether the async persistence change for season approval is intentional and necessary, or if it should be separated into a distinct PR for clearer scope management.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: isolating tag fetch failures to prevent sync abort. It directly reflects the core fix in the changeset.
Linked Issues check ✅ Passed The PR successfully addresses issue #2664 by wrapping tag request blocks in try-catch for both Radarr and Sonarr, allowing syncs to continue even when tag fetching fails.

✏️ 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.

@gitdeathbot
Copy link
Copy Markdown
Author

fixes #2664

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
server/subscriber/MediaRequestSubscriber.ts (1)

303-355: Extract the Arr requester-tag flow into a shared helper.

These Radarr and Sonarr blocks are now effectively the same algorithm with different clients and log metadata. Pulling the lookup/create/append/warn path into one helper would reduce drift the next time the tag format, fallback order, or warning fields change.

Also applies to: 668-720

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

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 303 - 355, Extract
the duplicated Radarr/Sonarr requester-tag logic into a shared helper (e.g.,
ensureRequesterTag or addRequesterTag) that accepts the client (radarr/sonarr),
server settings (radarrSettings/sonarrSettings), the tags array, the request
entity (entity), logger, and sanitizeDisplayName; inside the helper implement
the lookup fallback (id + ' - ' then id + '-' ), create tag if missing, append
tag id to tags only if not present, and emit the same info/warn logs and return
success/failure (or the tag id) so callers in MediaRequestSubscriber can call
ensureRequesterTag(...) for both Radarr and Sonarr blocks instead of duplicating
lines around getTags/createTag, logging, and the tags push.
🤖 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/subscriber/MediaRequestSubscriber.ts`:
- Around line 307-314: The tag lookup currently prefers the legacy label "<id> -
" before the canonical "<id>-", causing new requests to attach to deprecated
tags; update the Radarr block (the userTag assignment using radarrTags.find and
entity.requestedBy.id) to first search for labels starting with
entity.requestedBy.id + '-' (no space) and only if not found fall back to
entity.requestedBy.id + ' - ' (with space), and apply the exact same swap to the
Sonarr lookup block (the analogous userTag/sonarrTags find logic around lines
~672-679) so canonical tags are preferred before legacy ones.

---

Nitpick comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 303-355: Extract the duplicated Radarr/Sonarr requester-tag logic
into a shared helper (e.g., ensureRequesterTag or addRequesterTag) that accepts
the client (radarr/sonarr), server settings (radarrSettings/sonarrSettings), the
tags array, the request entity (entity), logger, and sanitizeDisplayName; inside
the helper implement the lookup fallback (id + ' - ' then id + '-' ), create tag
if missing, append tag id to tags only if not present, and emit the same
info/warn logs and return success/failure (or the tag id) so callers in
MediaRequestSubscriber can call ensureRequesterTag(...) for both Radarr and
Sonarr blocks instead of duplicating lines around getTags/createTag, logging,
and the tags push.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78214159-f662-4564-a35d-8e6499d58d7c

📥 Commits

Reviewing files that changed from the base of the PR and between f047cab and bb84d4a.

📒 Files selected for processing (1)
  • server/subscriber/MediaRequestSubscriber.ts

Comment thread server/subscriber/MediaRequestSubscriber.ts
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.

Please re-edit your pr description to follow the pr template:
https://github.com/seerr-team/seerr/blob/develop/.github%2FPULL_REQUEST_TEMPLATE.md

mels0n added 2 commits March 26, 2026 15:08
This is not related to the tag bugfix, but rather an existing build issue for prettier
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/subscriber/MediaRequestSubscriber.ts`:
- Around line 958-961: Replace the non-async entity.seasons.forEach callback
with an async-aware loop so awaits actually run: iterate with for (const season
of entity.seasons) { season.status = MediaRequestStatus.APPROVED; await
seasonRequestRepository.save(season); } (or map to promises and await
Promise.all) and ensure the enclosing function (the one that calls this code,
e.g., updateParentStatus) is declared async so awaits are valid; keep references
to entity.seasons, season.status, MediaRequestStatus.APPROVED and
seasonRequestRepository.save when making the change.
🪄 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: 6217a836-30e3-4cf0-af67-c03517d9a667

📥 Commits

Reviewing files that changed from the base of the PR and between bb84d4a and df860f3.

📒 Files selected for processing (1)
  • server/subscriber/MediaRequestSubscriber.ts

Comment on lines 958 to 961
entity.seasons.forEach((season) => {
season.status = MediaRequestStatus.APPROVED;
seasonRequestRepository.save(season);
await seasonRequestRepository.save(season);
});
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
from pathlib import Path

path = Path("server/subscriber/MediaRequestSubscriber.ts")
lines = path.read_text().splitlines()
start, end = 953, 962

for lineno in range(start, end + 1):
    print(f"{lineno}: {lines[lineno - 1]}")

block = "\n".join(lines[start - 1 : end])
has_for_each = "entity.seasons.forEach(" in block
has_await_save = "await seasonRequestRepository.save(season);" in block
has_async_callback = "entity.seasons.forEach(async" in block

if has_for_each and has_await_save and not has_async_callback:
    raise SystemExit(
        "\nVerification failed: found `await` inside a non-async `forEach` callback."
    )

if has_for_each:
    raise SystemExit(
        "\nVerification failed: child season approvals still use `forEach`, so async saves are not awaited correctly."
    )

print("\nVerification passed.")
PY

Repository: seerr-team/seerr

Length of output: 480


Replace forEach with a for...of loop; the await on line 959 is invalid and still wouldn't wait.

Line 959 uses await inside the non-async callback opened on line 957, which is a syntax error. Even if that callback were changed to async, forEach would not wait for all season saves to finish before updateParentStatus() returns.

Suggested fix
-      entity.seasons.forEach((season) => {
-        season.status = MediaRequestStatus.APPROVED;
-        await seasonRequestRepository.save(season);
-      });
+      for (const season of entity.seasons) {
+        season.status = MediaRequestStatus.APPROVED;
+        await seasonRequestRepository.save(season);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
entity.seasons.forEach((season) => {
season.status = MediaRequestStatus.APPROVED;
seasonRequestRepository.save(season);
await seasonRequestRepository.save(season);
});
for (const season of entity.seasons) {
season.status = MediaRequestStatus.APPROVED;
await seasonRequestRepository.save(season);
}
🧰 Tools
🪛 Biome (2.4.9)

[error] 960-960: await is only allowed within async functions and at the top levels of modules.

(parse)

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

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 958 - 961, Replace
the non-async entity.seasons.forEach callback with an async-aware loop so awaits
actually run: iterate with for (const season of entity.seasons) { season.status
= MediaRequestStatus.APPROVED; await seasonRequestRepository.save(season); } (or
map to promises and await Promise.all) and ensure the enclosing function (the
one that calls this code, e.g., updateParentStatus) is declared async so awaits
are valid; keep references to entity.seasons, season.status,
MediaRequestStatus.APPROVED and seasonRequestRepository.save when making the
change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR needs a rebase as this is already fixed in the current develop so this entire fix of adding await here should be removed with merge conflict resolution

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@fallenbagel, thanks for the clarification!

@gitdeathbot — noted: the await-in-forEach fix already exists in develop, so when you rebase this PR onto develop you should simply drop this hunk during merge conflict resolution rather than carrying it forward. My original suggestion can be disregarded for the purposes of this PR.


🧠 Learnings used
Learnt from: gauthier-th
Repo: seerr-team/seerr PR: 2810
File: server/routes/issue.ts:278-279
Timestamp: 2026-04-12T14:02:31.091Z
Learning: In seerr-team/seerr, `IssueCommentSubscriber.ts` (server/subscriber/IssueCommentSubscriber.ts) re-fetches the IssueComment entity inside `sendIssueCommentNotification` using `getRepository(IssueComment).findOneOrFail({ where: { id: entity.id }, relations: { issue: { comments: true } } })` and does NOT rely on the subscriber event entity's relation data. Do not flag missing relation loading on the event entity in this subscriber.

Learnt from: fallenbagel
Repo: seerr-team/seerr PR: 2731
File: server/lib/i18n/extractMessages.ts:71-71
Timestamp: 2026-03-20T01:39:15.788Z
Learning: In seerr-team/seerr, the server-side i18n extraction script (`server/lib/i18n/extractMessages.ts`) intentionally calls `saveMessages()` without a `.catch()` handler, mirroring the pre-existing pattern in the frontend extractor. This is acceptable because Node.js 15+ exits with a non-zero code on unhandled promise rejections by default, ensuring CI correctly detects failures. Do not flag this as an issue in future reviews.

Learnt from: fallenbagel
Repo: seerr-team/seerr PR: 2810
File: server/routes/issue.ts:388-395
Timestamp: 2026-04-07T08:22:22.643Z
Learning: Repo seerr-team/seerr — Maintainers consider TOCTOU race conditions on issue delete (count-then-delete without a transaction/lock in server/routes/issue.ts) as overkill and irrelevant for this media request app. Do not suggest wrapping comment-count checks and deletes in a DB transaction or pessimistic locking for this code path.

Learnt from: Rubeanie
Repo: seerr-team/seerr PR: 2606
File: server/lib/availabilitySync.ts:998-1005
Timestamp: 2026-03-07T07:29:25.941Z
Learning: In `server/lib/availabilitySync.ts`, `seasonExistsInPlex()` and `seasonExistsInJellyfin()` intentionally return `true` (fail-open) when the episode fetch succeeds but returns an empty array. This is a deliberate design choice consistent with the broader fail-open pattern used throughout the class (e.g., catch blocks also return `true`), to avoid false negatives that would incorrectly mark media as deleted.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
server/subscriber/MediaRequestSubscriber.ts (1)

957-960: ⚠️ Potential issue | 🔴 Critical

Replace this forEach with an async-aware loop.

Line 959 is a parse error: await is not valid inside this non-async callback. Even if you mark the callback async, forEach still will not wait for the season saves before updateParentStatus() returns.

Suggested fix
-      entity.seasons.forEach((season) => {
-        season.status = MediaRequestStatus.APPROVED;
-        await seasonRequestRepository.save(season);
-      });
+      for (const season of entity.seasons) {
+        season.status = MediaRequestStatus.APPROVED;
+        await seasonRequestRepository.save(season);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/subscriber/MediaRequestSubscriber.ts` around lines 957 - 960, The code
is using entity.seasons.forEach with an await inside a callback which is invalid
and not waited on; replace the forEach block with an async-aware loop (e.g., a
for...of loop or Promise.all) that awaits seasonRequestRepository.save for each
season and only calls updateParentStatus() after all saves complete; locate the
block referencing entity.seasons, season.status = MediaRequestStatus.APPROVED,
and seasonRequestRepository.save and change to a loop that properly awaits each
save (or map to promises and await Promise.all) before proceeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/subscriber/MediaRequestSubscriber.ts`:
- Around line 957-960: The code is using entity.seasons.forEach with an await
inside a callback which is invalid and not waited on; replace the forEach block
with an async-aware loop (e.g., a for...of loop or Promise.all) that awaits
seasonRequestRepository.save for each season and only calls updateParentStatus()
after all saves complete; locate the block referencing entity.seasons,
season.status = MediaRequestStatus.APPROVED, and seasonRequestRepository.save
and change to a loop that properly awaits each save (or map to promises and
await Promise.all) before proceeding.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8fa49a1-ccdb-4e1b-af33-40def7481287

📥 Commits

Reviewing files that changed from the base of the PR and between df860f3 and 6633dff.

📒 Files selected for processing (1)
  • server/subscriber/MediaRequestSubscriber.ts

@fallenbagel fallenbagel added this to the v3.2.0 milestone Mar 28, 2026
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Apr 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

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

@fallenbagel
Copy link
Copy Markdown
Collaborator

@gitdeathbot Would it be possible for you to resolve the merge conflicts? Or should we take over this PR?

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

Labels

merge conflict Cannot merge due to merge conflicts pending author's response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possible tag.Requests error handling issue

5 participants