Skip to content

Commit a0a99cb

Browse files
dsnetgopherbot
authored andcommitted
encoding/json/v2: report wrapped io.ErrUnexpectedEOF
In the event that the input is just JSON whitespace, the underlying jsontext.Decoder treats this as an empty stream and reports io.EOF. The logic in unmarshalFull simply casted io.EOF as io.ErrUnexpectedEOF, which is inconsistent with how all other io.ErrUnexpectedEOF are reported, which are wrapped within a jsontext.SyntacticError. Do the same thing for consistency. We add a v1 test (without goexperiment.jsonv2) to verify that the behavior is identical to how v1 has always behaved. We add a v1in2 test (with goexperiment.jsonv2) to verify that the v1in2 behavior correctly replicates historical v1 behavior. We also fix a faulty check in v1 Decoder.Decode, where it tried to detect errUnexpectedEnd and return an unwrapped io.ErrUnexpectedEOF error. This is the exact semantic that v1 has always done in streaming Decoder.Decode (but not non-streaming Unmarshal). There is a prior bug reported in #25956 about this inconsistency, but we aim to preserve historical v1 behavior to reduce the probability of churn when v1 is re-implemented in terms of v2. Fixes #74548 Change-Id: Ibca52c3699ff3c09141e081c85f853781a86ec8e Reviewed-on: https://go-review.googlesource.com/c/go/+/687115 Auto-Submit: Joseph Tsai <[email protected]> Reviewed-by: Carlos Amedee <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Damien Neil <[email protected]>
1 parent 9d04122 commit a0a99cb

File tree

6 files changed

+40
-7
lines changed

6 files changed

+40
-7
lines changed

