Skip to content

MarkdownCodeBlockCleaner.clean() throws StringIndexOutOfBoundsException on degenerate fence-only input #6641

Description

@sumituppal03

Component

org.springframework.ai.converter.MarkdownCodeBlockCleaner
File: spring-ai-model/src/main/java/org/springframework/ai/converter/MarkdownCodeBlockCleaner.java

Spring AI version

Checked against main (commit fb51ef2, as of 2026-07-17).

Describe the bug

clean(String) does not guard against negative lengths when stripping the
trailing fence on a single-line input. For input strings that are only
backticks (3 to 5 of them), text.startsWith("```") and text.endsWith("```")
are both true simultaneously (the fence characters overlap), so the method
proceeds into the single-line branch and eventually calls:

text = text.substring(0, text.length() - 3);

When text.length() - 3 is negative, this throws
StringIndexOutOfBoundsException instead of returning something sane (e.g.
the original text, or an empty string).

Steps to reproduce

MarkdownCodeBlockCleaner cleaner = new MarkdownCodeBlockCleaner();
cleaner.clean("```");
cleaner.clean("````");
cleaner.clean("`````");
cleaner.clean("``````");

Verified output (JDK 21.0.11, logic extracted verbatim from clean()):

INPUT: ```     ->  THROWS: java.lang.StringIndexOutOfBoundsException: Range [0, -3) out of bounds for length 0
INPUT: ````    ->  THROWS: java.lang.StringIndexOutOfBoundsException: Range [0, -2) out of bounds for length 1
INPUT: `````   ->  THROWS: java.lang.StringIndexOutOfBoundsException: Range [0, -1) out of bounds for length 2
INPUT: ``````  ->  OUTPUT: [] (does not throw)

Expected behavior

The cleaner should handle degenerate/malformed fence-only input gracefully
(e.g. return the input unchanged, or an empty string) rather than throwing an
unchecked exception. Since clean() is typically invoked on raw LLM output,
a malformed or truncated response from a model could trigger this at
runtime in application code that doesn't expect it.

Actual behavior

StringIndexOutOfBoundsException is thrown for 3-, 4-, and 5-backtick-only
inputs.

Additional context

I extracted the clean() logic verbatim from the current main branch source
and ran it standalone under a real JDK 21 to confirm the above output — this
isn't a theoretical trace, it's actual reproduced behavior. I have not yet
run it through the module's own Maven build/test suite (just an isolated
JVM run of the identical logic), so I'm happy to be corrected if there's
context I'm missing, or if this overlaps with prior triage.

I'd like to submit a fix + accompanying unit tests for this if it's
confirmed and not already tracked elsewhere. Let me know if this overlaps
with prior work (e.g. #6403) or if there's a preferred approach (early
return vs. defensive length check).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions