Skip to content

fix(feed): bound the stream catch-up replay to the newest items - #134

Merged
lopatnov merged 4 commits into
mainfrom
refactor/maintain-stream-catchup-bound
Sep 5, 2026
Merged

fix(feed): bound the stream catch-up replay to the newest items#134
lopatnov merged 4 commits into
mainfrom
refactor/maintain-stream-catchup-bound

Conversation

@lopatnov

@lopatnov lopatnov commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • StreamFeedUpdates's catch-up replay (for a client's since_timestamp on reconnect) had no cap — a client-supplied, unvalidated, old (or hostile) since_timestamp could make the server materialize the caller's entire retained history into a List and write all of it to the stream, the one listing path in the API without the ClampPageSize discipline every other one follows
  • Caps the replay at the newest 100 items (MaxCatchUpItems, matching PagingDefaults.MaxPageSize); delivery order is unchanged — the query orders newest-first so the cap keeps the newest items, and the write loop walks the capped list in reverse to preserve oldest-first delivery (the client prepends each item as it arrives, so the newest lands on top last)

Related issue

Found by architect during this cycle's /maintain refactor analysis. A dedicated security-engineer glance on this specific fix (requested since it's security-adjacent) approved it as-is — cap value matches existing pagination conventions, the query is fully index-backed (Subscription.UserId, FeedItem.SubscriptionId, FeedItem.(PublishedAt, Id)), the endpoint requires authentication ([Authorize] at the class level, no [AllowAnonymous] override), and since_timestamp parsing already fails safely via a TryParse guard. That review separately flagged a genuine but distinct pre-existing gap — no rate limiting on this streaming endpoint against reconnect-spam — filed as #132, out of scope for this targeted fix.

Type of change

  • Bug fix
  • Security (resource-exhaustion / memory-amplification hardening)

Checklist

  • dotnet build --configuration Release passes with 0 errors, 0 warnings
  • cd src/pressmark-web && npm run build passes with 0 TypeScript errors (unaffected by this branch)
  • New behavior is covered by tests — FeedIntegrationTests.StreamCatchUp_CapsAtMaxItems_DeliveredOldestFirst, replicating the exact query and write-loop logic with 150 items to prove the cap and delivery order; 119/119 backend tests pass (note: like the rest of that fixture, the new test needs TEST_MSSQL_CONNECTION_STRING to actually execute against a live SQL Server — it wasn't available in the environment that authored/verified it, same limitation as every other test in that file)
  • All UI strings use t('ns:key') — no UI touched

Notes for reviewers

Independently verified (build, full test suite, format) before opening this PR, separately from the architect/security-engineer/tester agents that authored the fix, security review, and test.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg


Generated by Claude Code

StreamFeedUpdates replayed every item newer than the caller's since_timestamp
with no limit. That value is client-supplied and never validated, so an old (or
deliberately ancient) one made the server materialise the caller's whole retained
history into a List and write all of it to the stream — the one listing path in
the API without the ClampPageSize discipline every other one follows, and an easy
way for an authenticated client to amplify memory use across several streams.

Cap the replay at the newest MaxCatchUpItems. Delivery order is unchanged: the
query orders newest-first so the cap keeps the newest items, and the write loop
walks it in reverse to keep replaying oldest-first, which is what leaves the
newest item on top of the client's list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
Adds a regression test for the unbounded catch-up replay fixed in
5663caa: with more than MaxCatchUpItems (100)
matching items, StreamFeedUpdates's catch-up query must return exactly the
newest 100, and the write loop must still deliver them oldest-first on the
wire (reversing the newest-first query result), which is what leaves the
newest article on top of the client's list after each prepend.

Follows this file's existing convention of replicating the exact production
query against IntegrationFixture's real SQL Server database rather than
invoking FeedServiceImpl through gRPC plumbing, since no such harness exists
in this test project yet. Skips silently without TEST_MSSQL_CONNECTION_STRING,
same as every other test in this file — not runnable against a live DB in
this sandbox, so verified compiling and passing (skipped) via `dotnet test`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 33 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: b969f225-9e71-4fe3-84a6-dfb4ce46e5ee

📥 Commits

Reviewing files that changed from the base of the PR and between 73ed79b and 282fb7c.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/Pressmark.Api.Tests/FeedIntegrationTests.cs
  • src/Pressmark.Api/Services/FeedServiceImpl.cs

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.

Comment thread src/Pressmark.Api.Tests/FeedIntegrationTests.cs Outdated
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@lopatnov
lopatnov marked this pull request as ready for review September 5, 2026 07:50
Independent architect review of this PR found the regression test
duplicated FeedServiceImpl's query and write-loop logic with its own
hardcoded copy of the cap value, so it couldn't actually catch the
service changing (e.g. reverting the Take, or the cap value drifting).
Expose MaxCatchUpItems as internal (InternalsVisibleTo already covers the
test project) and reference it directly.

Also adds the ThenByDescending(Id) tiebreaker the review flagged: without
it, which items land inside the cap on a PublishedAt tie is
non-deterministic, and the strict `>` catch-up predicate never re-delivers
a loser on a later reconnect — same tiebreaker already used by cursor
pagination, and backed by the same (PublishedAt, Id) index.

Minor: reworded the "gets from normal paging" comment — paging continues
from the oldest item via nextCursor, so a gap past the cap isn't actually
refilled by paging, only by a reload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnoh6YfJwp63szeP8ZFqVg
@gitar-bot

gitar-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Bounds the stream catch-up replay to the newest 100 items, preventing a client-supplied since_timestamp from materializing the entire retained history into memory. Delivery order is preserved (oldest-first) via the reverse iteration of the capped list. The test replicating the query and write-loop logic with 150 items confirms the cap and ordering behavior. No issues found.

✅ 1 resolved
Quality: Catch-up test duplicates logic instead of calling StreamFeedUpdates

📄 src/Pressmark.Api.Tests/FeedIntegrationTests.cs:339-353
StreamCatchUp_CapsAtMaxItems_DeliveredOldestFirst re-implements the query and reverse write-loop inline rather than invoking the real StreamFeedUpdates method. If someone later removes .Take(MaxCatchUpItems) or changes the ordering in the service, this test keeps passing against its own copy of the code and the regression goes undetected. Consider exercising StreamFeedUpdates through the gRPC service (with a fake IServerStreamWriter capturing emitted items) so the test actually guards the production path.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

@lopatnov
lopatnov merged commit efcda89 into main Sep 5, 2026
15 checks passed
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.

2 participants