Skip to content

fix(common): stop the HTML-escape removal from corrupting escaped backslashes - #1293

Merged
kabir merged 1 commit into
a2aproject:mainfrom
WMK666666:issue-1292
Nov 5, 2026
Merged

kabir merged 1 commit into
a2aproject:mainfrom
WMK666666:issue-1292

Conversation

@WMK666666

Copy link
Copy Markdown
Contributor

Fixes #1292


Problem

HtmlEscapeUtils.removeHtmlEscaping removes the HTML escapes JsonFormat adds by replacing every
\u003c, \u003e, \u0026, \u003d and \u0027 in the printed document with its character, using
String.replace on the whole string. It never asks whether the sequence it matched was itself
escaped.

JsonFormat prints a message text that really contains those characters as "\\u003c" — two
backslashes for the literal one, then the text u003c. The replacement starts at the second
backslash, so the body becomes "\<":

expected: <{"text":"a\\u003c b"}>
but was:  <{"text":"a\< b"}>

That is not valid JSON, and ProtoJsonUtils.toJson hands it straight to the transport, so the client
cannot parse a reply the server believes it serialized correctly:

com.google.gson.stream.MalformedJsonException:
    Invalid escape sequence at line 7 column 22 path $.message.parts[0].text

Any agent that echoes code, a regex, or a note about an escape sequence hits this. The fix lands in
common, the one place both spec-grpc.ProtoJsonUtils and compat-0.3's copy call, so the two
paths are fixed together.

Change

removeHtmlEscaping now walks the document instead of replacing substrings:

  • an escaped backslash (\\) is copied as is, so the value behind it is never mistaken for an escape;
  • a six-character \u00XX sequence is rewritten only when it is not preceded by a backslash, and
    only for the five HTML characters — anything else (\n, \u00e9, …) is copied verbatim;
  • lowercase and uppercase hex digits are still both accepted, and U+2028 / U+2029 are still left
    alone, as documented.

For anything that has no HTML escape the method returns the input unchanged, so the fast path is the
same as before.

Tests

  • HtmlEscapeUtilsTest.removeHtmlEscaping_leavesAnEscapedBackslashBeforeUEscapeAlone — a value holding
    a literal backslash followed by u003c comes back untouched (red on main: but was: <{"text":"a\< b"}>).
  • HtmlEscapeUtilsTest.removeHtmlEscaping_restoresTheEscapeAfterAnEscapedBackslash — a literal
    backslash followed by a real < escape keeps the escaped backslash and drops the escape (a\\<b);
    this one passes before and after, as a guard that the two cases stay distinguishable.
  • ProtoJsonUtilsTest.toJson_keepsALiteralBackslashBeforeUEscapeInMessageText — builds a
    SendMessageParams whose TextPart carries that text, prints it through ProtoJsonUtils.toJson,
    and reads the body back with JsonFormat.parser(). On main this fails with the
    MalformedJsonException above, i.e. the assertion is on the produced document, not on a string
    comparison.

Full module tests for common, spec-grpc, jsonrpc-common and compat-0.3/spec-grpc:
common 16, spec 163, jsonrpc-common 100, spec-grpc 130, compat-0.3/spec 53,
compat-0.3/spec-grpc 25 — 487 tests, 0 failures, 0 errors, BUILD SUCCESS.

spotless:check on Windows flags 12 files of the common module, but an untouched checkout
reports the same 12 (line-ending artifact), so that is not something this change introduced;
the formatting follows the file's existing style and stays within 100 columns.

Notes

compat-0.3/spec-grpc has its own ProtoJsonUtils_v0_3, but it delegates to the same
HtmlEscapeUtils, so no change is needed there.

…kslashes

removeHtmlEscaping replaced every HTML escape it could match in the printed
protobuf JSON, without looking at whether the sequence was itself escaped.
A message text holding a literal backslash followed by the characters
"u003c" is printed by JsonFormat as "\\u003c"; the replacement started at
the second backslash, so the body carried "\<", which is not valid JSON:

  MalformedJsonException: Invalid escape sequence at line 7 column 22
  path $.message.parts[0].text

The scan now copies an escaped backslash as it is and rewrites a
six-character escape only when no backslash precedes it, so both the value
and the escape removal survive.

Fixes 1292
@kabir
kabir merged commit 8101f3a into a2aproject:main Nov 5, 2026
14 checks passed
@kabir

kabir commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Thank you @WMK666666 :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: A message text with a literal \u003c is serialized as invalid JSON

2 participants