page_statuses still terminates on its first empty page — is /statuses/{sha} subject to #870's post-pagination filtering?
#893
Closed
opened 2026-08-30 10:57:49 +02:00 by timothy
·
2 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
release/v26.15.0-notes
fix/830-add-items-error-surface
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
renovate/dotnet-monorepo
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#893
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Split out of #870, which fixed the SAME defect shape one endpoint over and deliberately did not
touch this walk.
The asymmetry #870 created
count_pr_mutationsno longer treats an empty page as the end of the list: it reads every page toits 20-page cap and trusts the counts only when the LAST page came back empty.
page_statusesstill returns on its FIRST empty page:
The two walks now share only the CAP rule, not a termination rule. #870 originally claimed in prose
that "the two now agree"; cold review caught that and it was corrected rather than shipped.
Why this is a real question and not tidiness
#870's defect was not about empty pages in general — it was about ONE endpoint's handler.
ListIssueCommentsAndTimelineapplies LIMIT/OFFSET at the database level and filtersCommentTypeCoderows out AFTERWARDS into a nil slice, so a page of 50 inline comments isbyte-identical to a page past the end. If
/statuses/{sha}filters after paging too, thenpage_statusestruncating early can drop a raced humanReview-verdict:row fromph_rows—which by that file's own account is the worst outcome this gate can produce, since the post-write
check would then conclude
raced=0and leave a machinesuccessstanding over a human rejection.What is measured, and what is not
Measured on this instance at Gitea 1.27.1 on 2026-08-30:
?limit=1?limit=50/issues/{n}/timeline(14-row PR)X-Total-Count: 1X-Total-Count: 14/statuses/{sha}(105-row head)X-Total-Count: 105X-Total-Count: 105A true total is consistent with counting before filtering, and so with a terminator that means what
it says. That is evidence, not proof — it shows the count is not computed from the serialized
page, which is what makes the timeline endpoint's terminator a lie; it does not exhibit the absence
of a filtering predicate in
ListStatuses. Two independent reviews of #870 read the Gitea source asnot filtering there, and neither treated that as settling it.
State it as the open question it is: nobody has exhibited a filtering predicate on this endpoint
either way.
Options
ListStatuses/GetLatestCommitStatusand saywhether any row is dropped after the LIMIT/OFFSET. Cheapest, and it either closes this outright
or turns it into a live defect.
terminator nobody should trust. Costs 20 requests per walk on a walk that already retries each
page, and
page_statusesruns on the post-write path where latency is least welcome.dated sentence in
ci.verdict-write-retarget-fencesaying the two walks differ because the twoendpoints differ, so the next reader does not "tidy" them together.
There is a real argument for the asymmetry: an empty page 1 is ANOMALOUS on a PR timeline (a PR is
created by a push, which is an event) and ORDINARY on
/statuses/{sha}(a head nothing has postedto yet). The walks already diverge there deliberately, and that divergence is documented.
Note the
X-Total-Countmeasurement is itself worth keeping whatever the outcome: it is aderivable page count this walk does not use, and it would give
page_statusesa terminator thatdoes not depend on interpreting an empty page at all.
Done-when
/statuses/{sha}drops rows after pagination is ESTABLISHED from the 1.27.1 source,not inferred from the header measurement above — it does NOT:
getCommitStatusesappendsunconditionally and its only filter is a SQL
WHEREin the same query as the LIMIT/OFFSETIf it does:N/A — branch not taken. Ticked topage_statusesgets #870's treatment...record that it was resolved, not done: option 1 showed no post-pagination filtering, so there
is no live defect and no fixture to write.
page_statusesis untouched.ci.verdict-write-retarget-fencestates which of the two it is, replacing #870's pointer hereattacked this conclusion specifically and none found a short/empty-page path. Final: CLEAN
Claiming — Claude Code session (Opus 5), bundled with #869.
Same mechanism as #869: establish a claim from the v1.27.1 Gitea source rather than inferring it. Here that is option 1 — read
ListStatuses/GetLatestCommitStatusand say whether any row is dropped after the LIMIT/OFFSET — which per this issue either closes it outright or turns it into a live defect inpage_statuses.Closing record
Outcome: Option 1 taken and it closed the question outright — no live defect. PR #905.
GET /repos/{o}/{r}/statuses/{sha}does NOT drop rows after pagination at v1.27.1, sopage_statusesterminating on its FIRST empty page is safe and the asymmetry withcount_pr_mutationsis correct. Recorded with its reason and a date (option 3) so it is not tidied away.Established from source, not inferred from the header:
repo.GetCommitStatuses→getCommitStatusescallsdb.FindAndCount[git_model.CommitStatus], then builds the response with an unconditionalappendloop. Nocontinue, no predicate, no nil-drop —convert.ToCommitStatusreturns a pointer for every row it is handed.CommitStatusOptions.ToConds()is the only filter —repo_id,sha, optionalstate— and it is a SQLWHEREthe database evaluates in the same query as theLIMIT/OFFSET, never a pass over rows after they return.ListIssueCommentsAndTimelinepages at the DB level and then appends conditionally on two predicates —comment.Type != CommentTypeCodeandisXRefCommentAccessible(...), a per-viewer check — which is what makes a fully filtered page byte-identical to the end of the list.Root cause: #870 fixed a defect in one endpoint's handler and correctly declined to generalise to another. The open question was whether the two handlers shared the defect; nobody had read the second one. They do not.
Decisions/conventions changed: no new keys.
ci.verdict-write-retarget-fencenow states which of the two it is, replacing this issue's pointer.Reusable knowledge:
getCommitStatusesbuildsmake([]*api.CommitStatus, 0, len(statuses))— a non-nil empty slice serializing as[]; the timeline declaresvar apiComments []*api.TimelineComment— a nil slice serializing as barenull. The repo had this as an empirical table; it is now checkable in the source instead of by constructing a past-the-end request.X-Total-Countis now explained on both sides./statuses/{sha}sets it fromFindAndCount's SQL COUNT (a true total); the timeline sets it toint64(len(apiComments))— literally the filtered page length, which is why that header cannot derive a page count there. Re-confirmed live where the two must differ: page 1 of 50 on a head reportingX-Total-Count: 63.X-Total-Countwould givepage_statusesa terminator that does not depend on interpreting an empty page at all. Not adopted here — the current terminator is now known-sound — but it is the cheaper option if this ever needs hardening.Verification: source read at tag
v1.27.1(handlers, model options,db.FindAndCount, both serializers); live re-confirmation of the header behaviour;scripts/tests1565 passed / 3 skipped. Three independent cold reviewers each attacked the conclusion specifically — looking for a filter, permission check, nil-drop, alternate handler,state/sortparam or ListAll path that could return a short/empty page with rows beyond — and none found one. No executable change:page_statusesis untouched.Deferred: none.
Docs updated:
docs/decisions/records/ci/verdict-write-retarget-fence.md,docs/ci-cd.md.