rohitg00/ai-engineering-from-scratch · error · ProtocolError
response content must be a non-empty block list
Error message
response content must be a non-empty block list
What it means
Raised by _validated_blocks when a model response's 'content' field is missing, not a list, or an empty list. The Messages API always returns at least one content block, so an empty or non-list content field indicates a malformed or truncated response.
Source
Thrown at certifications/claude/lessons/08-messages-api-and-application-lifecycle/code/main.py:149
value = handler(arguments)
return {
"type": "tool_result",
"tool_use_id": tool_id,
"content": json.dumps(value, sort_keys=True),
}
except Exception as exc: # Tool failures become model-visible results.
return {
"type": "tool_result",
"tool_use_id": tool_id,
"content": f"{type(exc).__name__}: {exc}",
"is_error": True,
}
def _validated_blocks(response: dict[str, Any]) -> list[dict[str, Any]]:
blocks = response.get("content")
if not isinstance(blocks, list) or not blocks:
raise ProtocolError("response content must be a non-empty block list")
if not all(isinstance(block, dict) and isinstance(block.get("type"), str) for block in blocks):
raise ProtocolError("every content block needs a type")
return blocks
def _text_from_blocks(blocks: list[dict[str, Any]]) -> str:
return "".join(str(block.get("text", "")) for block in blocks if block["type"] == "text")
def collect_stream_text(events: Iterable[dict[str, Any]]) -> str:
"""Collect only text deltas while checking that a stream terminates."""
chunks: list[str] = []
stopped = False
for event in events:
event_type = event.get("type")
if stopped:
raise ProtocolError("event arrived after message_stop")
if event_type == "content_block_delta":View on GitHub (pinned to 39ea8a1c6d)
Solutions
- Ensure the response dict has a non-empty 'content' list of block objects
- When replaying captures, verify the full response body was saved, not just text
- Add a fixture builder that always includes at least one text block
Example fix
// before
{"role":"assistant","content":[]}
// after
{"role":"assistant","content":[{"type":"text","text":"done"}]} Defensive patterns
Strategy: validation
Validate before calling
def has_valid_content(response: dict) -> bool:
content = response.get("content")
return isinstance(content, list) and len(content) > 0 Type guard
def is_response_with_content(resp) -> bool:
return isinstance(resp, dict) and isinstance(resp.get("content"), list) and bool(resp["content"]) Try / catch
try:
agent.run(...)
except ProtocolError as exc:
if "non-empty block list" in str(exc):
reject_or_refetch_response() Prevention
- Never stub a response with empty content; always include one text block
- Validate captured or replayed response bodies before use
When it happens
Trigger: Calling run() with a response dict like {"content": []}, {"content": "text"}, or one that omits 'content' entirely.
Common situations: Stubbing API responses in tests with minimal dicts, partial JSON parsing of a streamed response that was cut off, or upstream schema drift after an API version change.
Related errors
- tool_use requires name and object input
- every content block needs a type
- event arrived after message_stop
- stream ended without message_stop
- batch size must be positive
AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26).
Data as JSON: /api/errors/43d8578b4c04e138.
Report an issue: GitHub.