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

  1. Wrap every element as an object with a string 'type' ("text", "tool_use", ...)
  2. Check for typos like "types" or "block_type" in mock blocks
  3. 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

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


AI-assisted analysis of rohitg00/ai-engineering-from-scratch@39ea8a1c6d (2026-08-26). Data as JSON: /api/errors/a88b805b2b4edb9f. Report an issue: GitHub.