deepset-ai/haystack · error · ValueError

Found unclosed <haystack_content_part> tag at position {tag_

Error message

Found unclosed <haystack_content_part> tag at position {tag_start}. Content: '{snippet}...'

What it means

The rendered template text contains an internal `<haystack_content_part>` sentinel opening tag whose matching closing tag was not found, so the content-part parser cannot extract the serialized part (e.g. an ImageContent or ToolCall). This indicates the sentinel structure was corrupted, typically by content truncation or by template text interfering with the sentinel format.

Source

Thrown at haystack/utils/jinja2_chat_extension.py:331

            if tag_start == -1:
                # No more tags, add remaining text if any
                remaining_text = content[cursor:].strip()
                if remaining_text:
                    parts.append(TextContent(text=remaining_text))
                break

            # Add text before tag if any
            if tag_start > cursor:
                plain_text = content[cursor:tag_start].strip()
                if plain_text:
                    parts.append(TextContent(text=plain_text))

            content_start = tag_start + len(start_tag)
            tag_end = content.find(end_tag, content_start)

            if tag_end == -1:
                snippet = _redact_nonce(content, start_tag, end_tag)[tag_start : tag_start + 50]
                raise ValueError(
                    f"Found unclosed <haystack_content_part> tag at position {tag_start}. Content: '{snippet}...'"
                )

            json_content = content[content_start:tag_end]
            data = json.loads(json_content)
            parts.append(_deserialize_content_part(data))

            cursor = tag_end + len(end_tag)

        return parts

    @staticmethod
    def _validate_build_chat_message(
        parts: list[ChatMessageContentT], role: str, meta: dict, name: str | None = None
    ) -> ChatMessage:
        """
        Validate the parts of a chat message and build a ChatMessage object.

View on GitHub (pinned to e318778c9b)

Solutions

  1. Ensure the opening content-part tag and its closing tag are inside the same Jinja2 conditional/block so they are emitted together.
  2. Do not place {% if %}/{% endif %} boundaries between a content part's start and end tags.
  3. Check for template truncation (e.g. missing end of string) and re-validate the full template syntax.
  4. If application data may contain tag-like text, escape it or keep it out of content-part regions.

Example fix

// before
{% if image %}<part-start {{ image }}
{% endif %}<part-end>  // tags split across blocks
// after
{% if image %}<part-start {{ image }}><part-end>{% endif %}
Defensive patterns

Strategy: validation

Validate before calling

def validate_part_tags_balanced(rendered: str, start_tag: str, end_tag: str) -> None:
    if rendered.count(start_tag) != rendered.count(end_tag):
        raise ValueError("Unbalanced <haystack_content_part> sentinel tags in rendered template")

Type guard

def tags_balanced(rendered: str, start_tag: str, end_tag: str) -> bool:
    return rendered.count(start_tag) == rendered.count(end_tag)

Try / catch

try:
    messages = renderer.run(template=tpl, variables=vars)["messages"]
except ValueError as e:
    if "unclosed <haystack_content_part>" in str(e):
        log.error("Sentinel tag structure broken; check Jinja2 blocks around content parts")
    raise

Prevention

When it happens

Trigger: Template content gets truncated mid part-tag; user-provided text containing strings resembling the sentinel tags; a Jinja2 block breaks before the closing tag is emitted (e.g. an exception mid-render swallowed, or unbalanced Jinja2 tags).

Common situations: Multimodal templates with images/files where the closing tag ends up outside a Jinja2 conditional; documents containing odd markup that disrupts parsing; copying an incomplete template example.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/948c452638a7c163. Report an issue: GitHub.