Skip to content

fix(jellyfin): clamp negative StartIndex to zero - #6341 - #6366

Merged
deluan merged 4 commits into
navidrome:masterfrom
santhiprakash:fixing-negative-startindex/6341
Nov 10, 2026
Merged

deluan merged 4 commits into
navidrome:masterfrom
santhiprakash:fixing-negative-startindex/6341

Conversation

@santhiprakash

@santhiprakash santhiprakash commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

GET /Items?IncludeItemTypes=Audio,MusicAlbum&StartIndex=-1&Limit=5 panics with runtime error: slice bounds out of range [-1:] (recovered as an HTTP 500), even on an empty library.

Root cause: StartIndex is read with no lower bound and reaches paginate, whose offset >= len(items) check only guards the upper bound before items[offset:] slices. A negative StartIndex also under-widens the per-type merge window (window := offset + q.limit) and is echoed back verbatim in the response's StartIndex field.

The same unguarded read reaches paginate from two other endpoints — /Genres (via listGenres) and /Studios — so StartIndex=-1 panics there too. /Playlists/{id}/Items and single-type /Items push the offset down to SQL instead, so they cannot panic, but they still echoed the negative value.

Fix: clamp StartIndex to >= 0 at every startindex read (listParams, getGenres, getStudios, getPlaylistItems) — the same clamp idiom the codebase already uses for limit — and guard paginate itself so no current or future caller can slice with a negative offset. A negative StartIndex now behaves exactly like StartIndex=0 on all of these endpoints.

Related Issues

Closes #6341

Prior agreement is not needed here — this falls under the small-change exception: a small, unambiguous fix restoring established behavior (a panic recovered as HTTP 500), with reproduction evidence under How to Test.

Type of Change

  • Bug fix

Checklist

  • My code follows the project's coding style
  • I have tested the changes locally
  • I have added or updated documentation as needed
  • I have added tests that prove my fix/feature works (or explain why not)
  • All existing and new tests pass

How to Test

New regression specs fail before the fix and pass after:

  • server/jellyfin/items_test.go: multi-type /Items?IncludeItemTypes=Audio,MusicAlbum&StartIndex=-1&Limit=2 (the reported panic), plus a single-type /Items?...StartIndex=-1 control
  • server/jellyfin/browsing_test.go: /Genres?StartIndex=-1 and /Studios?StartIndex=-1
  • server/jellyfin/playlists_test.go: /Playlists/{id}/Items?StartIndex=-1

Verified on current master (df77c462):

  • Before: all 5 new specs failed — 4 panicked with slice bounds out of range [-1:], the 5th showed Offset/StartIndex propagating as -1.
  • After: go test ./server/jellyfin/ → ok (510 specs); go test -tags netgo,sqlite_fts5 ./... → all packages ok; go vet -tags netgo,sqlite_fts5 ./... → clean.
  • (go test/go vet without build tags fail on clean master too — the project requires the make test tags for main.go/FTS5; unrelated to this change.)

Screenshots / Demos (if applicable)

N/A — backend/API change, no UI.

Additional Notes

  • Open PR fix(jellyfin): list the user's libraries at the /Items user root #6214 touches the same files but clamps StartIndex only inside its new userRoot branch; it does not touch listParams, paginate, or these handlers, so this change is disjoint — a rebase after it merges should be clean.
  • The SQL-offset paths could not panic because the persistence layer only applies Offset when it is > 0; clamping there keeps semantics consistent and makes the echoed StartIndex honest.
  • chained's skip loop already behaved like offset 0 for a negative skip (n < skip is never true), so the unbounded multi-type merge path was safe.

Assistance

AI use (check one):

  • Yes, AI helped generate or modify code or tests, draft substantive PR text, or create this PR
  • No

AI tools helped draft this change and this description; the verification steps were actually run.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Jellyfin browsing requests for genres, studios, items, and playlists that use a negative start index.
    • Negative start indexes are now normalized to zero, so requests return results from the beginning instead of failing.
    • Pagination handles very large start indexes safely for item searches, while preserving the requested result limit.
    • These adjustments make pagination behavior more consistent across the supported browsing requests.

@strix-security

strix-security Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Strix Security Review

✅ No open security findings.

PR summary: Clamps a negative client-supplied StartIndex to zero in the Jellyfin pagination handlers (items, genres, studios, playlist items), adds a guard in the in-memory paginate helper, and makes the multi-type /Items search window overflow-safe, with regression specs for each endpoint.

