Skip to content

fix: code review bugs (LLM stream retry, SSE disconnect, publish retry variants, rate limit)#126

Merged
rogerdigital merged 4 commits into
mainfrom
fix/code-review-bugs
Jun 29, 2026
Merged

fix: code review bugs (LLM stream retry, SSE disconnect, publish retry variants, rate limit)#126
rogerdigital merged 4 commits into
mainfrom
fix/code-review-bugs

Conversation

@rogerdigital

@rogerdigital rogerdigital commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Background

Code review surfaced 4 confirmed bugs, split into 4 independent commits by theme.

Changes

1. LLM stream retry after mid-stream failure produced duplicate output (6d402d9)

The original retry loop wrapped both connection establishment and stream consumption in a single try/catch. When the stream failed mid-way (network drop after some tokens were already yielded), the catch branch would trigger a retry that re-yielded content from scratch → duplicated, out-of-order text.

Split into two phases: phase one only retries fetch + status check and breaks once a valid response is obtained; phase two consumes the stream and never retries, surfacing any error directly. Both providers (OpenAI / Anthropic) updated symmetrically.

2. SSE enqueue/close cascaded errors on client disconnect (9a18ac0)

After the client disconnected, controller.enqueue() threw a TypeError; the catch then tried to enqueue an error event (throwing again), and finally called controller.close() (throwing a third time).

Wrapped send / close in tolerant helpers and skip the error event when the abort signal indicates the client cancelled.

3. Retry publish dropped platform variant content (36787bf)

The retry path only passed title/content/platforms, omitting platformDrafts/variantIds, so a retried task published the original body instead of the selected per-platform variant.

Added an optional variantIds field on SyncTask (backward compatible, no schema migration), persisted at task creation; extracted the variantIds → platformDrafts resolution into resolveVariantsToDrafts, shared by both publish and retry.

4. rateLimit trusted spoofable XFF header (dc26853)

As a local single-user tool with no reverse proxy, x-forwarded-for / x-real-ip are client-controlled and trivially spoofable — rotating the header bypassed rate limiting entirely.

Dropped IP extraction in favor of a fixed local key, keeping the separate agent/default limits. localhostGuard logic is unchanged (binding to 127.0.0.1 is the real boundary); added a clarifying comment on its role.

Verification

  • pnpm --filter @publio/api exec tsc --noEmit
  • pnpm --filter @publio/web exec tsc --noEmit
  • pnpm lint

…cate output

The previous retry loop wrapped both connection establishment and stream
consumption in one try/catch. When the stream failed mid-way (network
drop after some tokens were already yielded), the catch branch would
trigger a retry that re-yielded content from scratch, producing
duplicated and out-of-order text in the user's output.

Split the loop into two phases: phase one only retries fetch + status
check and breaks once a valid response is obtained; phase two consumes
the stream and never retries, surfacing any error directly.
When the client disconnects, controller.enqueue() throws a TypeError on
the already-closed stream; the previous catch then tried to enqueue an
error event (throwing again) and finally called controller.close()
(throwing a third time).

Wrap every enqueue and close in tolerant helpers, and skip the error
event when the abort signal indicates the client cancelled. Stream
consumption now ends cleanly instead of cascading exceptions.
Retry only passed title/content/platforms, dropping the per-platform
variant content selected at publish time. A retried task would then
publish the original body instead of the adapted variant.

Persist variantIds on the SyncTask (optional, no schema migration), and
rebuild platformDrafts from those variants on retry via the same
resolveVariantsToDrafts helper now shared between publish and retry.
As a local single-user tool there is no reverse proxy, so
x-forwarded-for and x-real-ip are client-controlled and trivially
spoofable — rotating the header bypassed rate limiting entirely.

Drop the IP extraction and bucket by a fixed local key, keeping the
separate agent/default limits. localhostGuard is left as-is (its Host
check is auxiliary; binding to 127.0.0.1 is the real boundary) with a
clarifying comment.
@rogerdigital rogerdigital merged commit 9ce9872 into main Jun 29, 2026
3 checks passed
@rogerdigital rogerdigital deleted the fix/code-review-bugs branch June 29, 2026 16:42
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.

1 participant