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

  1. Ensure the response dict has a non-empty 'content' list of block objects
  2. When replaying captures, verify the full response body was saved, not just text
  3. 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

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


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