Skip to content

AUS-1004: Add JsonRpcLimits with document and batch bounds - #165

Open
Astn wants to merge 1 commit into
aus-1003-reserved-namesfrom
aus-1004-limits
Open

Astn wants to merge 1 commit into
aus-1003-reserved-namesfrom
aus-1004-limits

Conversation

@Astn

@Astn Astn commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Third of the ten 2.0.0 pull requests sequenced in AUS-1001 (https://linear.app/austins-test/issue/AUS-1001). Ticket: https://linear.app/austins-test/issue/AUS-1004. Stacked on #164; retarget to master once that merges.

  • JsonRpcLimits(maxDocumentBytes = 4 MiB, maxBatchCount = 1024), Default, Unlimited; Config.SetLimits(limits) and Config.SetLimits(sessionId, limits) (null inherits). Zero disables a field, negatives throw at construction.
  • Byte check at every public Process / ProcessAsync entry before any copy, flattening, checked conversion or transcoding; string overloads measure the UTF-8 byte count and size the buffer by it. Batch check after a full parse and before the first dispatch, counting every top-level element; nothing in an over-long batch runs, notification-only batches included.
  • Either limit answers {"jsonrpc":"2.0","error":{"code":-32600,"message":"Invalid Request","data":{"limit":"maxDocumentBytes","maximum":4194304}},"id":null} (or maxBatchCount / 1024) with the configured maximum, identical bytes for every serializer (LimitExceededInfo with its own writer), passed through the parse-error handler when one is set, like the empty-batch answer.
  • Kestrel's MaxRequestBytes is unchanged and is met first (HTTP 413 / raw abort); a host that raises it gets the core answer.
  • Each entry resolves the session handler and the effective limits once and carries them into the core, so the plain path pays the moved lookup, one volatile read and one long compare; no lock, no allocation. The ProcessAsync memory entry keeps its base exception behaviour (faulted or cancelled task, no new exception region on the hot path).
  • LimitsTests (16 cases across the three serializers) and two Kestrel cases; README (raw connections, error table, Configuration/Limits, Security), SECURITY.md, CHANGELOG (Added, Security) and docs/upgrading.md.

Verification: Release build 0 warnings / 0 errors; 1216 tests pass on net8.0 and net10.0; check_request_path_sync.py 23 listed uses, none unlisted (no allowlist change); render.py --check agrees.

Reference machine (8 cores / 16 logical, about 20 percent foreign CPU load from other sessions throughout), base 4724600 vs this branch, interleaved pairs, medians with min..max:

Row Base Branch Note
--sync 3 16, 16 threads RPC/s 34.9 M (28.1..35.5) 34.5 M (30.5..36.1) level
--sync, allocation per numeric request 0 B 0 B unchanged (StringMe 32 B both)
--scale 3 16 4.0 pass, 16/1 about 7.0 pass, 16/1 about 7.0 level
--async 3 16 sync/None RPC/s 23.7 M (23.4..24.1) 23.8 M (22.4..24.1) level
--async Task/None 27.4 M (26.5..27.7) 26.6 M (25.9..27.9) inside spread
--async ValueTask/None 30.6 M (30.1..31.2) 29.5 M (28.8..29.5) 3.8 percent lower in all three pairs of this set; level in an earlier set; same-path rows level; single-thread micro row level. Flagged for the AUS-985 idle re-measure.
--async yield rows and every B/RPC level / identical
Micro AsyncDispatchBenchmarks.SyncViaProcessAsync (default job, two pairs) 182.2, 176.3 ns 185.0, 181.9 ns about 3 ns, within stddev overlap
Micro ValueTaskInline 198.6, 181.6 ns 186.5, 188.9 ns level
Micro DispatchBenchmarks.Add / AddInt 236.5, 212.6 / 169.4, 164.3 ns 210.1, 238.0 / 162.1, 161.3 ns level

Raw run files are in the session scratchpad (impl/aus-1004-bench*, impl/aus-1004-micro*).

The core now bounds what it admits: a document over JsonRpcLimits.MaxDocumentBytes (4 MiB by default) or a batch with more than MaxBatchCount entries (1024) is answered with -32600 and a data object naming the limit and the configured maximum, before anything is parsed or executed. The byte check runs at every public Process and ProcessAsync entry before any copy, flattening or transcoding (string overloads measure the UTF-8 byte count); the batch check runs after a full parse and before the first dispatch, so no prefix of an over-long batch executes. Config.SetLimits sets the process-wide value or a per-session override; zero disables a field and JsonRpcLimits.Unlimited restores the 1.x behaviour. Kestrel's MaxRequestBytes stays and is met first. README, SECURITY.md, CHANGELOG and docs/upgrading.md describe the limits and the staged host-responsibility paragraph.

This branch has not been deployed

No deployments
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