rohitg00/ai-engineering-from-scratch · error · ProtocolError
every content block needs a type
Error message
every content block needs a type
What it means
Raised by _validated_blocks when any element of the response content list is not a dict or lacks a string 'type' field. Every Messages API content block (text, tool_use, etc.) must be an object with a discriminating 'type' string; this check runs before any downstream text extraction.
Source
Thrown at certifications/claude/lessons/08-messages-api-and-application-lifecycle/code/main.py:151
"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":
delta = event.get("delta", {})
if delta.get("type") == "text_delta":View on GitHub (pinned to 39ea8a1c6d)
Solutions
- Wrap every element as an object with a string 'type' ("text", "tool_use", ...)
- Check for typos like "types" or "block_type" in mock blocks
- Validate fixtures once in a shared helper so all tests emit well-formed blocks
Example fix
// before
{"content":["hello"]}
// after
{"content":[{"type":"text","text":"hello"}]} Defensive patterns
Strategy: type-guard
Validate before calling
def blocks_are_typed(blocks):
return all(isinstance(b, dict) and isinstance(b.get("type"), str) for b in blocks) Type guard
def are_typed_blocks(content: object) -> bool:
return isinstance(content, list) and all(
isinstance(b, dict) and isinstance(b.get("type"), str) for b in content
) Try / catch
try:
text = _text_from_blocks(_validated_blocks(response))
except ProtocolError as exc:
if "needs a type" in str(exc):
normalize_blocks_or_reject() Prevention
- Use one fixture factory that emits typed block objects
- Diff mock block shapes against a real captured response
When it happens
Trigger: A content list containing a bare string (e.g. ["hello"]) or a block like {"text":"hi"} with no 'type' key, passed into run().
Common situations: Hand-written mocks that put plain strings in content, schema drift where 'type' was renamed, or concatenating already-joined text back into the list.
Related errors
- tool_use requires name and object input
- response content must be a non-empty block list
- event arrived after message_stop
- stream ended without message_stop
- max_attempts must be positive
AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26).
Data as JSON: /api/errors/a88b805b2b4edb9f.
Report an issue: GitHub.