Skip to content

Commit 4a741df

Browse files
committed
common/hexutil: wrap errors in json.UnmarshalTypeError
This adds type and struct field context to error messages. Instead of "hex string of odd length" users will now see "json: cannot unmarshal hex string of odd length into Go struct field SendTxArgs.from of type common.Address".
1 parent b664bed commit 4a741df

File tree

5 files changed

+102
-59
lines changed

5 files changed

+102
-59
lines changed

common/hexutil/hexutil.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ package hexutil
3232

3333
import (
3434
"encoding/hex"
35-
"errors"
3635
"fmt"
3736
"math/big"
3837
"strconv"
@@ -41,17 +40,23 @@ import (
4140
const uintBits = 32 << (uint64(^uint(0)) >> 63)
4241

4342
var (
44-
ErrEmptyString = errors.New("empty hex string")
45-
ErrMissingPrefix = errors.New("missing 0x prefix for hex data")
46-
ErrSyntax = errors.New("invalid hex")
47-
ErrEmptyNumber = errors.New("hex number has no digits after 0x")
48-
ErrLeadingZero = errors.New("hex number has leading zero digits after 0x")
49-
ErrOddLength = errors.New("hex string has odd length")
50-
ErrUint64Range = errors.New("hex number does not fit into 64 bits")
51-
ErrUintRange = fmt.Errorf("hex number does not fit into %d bits", uintBits)
52-
ErrBig256Range = errors.New("hex number does not fit into 256 bits")
43+
ErrEmptyString = &decError{"empty hex string"}
44+
ErrSyntax = &decError{"invalid hex string"}
45+
ErrMissingPrefix = &decError{"hex string without 0x prefix"}
46+
ErrOddLength = &decError{"hex string of odd length"}
47+
ErrEmptyNumber = &decError{"hex string \"0x\""}
48+
ErrLeadingZero = &decError{"hex number with leading zero digits"}
49+
ErrUint64Range = &decError{"hex number > 64 bits"}
50+
ErrUintRange = &decError{fmt.Sprintf("hex number > %d bits", uintBits)}
51+
ErrBig256Range = &decError{"hex number > 256 bits"}
5352
)
5453

54+
type decError struct{ msg string }
55+
56+
func (err decError) Error() string {
57+
return string(err.msg)
58+
}
59+
5560
// Decode decodes a hex string with 0x prefix.
5661
func Decode(input string) ([]byte, error) {
5762
if len(input) == 0 {

common/hexutil/json.go

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ package hexutil
1818

1919
import (
2020
"encoding/hex"
21-
"errors"
21+
"encoding/json"
2222
"fmt"
2323
"math/big"
24+
"reflect"
2425
"strconv"
2526
)
2627

2728
var (
28-
textZero = []byte(`0x0`)
29-
errNonString = errors.New("cannot unmarshal non-string as hex data")
29+
textZero = []byte(`0x0`)
30+
bytesT = reflect.TypeOf(Bytes(nil))
31+
bigT = reflect.TypeOf((*Big)(nil))
32+
uintT = reflect.TypeOf(Uint(0))
33+
uint64T = reflect.TypeOf(Uint64(0))
3034
)
3135

3236
// Bytes marshals/unmarshals as a JSON string with 0x prefix.
@@ -44,9 +48,9 @@ func (b Bytes) MarshalText() ([]byte, error) {
4448
// UnmarshalJSON implements json.Unmarshaler.
4549
func (b *Bytes) UnmarshalJSON(input []byte) error {
4650
if !isString(input) {
47-
return errNonString
51+
return errNonString(bytesT)
4852
}
49-
return b.UnmarshalText(input[1 : len(input)-1])
53+
return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bytesT)
5054
}
5155

5256
// UnmarshalText implements encoding.TextUnmarshaler.
@@ -69,6 +73,16 @@ func (b Bytes) String() string {
6973
return Encode(b)
7074
}
7175

76+
// UnmarshalFixedJSON decodes the input as a string with 0x prefix. The length of out
77+
// determines the required input length. This function is commonly used to implement the
78+
// UnmarshalJSON method for fixed-size types.
79+
func UnmarshalFixedJSON(typ reflect.Type, input, out []byte) error {
80+
if !isString(input) {
81+
return errNonString(typ)
82+
}
83+
return wrapTypeError(UnmarshalFixedText(typ.String(), input[1:len(input)-1], out), typ)
84+
}
85+
7286
// UnmarshalFixedText decodes the input as a string with 0x prefix. The length of out
7387
// determines the required input length. This function is commonly used to implement the
7488
// UnmarshalText method for fixed-size types.
@@ -127,9 +141,9 @@ func (b Big) MarshalText() ([]byte, error) {
127141
// UnmarshalJSON implements json.Unmarshaler.
128142
func (b *Big) UnmarshalJSON(input []byte) error {
129143
if !isString(input) {
130-
return errNonString
144+
return errNonString(bigT)
131145
}
132-
return b.UnmarshalText(input[1 : len(input)-1])
146+
return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bigT)
133147
}
134148

135149
// UnmarshalText implements encoding.TextUnmarshaler
@@ -189,9 +203,9 @@ func (b Uint64) MarshalText() ([]byte, error) {
189203
// UnmarshalJSON implements json.Unmarshaler.
190204
func (b *Uint64) UnmarshalJSON(input []byte) error {
191205
if !isString(input) {
192-
return errNonString
206+
return errNonString(uint64T)
193207
}
194-
return b.UnmarshalText(input[1 : len(input)-1])
208+
return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uint64T)
195209
}
196210

197211
// UnmarshalText implements encoding.TextUnmarshaler
@@ -233,9 +247,9 @@ func (b Uint) MarshalText() ([]byte, error) {
233247
// UnmarshalJSON implements json.Unmarshaler.
234248
func (b *Uint) UnmarshalJSON(input []byte) error {
235249
if !isString(input) {
236-
return errNonString
250+
return errNonString(uintT)
237251
}
238-
return b.UnmarshalText(input[1 : len(input)-1])
252+
return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uintT)
239253
}
240254

241255
// UnmarshalText implements encoding.TextUnmarshaler.
@@ -295,3 +309,14 @@ func checkNumberText(input []byte) (raw []byte, err error) {
295309
}
296310
return input, nil
297311
}
312+
313+
func wrapTypeError(err error, typ reflect.Type) error {
314+
if _, ok := err.(*decError); ok {
315+
return &json.UnmarshalTypeError{Value: err.Error(), Type: typ}
316+
}
317+
return err
318+
}
319+
320+
func errNonString(typ reflect.Type) error {
321+
return &json.UnmarshalTypeError{Value: "non-string", Type: typ}
322+
}

common/hexutil/json_test.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ var errJSONEOF = errors.New("unexpected end of JSON input")
6262
var unmarshalBytesTests = []unmarshalTest{
6363
// invalid encoding
6464
{input: "", wantErr: errJSONEOF},
65-
{input: "null", wantErr: errNonString},
66-
{input: "10", wantErr: errNonString},
67-
{input: `"0"`, wantErr: ErrMissingPrefix},
68-
{input: `"0x0"`, wantErr: ErrOddLength},
69-
{input: `"0xxx"`, wantErr: ErrSyntax},
70-
{input: `"0x01zz01"`, wantErr: ErrSyntax},
65+
{input: "null", wantErr: errNonString(bytesT)},
66+
{input: "10", wantErr: errNonString(bytesT)},
67+
{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bytesT)},
68+
{input: `"0x0"`, wantErr: wrapTypeError(ErrOddLength, bytesT)},
69+
{input: `"0xxx"`, wantErr: wrapTypeError(ErrSyntax, bytesT)},
70+
{input: `"0x01zz01"`, wantErr: wrapTypeError(ErrSyntax, bytesT)},
7171

7272
// valid encoding
7373
{input: `""`, want: referenceBytes("")},
@@ -127,16 +127,16 @@ func TestMarshalBytes(t *testing.T) {
127127
var unmarshalBigTests = []unmarshalTest{
128128
// invalid encoding
129129
{input: "", wantErr: errJSONEOF},
130-
{input: "null", wantErr: errNonString},
131-
{input: "10", wantErr: errNonString},
132-
{input: `"0"`, wantErr: ErrMissingPrefix},
133-
{input: `"0x"`, wantErr: ErrEmptyNumber},
134-
{input: `"0x01"`, wantErr: ErrLeadingZero},
135-
{input: `"0xx"`, wantErr: ErrSyntax},
136-
{input: `"0x1zz01"`, wantErr: ErrSyntax},
130+
{input: "null", wantErr: errNonString(bigT)},
131+
{input: "10", wantErr: errNonString(bigT)},
132+
{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bigT)},
133+
{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, bigT)},
134+
{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, bigT)},
135+
{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, bigT)},
136+
{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, bigT)},
137137
{
138138
input: `"0x10000000000000000000000000000000000000000000000000000000000000000"`,
139-
wantErr: ErrBig256Range,
139+
wantErr: wrapTypeError(ErrBig256Range, bigT),
140140
},
141141