src/encoding/json/decode_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"errors"
1313
"fmt"
1414
"image"
15+
"io"
1516
"maps"
1617
"math"
1718
"math/big"
@@ -469,11 +470,13 @@ var unmarshalTests = []struct {
469470
{CaseName: Name(""), in: `{"alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true},
470471

471472
// syntax errors
473+
{CaseName: Name(""), in: ``, ptr: new(any), err: &SyntaxError{"unexpected end of JSON input", 0}},
474+
{CaseName: Name(""), in: " \n\r\t", ptr: new(any), err: &SyntaxError{"unexpected end of JSON input", 4}},
475+
{CaseName: Name(""), in: `[2, 3`, ptr: new(any), err: &SyntaxError{"unexpected end of JSON input", 5}},
472476
{CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object key", 17}},
473477
{CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", 9}},
474478
{CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", 8}, useNumber: true},
475-
{CaseName: Name(""), in: `[2, 3`, err: &SyntaxError{msg: "unexpected end of JSON input", Offset: 5}},
476-
{CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{msg: "invalid character '}' in numeric literal", Offset: 9}},
479+
{CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{"invalid character '}' in numeric literal", 9}},
477480

478481
// raw value errors
479482
{CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}},
@@ -1377,6 +1380,14 @@ func TestUnmarshal(t *testing.T) {
13771380
if tt.disallowUnknownFields {
13781381
dec.DisallowUnknownFields()
13791382
}
1383+
if tt.err != nil && strings.Contains(tt.err.Error(), "unexpected end of JSON input") {
1384+
// In streaming mode, we expect EOF or ErrUnexpectedEOF instead.
1385+
if strings.TrimSpace(tt.in) == "" {
1386+
tt.err = io.EOF
1387+
} else {
1388+
tt.err = io.ErrUnexpectedEOF
1389+
}
1390+
}
13801391
if err := dec.Decode(v.Interface()); !equalError(err, tt.err) {
13811392
t.Fatalf("%s: Decode error:\n\tgot: %v\n\twant: %v\n\n\tgot: %#v\n\twant: %#v", tt.Where, err, tt.err, err, tt.err)
13821393
} else if err != nil && tt.out == nil {

src/encoding/json/v2/arshal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,8 @@ func unmarshalFull(in *jsontext.Decoder, out any, uo *jsonopts.Struct) error {
438438
case nil:
439439
return export.Decoder(in).CheckEOF()
440440
case io.EOF:
441-
return io.ErrUnexpectedEOF
441+
offset := in.InputOffset() + int64(len(in.UnreadBuffer()))
442+
return &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
442443
default:
443444
return err
444445
}

src/encoding/json/v2/arshal_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7138,7 +7138,13 @@ func TestUnmarshal(t *testing.T) {
71387138
inBuf: ``,
71397139
inVal: addr(structAll{}),
71407140
want: addr(structAll{}),
7141-
wantErr: io.ErrUnexpectedEOF,
7141+
wantErr: &jsontext.SyntacticError{Err: io.ErrUnexpectedEOF},
7142+
}, {
7143+
name: jsontest.Name("Structs/Invalid/ErrUnexpectedEOF"),
7144+
inBuf: " \n\r\t",
7145+
inVal: addr(structAll{}),
7146+
want: addr(structAll{}),
7147+
wantErr: &jsontext.SyntacticError{Err: io.ErrUnexpectedEOF, ByteOffset: len64(" \n\r\t")},
71427148
}, {
71437149
name: jsontest.Name("Structs/Invalid/NestedErrUnexpectedEOF"),
71447150
inBuf: `{"Pointer":`,

src/encoding/json/v2_decode_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"errors"
1313
"fmt"
1414
"image"
15+
"io"
1516
"maps"
1617
"math"
1718
"math/big"
@@ -473,11 +474,13 @@ var unmarshalTests = []struct {
473474
{CaseName: Name(""), in: `{"alphabet": "xyz"}`, ptr: new(U), err: fmt.Errorf("json: unknown field \"alphabet\""), disallowUnknownFields: true},
474475

475476
// syntax errors
477+
{CaseName: Name(""), in: ``, ptr: new(any), err: &SyntaxError{errUnexpectedEnd.Error(), 0}},
478+
{CaseName: Name(""), in: " \n\r\t", ptr: new(any), err: &SyntaxError{errUnexpectedEnd.Error(), len64(" \n\r\t")}},
479+
{CaseName: Name(""), in: `[2, 3`, ptr: new(any), err: &SyntaxError{errUnexpectedEnd.Error(), len64(`[2, 3`)}},
476480
{CaseName: Name(""), in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object key", len64(`{"X": "foo", "Y"`)}},
477481
{CaseName: Name(""), in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", len64(`[1, 2, 3`)}},
478482
{CaseName: Name(""), in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", len64(`{"X":12`)}, useNumber: true},
479-
{CaseName: Name(""), in: `[2, 3`, err: &SyntaxError{msg: "unexpected end of JSON input", Offset: len64(`[2, 3`)}},
480-
{CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{msg: "invalid character '}' in numeric literal", Offset: len64(`{"F3": -`)}},
483+
{CaseName: Name(""), in: `{"F3": -}`, ptr: new(V), err: &SyntaxError{"invalid character '}' in numeric literal", len64(`{"F3": -`)}},
481484

482485
// raw value errors
483486
{CaseName: Name(""), in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", len64(``)}},
@@ -1382,6 +1385,14 @@ func TestUnmarshal(t *testing.T) {
13821385
if tt.disallowUnknownFields {
13831386
dec.DisallowUnknownFields()
13841387
}
1388+
if tt.err != nil && strings.Contains(tt.err.Error(), errUnexpectedEnd.Error()) {
1389+
// In streaming mode, we expect EOF or ErrUnexpectedEOF instead.
1390+
if strings.TrimSpace(tt.in) == "" {
1391+
tt.err = io.EOF
1392+
} else {
1393+
tt.err = io.ErrUnexpectedEOF
1394+
}
1395+
}
13851396
if err := dec.Decode(v.Interface()); !equalError(err, tt.err) {
13861397
t.Fatalf("%s: Decode error:\n\tgot: %v\n\twant: %v\n\n\tgot: %#v\n\twant: %#v", tt.Where, err, tt.err, err, tt.err)
13871398
} else if err != nil && tt.out == nil {

src/encoding/json/v2_scanner.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ func checkValid(data []byte) error {
3030
xd := export.Decoder(d)
3131
xd.Struct.Flags.Set(jsonflags.AllowDuplicateNames | jsonflags.AllowInvalidUTF8 | 1)
3232
if _, err := d.ReadValue(); err != nil {
33+
if err == io.EOF {
34+
offset := d.InputOffset() + int64(len(d.UnreadBuffer()))
35+
err = &jsontext.SyntacticError{ByteOffset: offset, Err: io.ErrUnexpectedEOF}
36+
}
3337
return transformSyntacticError(err)
3438
}
3539
if err := xd.CheckEOF(); err != nil {

src/encoding/json/v2_stream.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (dec *Decoder) Decode(v any) error {
6868
b, err := dec.dec.ReadValue()
6969
if err != nil {
7070
dec.err = transformSyntacticError(err)
71-
if dec.err == errUnexpectedEnd {
71+
if dec.err.Error() == errUnexpectedEnd.Error() {
7272
// NOTE: Decode has always been inconsistent with Unmarshal
7373
// with regard to the exact error value for truncated input.
7474
dec.err = io.ErrUnexpectedEOF

0 commit comments

Comments
 (0)