Skip to content

Fix LineIterator hanging forever when the stream ends without a trailing newline - #6279

Merged
rsareddy0329 merged 3 commits into
aws:masterfrom
RudraDudhat2509:fix/lineiterator-no-trailing-newline-hang
Sep 30, 2026
Merged

rsareddy0329 merged 3 commits into
aws:masterfrom
RudraDudhat2509:fix/lineiterator-no-trailing-newline-hang

Conversation

@RudraDudhat2509

Copy link
Copy Markdown

Fixes #6278

Returns the leftover partial line once instead of continue, so the next call correctly hits read_pos == buffer size and raises StopIteration normally instead of looping forever.

Added test_line_iterator_no_trailing_newline_at_end. test_line_iterator_incomplete_line_at_end doesn't actually cover this, it only tests a stream that already ends with \n.

Verified against the pre-fix code: hangs, confirmed by killing it after 15s with no output. Full test_iterators.py 21 passed after the fix.

…ing newline

When the underlying event stream is exhausted (StopIteration) while the
buffer still has an unread trailing partial line (no "\n" at the end,
a normal case since the model's last streamed token has no reason to
be a newline), __next__ just did `continue`. That re-reads the exact
same unterminated line, fails the same check, and calls next() on an
already-exhausted iterator again, forever. Nothing in that branch ever
changes state, so it spins indefinitely instead of returning or
raising.

Return the leftover partial data once instead, so the following call
correctly hits the read_pos == buffer size case and raises
StopIteration normally.

Added test_line_iterator_no_trailing_newline_at_end since the existing
test_line_iterator_incomplete_line_at_end doesn't actually cover this,
it only tests a stream that ends cleanly with a trailing newline.
@RudraDudhat2509

Copy link
Copy Markdown
Author

Gentle ping

@rsareddy0329
rsareddy0329 deployed to manual-approval September 30, 2026 16:47 — with GitHub Actions Active
@rsareddy0329
rsareddy0329 deployed to manual-approval September 30, 2026 16:47 — with GitHub Actions Active
@rsareddy0329
rsareddy0329 deployed to manual-approval September 30, 2026 16:47 — with GitHub Actions Active
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Claude finished @rsareddy0329's task in 47s —— View job


Code Review

  • Read the PR diff and changed files
  • Review LineIterator.__next__ fix for correctness
  • Review the new regression test
  • Post review feedback

I reviewed the change to sagemaker-core/src/sagemaker/core/iterators.py and the new test. The fix is correct and well-targeted. ✅

Correctness

The root cause is accurately diagnosed. In LineIterator.__next__, when the stream ends with a trailing partial line (no \n):

  1. self.buffer.readline() returns the partial line, but since it doesn't end in \n, the if line and line[-1] == ord("\n") branch is skipped.
  2. next(self.byte_iterator) raises StopIteration because the underlying iterator is exhausted.
  3. read_pos < buffer size is True (leftover bytes remain), so the old continue re-entered the loop — but nothing had changed (read_pos, buffer, and the exhausted byte_iterator are all static), so it spun forever.

The fix returns the remainder once and advances read_pos to the buffer end. I traced the follow-up call: readline() returns b'', next(...) raises StopIteration, and now read_pos < buffer size is False (they're equal), so raise propagates a normal StopIteration. Behavior is correct and terminates. 👍

Returning the unterminated remainder (rather than dropping it) is the right call — it surfaces the final bytes to the consumer instead of silently discarding data, and it's consistent with the normal path since there is no \n to strip.

Tests

  • The new test_line_iterator_no_trailing_newline_at_end correctly covers the regressed path (a chunk with no trailing \n) and asserts both that the remainder is returned and that StopIteration follows.
  • The author's observation is accurate: the pre-existing test_line_iterator_incomplete_line_at_end (lines 221–233) uses a stream that does end with \n, so despite its name it never exercised this case.

Minor observation (non-blocking)

At iterators.py:184-185, remainder is re-read via seek(self.read_pos) + read(), but the line already produced by readline() at line 171 holds the same bytes — return line would avoid the extra seek/read. This is purely cosmetic; the current form is harmless and arguably more explicit, so no change is required.

No security, performance, or backward-compatibility concerns. This is a solid bug fix.

@RudraDudhat2509

RudraDudhat2509 commented Sep 30, 2026 •

Copy link
Copy Markdown
Author

@rsareddy0329 thanks for kicking off the review. happy to fix the non blocking issue that claude pointed out, also the ci fails are unrelated to the files staged, thx again

@rsareddy0329

rsareddy0329 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

@rsareddy0329 thanks for kicking off the review. happy to fix the non blocking issue that claude pointed out, also the ci fails are unrelated to the files staged, thx again

PR changes looks good and approving as claude recommendation is non-blocking.
You can include the claude recommended changes in a seperate PR.

@rsareddy0329
rsareddy0329 merged commit e32909e into aws:master Sep 30, 2026
25 of 28 checks passed
@RudraDudhat2509

Copy link
Copy Markdown
Author

Appreciate the quick review cycle thanks !🚀

@RudraDudhat2509
RudraDudhat2509 deleted the fix/lineiterator-no-trailing-newline-hang branch September 30, 2026 20:42

This branch was successfully deployed

1 active deployment
manual-approval — 575aed95 Deployed Sep 30, 2026 by rsareddy0329 via wait-for-approval #1874
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.

LineIterator hangs forever if the streamed response ends without a trailing newline

2 participants