142142
// valid encoding
@@ -208,14 +208,14 @@ func TestMarshalBig(t *testing.T) {
208208
var unmarshalUint64Tests = []unmarshalTest{
209209
// invalid encoding
210210
{input: "", wantErr: errJSONEOF},
211-
{input: "null", wantErr: errNonString},
212-
{input: "10", wantErr: errNonString},
213-
{input: `"0"`, wantErr: ErrMissingPrefix},
214-
{input: `"0x"`, wantErr: ErrEmptyNumber},
215-
{input: `"0x01"`, wantErr: ErrLeadingZero},
216-
{input: `"0xfffffffffffffffff"`, wantErr: ErrUint64Range},
217-
{input: `"0xx"`, wantErr: ErrSyntax},
218-
{input: `"0x1zz01"`, wantErr: ErrSyntax},
211+
{input: "null", wantErr: errNonString(uint64T)},
212+
{input: "10", wantErr: errNonString(uint64T)},
213+
{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uint64T)},
214+
{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uint64T)},
215+
{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uint64T)},
216+
{input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUint64Range, uint64T)},
217+
{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uint64T)},
218+
{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uint64T)},
219219

220220
// valid encoding
221221
{input: `""`, want: uint64(0)},
@@ -298,15 +298,15 @@ var (
298298
var unmarshalUintTests = []unmarshalTest{
299299
// invalid encoding
300300
{input: "", wantErr: errJSONEOF},
301-
{input: "null", wantErr: errNonString},
302-
{input: "10", wantErr: errNonString},
303-
{input: `"0"`, wantErr: ErrMissingPrefix},
304-
{input: `"0x"`, wantErr: ErrEmptyNumber},
305-
{input: `"0x01"`, wantErr: ErrLeadingZero},
306-
{input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: ErrUintRange},
307-
{input: `"0xfffffffffffffffff"`, wantErr: ErrUintRange},
308-
{input: `"0xx"`, wantErr: ErrSyntax},
309-
{input: `"0x1zz01"`, wantErr: ErrSyntax},
301+
{input: "null", wantErr: errNonString(uintT)},
302+
{input: "10", wantErr: errNonString(uintT)},
303+
{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uintT)},
304+
{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uintT)},
305+
{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uintT)},
306+
{input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)},
307+
{input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUintRange, uintT)},
308+
{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uintT)},
309+
{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uintT)},
310310

311311
// valid encoding
312312
{input: `""`, want: uint(0)},
@@ -317,7 +317,7 @@ var unmarshalUintTests = []unmarshalTest{
317317
{input: `"0x1122aaff"`, want: uint(0x1122aaff)},
318318
{input: `"0xbbb"`, want: uint(0xbbb)},
319319
{input: `"0xffffffff"`, want: uint(0xffffffff)},
320-
{input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: ErrUintRange},
320+
{input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)},
321321
}
322322

323323
func TestUnmarshalUint(t *testing.T) {

common/types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const (
3131
AddressLength = 20
3232
)
3333

34+
var (
35+
hashT = reflect.TypeOf(Hash{})
36+
addressT = reflect.TypeOf(Address{})
37+
)
38+
3439
// Hash represents the 32 byte Keccak256 hash of arbitrary data.
3540
type Hash [HashLength]byte
3641

@@ -72,6 +77,11 @@ func (h *Hash) UnmarshalText(input []byte) error {
7277
return hexutil.UnmarshalFixedText("Hash", input, h[:])
7378
}
7479

80+
// UnmarshalJSON parses a hash in hex syntax.
81+
func (h *Hash) UnmarshalJSON(input []byte) error {
82+
return hexutil.UnmarshalFixedJSON(hashT, input, h[:])
83+
}
84+
7585
// MarshalText returns the hex representation of h.
7686
func (h Hash) MarshalText() ([]byte, error) {
7787
return hexutil.Bytes(h[:]).MarshalText()
@@ -194,6 +204,11 @@ func (a *Address) UnmarshalText(input []byte) error {
194204
return hexutil.UnmarshalFixedText("Address", input, a[:])
195205
}
196206

207+
// UnmarshalJSON parses a hash in hex syntax.
208+
func (a *Address) UnmarshalJSON(input []byte) error {
209+
return hexutil.UnmarshalFixedJSON(addressT, input, a[:])
210+
}
211+
197212
// UnprefixedHash allows marshaling an Address without 0x prefix.
198213
type UnprefixedAddress Address
199214

common/types_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import (
2121
"math/big"
2222
"strings"
2323
"testing"
24-
25-
"github.com/ethereum/go-ethereum/common/hexutil"
2624
)
2725

2826
func TestBytesConversion(t *testing.T) {
@@ -43,10 +41,10 @@ func TestHashJsonValidation(t *testing.T) {
4341
Size int
4442
Error string
4543
}{
46-
{"", 62, hexutil.ErrMissingPrefix.Error()},
47-
{"0x", 66, "hex string has length 66, want 64 for Hash"},
48-
{"0x", 63, hexutil.ErrOddLength.Error()},
49-
{"0x", 0, "hex string has length 0, want 64 for Hash"},
44+
{"", 62, "json: cannot unmarshal hex string without 0x prefix into Go value of type common.Hash"},
45+
{"0x", 66, "hex string has length 66, want 64 for common.Hash"},
46+
{"0x", 63, "json: cannot unmarshal hex string of odd length into Go value of type common.Hash"},
47+
{"0x", 0, "hex string has length 0, want 64 for common.Hash"},
5048
{"0x", 64, ""},
5149
{"0X", 64, ""},
5250
}

0 commit comments

Comments
 (0)