{"record":{"id":"c4b2cef6a7146470","repo":"rohitg00/ai-engineering-from-scratch","slug":"stream-ended-without-message-stop","errorCode":null,"errorMessage":"stream ended without message_stop","messagePattern":"stream ended without message_stop","errorType":"exception","errorClass":"ProtocolError","httpStatus":null,"severity":"error","filePath":"certifications/claude/lessons/08-messages-api-and-application-lifecycle/code/main.py","lineNumber":174,"sourceCode":"    return \"\".join(str(block.get(\"text\", \"\")) for block in blocks if block[\"type\"] == \"text\")\n\n\ndef collect_stream_text(events: Iterable[dict[str, Any]]) -> str:\n    \"\"\"Collect only text deltas while checking that a stream terminates.\"\"\"\n    chunks: list[str] = []\n    stopped = False\n    for event in events:\n        event_type = event.get(\"type\")\n        if stopped:\n            raise ProtocolError(\"event arrived after message_stop\")\n        if event_type == \"content_block_delta\":\n            delta = event.get(\"delta\", {})\n            if delta.get(\"type\") == \"text_delta\":\n                chunks.append(str(delta.get(\"text\", \"\")))\n        elif event_type == \"message_stop\":\n            stopped = True\n    if not stopped:\n        raise ProtocolError(\"stream ended without message_stop\")\n    return \"\".join(chunks)\n\n\ndef batch(items: list[Any], size: int) -> list[list[Any]]:\n    if size < 1:\n        raise ValueError(\"batch size must be positive\")\n    return [items[index : index + size] for index in range(0, len(items), size)]\n\n\ndef stable_cache_key(model: str, stable_prefix: str) -> str:\n    payload = f\"{model}\\0{stable_prefix}\".encode(\"utf-8\")\n    return hashlib.sha256(payload).hexdigest()\n\n\nIMAGE_MEDIA_TYPES = {\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"}\nDOCUMENT_MEDIA_TYPES = {\"application/pdf\", \"text/plain\"}\n\n","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/rohitg00/ai-engineering-from-scratch/blob/39ea8a1c6d0b61f071226eff7ede4d4105fed820/certifications/claude/lessons/08-messages-api-and-application-lifecycle/code/main.py#L156-L192","documentation":"Raised by collect_stream_text when the event iterator exhausts without a message_stop event. The collector treats message_stop as proof the stream terminated cleanly; its absence means the stream was truncated by a network drop, timeout, or an incomplete capture.","triggerScenarios":"Passing a list of content_block_delta events with no trailing {\"type\":\"message_stop\"}, e.g. a capture that stopped mid-response (test_stream_collector_requires_stop).","commonSituations":"Reading a stream that hit a connection reset or client read timeout, saving a partial SSE log, or forgetting to append the terminal event in mock streams.","solutions":["Append {\"type\":\"message_stop\"} to mock/test event lists","On live streams, retry the request when this error fires instead of using partial text","Verify captures include the final SSE event before replaying them"],"exampleFix":"// before\nevents = [{\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"hi\"}}]\n// after\nevents = [\n  {\"type\":\"content_block_delta\",\"delta\":{\"type\":\"text_delta\",\"text\":\"hi\"}},\n  {\"type\":\"message_stop\"}\n]","handlingStrategy":"retry","validationCode":"def stream_is_complete(events):\n    return bool(events) and events[-1].get(\"type\") == \"message_stop\"","typeGuard":null,"tryCatchPattern":"try:\n    text = collect_stream_text(events)\nexcept ProtocolError as exc:\n    if \"without message_stop\" in str(exc):\n        text = collect_stream_text(stream_request_again())  # discard partial","preventionTips":["Treat this error as a truncation signal: discard partial text and retry","Raise max_tokens and check network stability when truncation recurs","Ensure captures end with message_stop before replaying"],"tags":["streaming","truncated-response","messages-api","python"],"backgroundTag":"truncated-stream-missing-stop","analyzedSha":"39ea8a1c6d0b61f071226eff7ede4d4105fed820","analyzedAt":"2026-08-26T03:13:46.626Z","schemaVersion":2},"datasetVersion":"2026-08-26T07:17:17.940Z"}