Skip to content

(py) Mid-stream body error raises StopAsyncIteration, so truncated streams look like clean EOF #475

@barjin

Description

@barjin

🤖 Found by Claude ultrareview — automated high-effort code review. Please verify independently before acting.

Location: impit-python/src/response.rs:158 (PyResponseAsyncBytesIterator::__anext__)

A mid-stream body error is mapped to PyStopAsyncIteration:

Some(Err(e)) => {
    if let Some(parent) = parent_response {
        Python::attach(|py| {
            if let Ok(mut parent_ref) = parent.try_borrow_mut(py) {
                parent_ref.inner_state = InnerResponseState::StreamingClosed;
                parent_ref.is_closed = true;
            }
        });
    }
    Err(pyo3::exceptions::PyStopAsyncIteration::new_err(format!(...)))
}

Impact: Raising StopAsyncIteration signals normal end-of-iteration to async for. So a connection reset or truncated chunked transfer mid-body silently ends the loop as if the stream completed, and the caller processes a partial body believing it is complete — a silent data-integrity bug. The error message is formatted into the exception but async for never surfaces it.

Additionally, unlike the clean-EOF branch, this path leaves is_stream_consumed = false, so a subsequent read()/content/text reports StreamClosed instead of the real failure.

Suggested direction: propagate stream errors as a real exception type (the classified ImpitError), not StopAsyncIteration, and set the consumed/closed flags consistently with the EOF branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working.pythonThis issue is in the Python impit bindings.rustThis issue concerns the Rust part of this monorepo.t-toolingIssues with this label are in the ownership of the tooling team.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions