Skip to content

Tolerate lone-surrogate escapes in JSON strings#52

Merged
feruzm merged 2 commits into
mainfrom
fix/lenient-json-strings
Jul 12, 2026
Merged

Tolerate lone-surrogate escapes in JSON strings#52
feruzm merged 2 commits into
mainfrom
fix/lenient-json-strings

Conversation

@feruzm

@feruzm feruzm commented Jul 12, 2026

Copy link
Copy Markdown
Member

Day-after production monitoring caught dozens of unhandled InvalidOperationException: Cannot read incomplete UTF-16 JSON text as string with missing low surrogate per day. Root cause: JavaScript's JSON.parse accepts lone surrogate escapes (JS strings are arbitrary UTF-16), but System.Text.Json throws when materializing such strings — so requests the Node service handled fine (typically bot traffic with malformed unicode) were returning 500.

  • All string extraction now routes through JsVal.TryGetStringLenient, which falls back to unescaping the raw JSON text with JS semantics (\uXXXX decodes to the code unit, paired or not).
  • JsJson.Stringify already re-emits lone surrogates as escapes (well-formed JSON.stringify mode), so the signature-hashing path stays byte-exact — proven by a new V8-generated golden vector that runs through the full validateCode re-serialization parity test.
  • pipe() diagnostics now include the request path, making the headers-already-sent warnings attributable to specific endpoints.

Verified: 43 tests green; live smoke with lone-surrogate payloads returns the same statuses Node did (401 invalid-token / 200 token-create) with zero unhandled exceptions.

Summary by CodeRabbit

  • Bug Fixes

    • Improved compatibility with JavaScript-style JSON string handling, including escaped and lone-surrogate characters.
    • Updated request, response, authentication, payment, search, and token-processing flows to handle loosely typed JSON string values more reliably.
    • Improved serialization and string coercion for JSON values.
  • Tests

    • Added coverage for lone-surrogate extraction, coercion, serialization, and validation scenarios.

…pe diagnostics

JavaScript's JSON.parse accepts lone surrogate escapes (JS strings are
arbitrary UTF-16), but System.Text.Json throws InvalidOperationException when
materializing such strings — so payloads the Node service handled fine were
turning into 500s (dozens/day in production logs). Route all string extraction
through JsVal.TryGetStringLenient, which falls back to unescaping the raw JSON
text with JS semantics; Stringify already re-emits lone surrogates as escapes,
matching well-formed JSON.stringify (new V8-generated golden vector proves the
full hash-parity path).

Also include the request path in pipe() diagnostics so the
headers-already-sent warnings are attributable to endpoints.
@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes JSON string handling tolerate lone-surrogate escapes like JavaScript does. The main changes are:

  • Added lenient string extraction for JsonValue strings.
  • Routed handler string reads through the new extraction helper.
  • Updated JSON stringification and response writing to preserve lone-surrogate behavior.
  • Added test vectors for validation and serialization parity.
  • Added request paths to pipe() diagnostic logs.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
dotnet/EcencyApi/Infrastructure/JsVal.cs Adds the shared lenient string extraction and raw JSON unescape path.
dotnet/EcencyApi/Infrastructure/JsJson.cs Uses lenient string extraction when serializing and checking JSON value truthiness.
dotnet/EcencyApi/Infrastructure/ApiClient.cs Uses lenient string extraction while converting configured API auth values into outbound headers.
dotnet/EcencyApi/Infrastructure/Upstream.cs Uses lenient string extraction for text responses and adds request paths to diagnostics.

Reviews (2): Last reviewed commit: "fix: guard the lenient string extraction..." | Re-trigger Greptile

