Skip to content

fix: support exact JSON number decoding for large request IDs above 2^53 - #1401

Merged
guglielmo-san merged 7 commits into
mainfrom
guglielmoc/strict_integer_request_ids
Oct 15, 2026
Merged

guglielmo-san merged 7 commits into
mainfrom
guglielmoc/strict_integer_request_ids

Conversation

@guglielmo-san

Copy link
Copy Markdown
Contributor

Fixes #1376

@guglielmo-san
guglielmo-san merged commit 95591a7 into main Oct 15, 2026
9 checks passed
@guglielmo-san
guglielmo-san deleted the guglielmoc/strict_integer_request_ids branch October 15, 2026 15:48
guglielmo-san added a commit that referenced this pull request Oct 28, 2026
…#1454)

`internal/json.Unmarshal` decoded through segmentio's streaming
`json.Decoder`, which copies its input into a read buffer of at least 32
KiB on every call. Unmarshal runs several times per request, so these
buffers made up most of the memory allocated to serve a tool call (528
KiB per call in `BenchmarkStreamableServing`). Details in #1453.

This change decodes in place:

- `json.Parse` into a `json.RawMessage` with `DontCopyRawMessage` finds
the extent of the first JSON value. It's the same `parseValue` the
Decoder runs on its buffer.
- That value is then decoded with the same flags as before.
- If finding the value fails, the input is malformed and goes through a
`Decoder` as before. Errors are therefore exactly today's: `io.EOF` for
empty input, `io.ErrUnexpectedEOF` for truncated input, and the same
`SyntaxError` and `UnmarshalTypeError` text.

Case-sensitive matching and the depth limit are unchanged.

One function, `newDecoder`, configures the Decoder, and `parseFlags`
gives `json.Parse` the same rules. `TestUnmarshalSameAsStream` and
`FuzzUnmarshalSameAsStream` compare the new path with the old one for
`Unmarshal` and `UnmarshalUseNumber` into several target types, checking
the decoded value, the error type and the error text. Locally the fuzzer
ran about 5 million inputs without finding a difference.
`TestUnmarshalDoesNotAliasInput` checks that decoded values never share
memory with the input, since the value is now found in place and callers
may reuse their buffers. `TestUnmarshalUseNumberCaseSensitivity` covers
`UnmarshalUseNumber`, which decodes every JSON-RPC envelope since #1401;
the existing case-sensitivity tests only cover `Unmarshal`.

```
name                  old time/op    new time/op    delta
StreamableServing     368.7µs ± 2%   219.3µs ± 1%   -40.51%
Unmarshal             4.015µs ± 1%   1.337µs ± 1%   -66.71%

name                  old alloc/op   new alloc/op   delta
StreamableServing     528.3KiB ± 0%  60.1KiB ± 1%   -88.62%
Unmarshal             37.41KiB ± 0%  5.39KiB ± 0%   -85.59%
```

Run with `go test ./...`, `go test -race ./...`, `go vet ./...`,
staticcheck and `go generate ./...` on Go 1.26.6, and `internal/json`
and `mcp` on Go 1.25.0.

This covers every call site, including the one #1395 changes, and
doesn't conflict with #1287.

Fixes #1453

Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
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.

jsonrpc: numeric request IDs above 2^53 lose precision

2 participants