From de5c138eef88685442dc71e36dd98d66b885a605 Mon Sep 17 00:00:00 2001 From: Lars Grote Date: Wed, 25 Feb 2026 12:07:25 +0100 Subject: encoding/json: unwrap IO errors from SyntacticError in transformSyntacticError When GOEXPERIMENT=jsonv2 is enabled, transformSyntacticError has a case for bare IO errors (export.IsIOError), but this case is never reached when the IO error is wrapped inside a *jsontext.SyntacticError. This happens because consumeObject wraps errors with wrapWithObjectName before they reach ReadValue, which then wraps them in *SyntacticError via wrapSyntacticError. The result is that transformSyntacticError matches the SyntacticError case first, converts it to a v1 *SyntaxError using only the message string, and discards the underlying IO error chain. This breaks errors.As for callers checking IO errors such as *http.MaxBytesError, which is a common pattern for returning HTTP 413 on oversized request bodies. Fix by checking whether the SyntacticError wraps an IO error and unwrapping it directly, matching v1 behavior which returned IO errors without wrapping. Fixes #77789 Change-Id: Idad84a006a0905b4a20125f676634e1000fb5f48 Reviewed-on: https://go-review.googlesource.com/c/go/+/748860 Reviewed-by: Joseph Tsai Reviewed-by: Cherry Mui LUCI-TryBot-Result: Go LUCI Reviewed-by: David Chase --- src/encoding/json/v2_scanner.go | 5 +++++ src/encoding/json/v2_stream_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) (limited to 'src/encoding') diff --git a/src/encoding/json/v2_scanner.go b/src/encoding/json/v2_scanner.go index aef045f466..f08a0bcce3 100644 --- a/src/encoding/json/v2_scanner.go +++ b/src/encoding/json/v2_scanner.go @@ -56,6 +56,11 @@ var errUnexpectedEnd = errors.New("unexpected end of JSON input") func transformSyntacticError(err error) error { switch serr, ok := err.(*jsontext.SyntacticError); { case serr != nil: + // If the SyntacticError wraps an IO error, unwrap it + // to match v1 behavior which returned IO errors directly. + if export.IsIOError(serr.Err) { + return errors.Unwrap(serr.Err) + } if serr.Err == io.ErrUnexpectedEOF { serr.Err = errUnexpectedEnd } diff --git a/src/encoding/json/v2_stream_test.go b/src/encoding/json/v2_stream_test.go index d7f9f11084..b185b46acb 100644 --- a/src/encoding/json/v2_stream_test.go +++ b/src/encoding/json/v2_stream_test.go @@ -8,6 +8,7 @@ package json import ( "bytes" + "errors" "io" "log" "net" @@ -613,3 +614,26 @@ func TestDecoderInputOffset(t *testing.T) { t.Fatal("unconsumed testdata") } } + +func TestDecoderMaxBytesError(t *testing.T) { + // Verify that Decoder.Decode returns the underlying IO error + // (not wrapped in *SyntaxError) when http.MaxBytesReader + // triggers a read limit, matching v1 behavior. + oversized := strings.Repeat("x", 1<<20+1) + body := `{"name":"` + oversized + `"}` + + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) + rec := httptest.NewRecorder() + req.Body = http.MaxBytesReader(rec, req.Body, 1<<20) + + var v map[string]any + err := NewDecoder(req.Body).Decode(&v) + if err == nil { + t.Fatal("expected error, got nil") + } + + var maxBytesErr *http.MaxBytesError + if !errors.As(err, &maxBytesErr) { + t.Errorf("errors.As(err, *http.MaxBytesError) = false, want true\nerror type: %T\nerror: %v", err, err) + } +} -- cgit v1.3