headers[kv.Key] = kv.Value switch
{
JsonValue v when v.TryGetValue<string>(out var s) => s,
JsonValue v when JsVal.TryGetStringLenient(v, out var s) => s,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Surrogate Header Values Forwarded

When an auth config value contains a lone surrogate escape, this branch now decodes it into a raw .NET string and forwards it as an HTTP header value. Header values cannot safely carry isolated UTF-16 surrogates, so the private API request can fail later with malformed or rejected headers instead of rejecting the bad auth configuration at this boundary.

Fix in Claude Code

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value comes from PRIVATE_API_AUTH — operator-controlled deploy configuration (a base64-encoded JSON header map we set ourselves), not client input, so a lone surrogate here means broken config rather than a reachable attack surface. And the failure mode is identical either way: a header value containing a surrogate (or any non-ASCII) makes the HTTP client throw at send time in both runtimes (SocketsHttpHandler rejects non-ASCII header values; Node http throws ERR_INVALID_CHAR), which pipe() turns into the same 500. Rejecting earlier at this boundary would add validation for a config state that fails loudly on the first request regardless.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair point. The failure boundary argument doesn't hold here — SocketsHttpHandler throws on send regardless, and since PRIVATE_API_AUTH is operator config, a surrogate there is broken config that surfaces immediately on the first request. Early rejection at this boundary adds no safety net that doesn't already exist. The comment stands withdrawn.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f735de56-ca33-4c95-b5fc-a068cbe8aadd

📥 Commits

Reviewing files that changed from the base of the PR and between 5d77e04 and 18ae5ea.

📒 Files selected for processing (1)
  • dotnet/EcencyApi/Infrastructure/Upstream.cs
📝 Walkthrough

Walkthrough

The change adds lenient UTF-16 JSON string extraction for lone-surrogate escapes, updates JSON and API conversion paths to use it, expands validation/hash vectors, and adds regression coverage. Upstream error logs now include request paths.

Changes

Lenient JSON string handling

Layer / File(s) Summary
Lenient extraction core
dotnet/EcencyApi/Infrastructure/JsVal.cs
Adds raw JSON unescaping fallback for JsonValue strings and applies it to string detection, extraction, and JavaScript coercion.
JSON semantics and regression coverage
dotnet/EcencyApi/Infrastructure/JsJson.cs, dotnet/EcencyApi.Tests/JsValTests.cs, dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json, dotnet/tools/gen-vectors.js
Uses lenient extraction for primitive serialization and truthiness, and adds lone-surrogate tests and hashing vectors.
API and handler call-site migration
dotnet/EcencyApi/Handlers/*, dotnet/EcencyApi/Infrastructure/ApiClient.cs, dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs, dotnet/EcencyApi/Infrastructure/Upstream.cs
Replaces strict string extraction across request validation, token and balance parsing, templates, headers, responses, and URL/body conversion; upstream diagnostics include request paths.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

A rabbit found a surrogate hiding in the JSON hay,
Lenient strings helped it safely stay.
Tests echoed escapes, vectors joined the song,
APIs learned to carry the code along.
“Hop!” said the rabbit, “UTF-16 belongs!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: lenient handling of lone-surrogate escapes in JSON string extraction and serialization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lenient-json-strings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
dotnet/EcencyApi/Handlers/AuthApi.cs (1)

62-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM on the migration — return value is checked and fallbacks are preserved.

JsToString and JsTemplate are identical to their counterparts in SearchApi.cs (lines 45–63 and 42–43). Consider extracting both to a shared utility (e.g., JsCoercions or similar) to eliminate the duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dotnet/EcencyApi/Handlers/AuthApi.cs` around lines 62 - 65, Extract the
duplicated JsToString and JsTemplate implementations from AuthApi.cs and
SearchApi.cs into a shared utility such as JsCoercions. Update both handlers to
call the shared methods, preserving the existing string, boolean, JSON fallback,
and template coercion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@dotnet/EcencyApi/Infrastructure/Upstream.cs`:
- Around line 269-270: Update the response-writing logic around
JsVal.TryGetStringLenient to check its boolean result before calling
ctx.Response.WriteAsync. Write bodyStr only when conversion succeeds, and handle
the false case using the same established pattern as the other call sites in
this PR, avoiding WriteAsync with a null value.

---

Nitpick comments:
In `@dotnet/EcencyApi/Handlers/AuthApi.cs`:
- Around line 62-65: Extract the duplicated JsToString and JsTemplate
implementations from AuthApi.cs and SearchApi.cs into a shared utility such as
JsCoercions. Update both handlers to call the shared methods, preserving the
existing string, boolean, JSON fallback, and template coercion behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca3fb1ce-439a-4713-922d-0d591d25ba72

📥 Commits

Reviewing files that changed from the base of the PR and between ee15712 and 5d77e04.

📒 Files selected for processing (19)
  • dotnet/EcencyApi.Tests/JsValTests.cs
  • dotnet/EcencyApi.Tests/fixtures/crypto-vectors.json
  • dotnet/EcencyApi/Handlers/AuthApi.cs
  • dotnet/EcencyApi/Handlers/HiveExplorer.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Accounts.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Chain.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Core.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Feeds.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Misc.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.Payments.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.UserData1.cs
  • dotnet/EcencyApi/Handlers/PrivateApi.UserData2.cs
  • dotnet/EcencyApi/Handlers/SearchApi.cs
  • dotnet/EcencyApi/Infrastructure/ApiClient.cs
  • dotnet/EcencyApi/Infrastructure/HttpContextExtensions.cs
  • dotnet/EcencyApi/Infrastructure/JsJson.cs
  • dotnet/EcencyApi/Infrastructure/JsVal.cs
  • dotnet/EcencyApi/Infrastructure/Upstream.cs
  • dotnet/tools/gen-vectors.js

Comment thread dotnet/EcencyApi/Infrastructure/Upstream.cs Outdated
@feruzm feruzm merged commit 2965154 into main Jul 12, 2026
5 checks passed
@feruzm feruzm deleted the fix/lenient-json-strings branch July 12, 2026 07:08
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.

1 participant