Review summary

The PR clamps a client-supplied negative StartIndex to zero across the Jellyfin /Items, /Genres, /Studios and /Playlists/{id}/Items pagination reads and adds a defensive non-negative guard inside paginate. Verified that this closes the previously reported StartIndex-overflow panic on the multi-type search merge (offset is capped before the sum and search limits are clamped to [1, maxSearchLimit]), so that finding is resolved. One issue remains: the new shared pageParams helper still forwards Limit unvalidated, so on the non-search multi-type /Items merge a negative Limit or an overflowing StartIndex+Limit wraps the per-type window to a non-positive value, which persistence treats as "no LIMIT" — every row of every merged type is then drained and materialised in memory instead of taking the streaming path. This is reported as "Unvalidated Limit and StartIndex+Limit overflow force full-library materialisation on multi-type /Items". Library scoping for the edited /Studios handler and the SQL-offset paths (single-type /Items, playlists, genres) were traced and found sound.

Ruled out (1)
  • Does the edited /Studios handler return record labels from libraries the caller cannot access?
    The tag repository always applies its own user_library INNER JOIN, so an empty library grant yields zero rows rather than every library's labels.
Review history (4)
  • Review 4 · ef563a8 · Nov 10 21:07 UTC · Nothing new
  • Review 3 · 872a296 · Nov 10 20:49 UTC · Found 1 new Medium Vulnerability
  • Review 2 · a57467e · Nov 8 22:27 UTC · Nothing new
  • Review 1 · 8cc6178 · Oct 21 12:13 UTC · Nothing new

Review 4 on this PR · ef563a8 · requested Nov 10 20:56 UTC on a new commit


Reviewed by Strix
Re-run review · Configure security review settings

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T20:58:56.035921Z ef563a8 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Jellyfin item queries, genre and studio listings, and playlist item requests now normalize negative StartIndex values to zero. Regression tests cover these requests and large multi-type query offsets.

Changes: Jellyfin pagination

Layer / File(s) Summary
Item query and pagination
server/jellyfin/items.go, server/jellyfin/items_test.go
Item query parameters and pagination clamp negative offsets to zero. Multi-type search windows cap the offset before adding the limit. Tests cover negative offsets and large offsets, including math.MaxInt.
Browsing and playlist pagination
server/jellyfin/browsing.go, server/jellyfin/browsing_test.go, server/jellyfin/playlists.go, server/jellyfin/playlists_test.go
Genre, studio, and playlist item endpoints use the shared pagination helper. Tests verify successful responses and normalized indexes or query offsets.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Suggested reviewers: deluan

Merge Risk: Minimal · up to ef563

The pagination change is mergeable after normal checks; no new blocking risk was established.

Pre-merge checks: Passed 5

Check name Status Explanation
Title check Passed The title clearly summarizes the main change: clamping negative Jellyfin StartIndex values to zero.
Description check Passed The description includes the change, root cause, related issue, bug-fix type, testing steps and results, and relevant notes. It follows the template and provides sufficient detail.
Linked Issues check Passed Issue #6341 requires negative StartIndex on multi-type /Items to behave as zero or return HTTP 400, without panicking. listParams now gets pagination values from pageParams, and paginate cla…
Out of Scope Changes check Passed The changes support the pagination fix in #6341. The maxSearchLimit window cap and its large-offset tests protect the same multi-type pagination path. The endpoint changes and regression tests cover…
Docstring Coverage Passed Docstring coverage is inconclusive: files from earlier reviews still need to be rechecked within the check's analysis limits. Not blocking.
Full details: Linked Issues check

Explanation

Issue #6341 requires negative StartIndex on multi-type /Items to behave as zero or return HTTP 400, without panicking. listParams now gets pagination values from pageParams, and paginate clamps negative offsets before slicing. The summaries also show the shared helper is used by /Genres, /Studios, and /Playlists/{id}/Items. Regression tests cover these paths and single-type /Items, including normalized offsets. The change meets the linked issue requirement.

Full details: Out of Scope Changes check

Explanation

The changes support the pagination fix in #6341. The maxSearchLimit window cap and its large-offset tests protect the same multi-type pagination path. The endpoint changes and regression tests cover related paths identified in the issue and PR description. No unrelated change is demonstrated.

Finishing Touches

Generate unit tests (beta)

  • Create a new PR
  • 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

Recent review info

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 10a79ee3-26e8-465f-a5ea-9c85fa4beb7e
Commits

Reviewing files that changed from the base of the PR and between 872a296 and ef563a8.

Files selected for processing: 2

  • server/jellyfin/items.go
  • server/jellyfin/items_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

@santhiprakash

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@santhiprakash

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Code Metrics Report

master (27d0414) #6366 (ef563a8) +/-
Coverage 75.7% 75.6% -0.1%
Test Execution Time 3m16s 3m8s -8s
Details
  |                     | master (27d0414) | #6366 (ef563a8) |  +/-  |
  |---------------------|------------------|-----------------|-------|
- | Coverage            |            75.7% |           75.6% | -0.1% |
  |   Files             |              464 |             464 |     0 |
  |   Lines             |            30100 |           30105 |    +5 |
+ |   Covered           |            22787 |           22788 |    +1 |
+ | Test Execution Time |            3m16s |            3m8s |   -8s |

Code coverage of files in pull request scope (82.2% → 82.0%, patch 100.0%)

Files Coverage +/- Patch Coverage Status
core/external/provider_similarsongs.go 86.1% -0.8% - affected
scanner/walk_dir_tree.go 85.7% +0.5% - affected
server/auth.go 80.9% -1.0% - affected
server/jellyfin/browsing.go 79.0% +0.3% 100.0% modified
server/jellyfin/items.go 88.8% +0.0% 100.0% modified
server/jellyfin/playlists.go 62.6% +0.1% 100.0% modified
server/middlewares.go 81.8% +0.7% - affected
server/subsonic/library_scanning.go 84.1% +1.5% - affected
utils/singleton/singleton.go 67.6% -11.8% - affected

Reported by octocov

… helper

The negative-StartIndex clamp was repeated in getItems (listParams), getGenres,
getStudios and getPlaylistItems. Move the StartIndex/Limit parsing into one
pageParams helper so a new paginated endpoint gets the clamp by calling it,
instead of having to copy max(..., 0). Behavior is unchanged: Limit is read
as before, and the existing negative-StartIndex specs cover every caller.
…tartindex/6341

Signed-off-by: Santhi Prakash <[email protected]>

# Conflicts:
#	server/jellyfin/items.go
@deluan

deluan commented Nov 10, 2026

Copy link
Copy Markdown
Member

Thanks for the fix and the thorough specs, @santhiprakash! I pushed a small follow-up commit that moves the StartIndex/Limit parsing into a shared pageParams helper. That way new paginated endpoints get the clamp without having to copy max(..., 0). Behavior is the same, and your specs cover every caller.

@deluan

deluan commented Nov 10, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 872a29696c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

strix-security[bot]

This comment was marked as resolved.

…ge window

On a multi-type /Items search, mergeTypesPaged computed the per-type window as
offset+limit and only then capped it at maxSearchLimit. A StartIndex near the
int limit wrapped that sum negative, the cap kept the negative value, and
searchPage sliced rows[:Max] with it inside an errgroup worker, where the HTTP
Recoverer cannot catch the panic, so one request stopped the server.

Cap the offset before adding the limit. The search limit is already bounded by
clampLimit, so the sum stays small and the window is unchanged for every
in-range request.
@deluan

deluan commented Nov 10, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: ef563a8a92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@deluan
deluan merged commit 64cf86f into navidrome:master Nov 10, 2026
42 checks passed
@navidrome navidrome deleted a comment from strix-security Bot Nov 10, 2026
deluan added a commit to zapisanchez/navidrome that referenced this pull request Nov 10, 2026
Resolve the semantic conflicts with master. navidrome#6414 replaced artistId with
artistIds, so hasFilters now checks len(q.artistIds). navidrome#6414 also pairs each
SortOrder value with its SortBy key, so sortViews uses listParts and
sortDirection the same way applySort does, and a new spec covers the pairing.
navidrome#6366 clamps StartIndex in pageParams, so the user-root branch drops its own
max(q.offset, 0).
@santhiprakash
santhiprakash deleted the fixing-negative-startindex/6341 branch November 11, 2026 20:34
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.

[Bug]: Jellyfin API: negative StartIndex on a multi-type /Items request panics

2 participants