Skip to content

Commit 25aa66b

Browse files
committed
Add offset and path info to error messages
Improved error messages to include byte offset information and, for the reflection-based API, path information for nested structures using JSON Pointer format. For example, errors may now show "at offset 1234, path /city/names/en" or "at offset 1234, path /list/0/name" instead of just the underlying error message. The implementation maintains zero allocation on the happy path through retroactive path building during error unwinding.
1 parent 461ae40 commit 25aa66b

File tree

8 files changed

+641
-56
lines changed

8 files changed

+641
-56
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
elements, and map values. The custom unmarshaler is now called recursively
2323
for any type that implements the `Unmarshaler` interface, similar to
2424
`encoding/json`.
25+
- Improved error messages to include byte offset information and, for the
26+
reflection-based API, path information for nested structures using JSON
27+
Pointer format. For example, errors may now show "at offset 1234, path
28+
/city/names/en" or "at offset 1234, path /list/0/name" instead of just the
29+
underlying error message.
2530

2631
## 2.0.0-beta.3 - 2025-02-16
2732

internal/decoder/decoder.go

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ func NewDecoder(d DataDecoder, offset uint, options ...DecoderOption) *Decoder {
3333
for _, option := range options {
3434
option(&opts)
3535
}
36-
return &Decoder{d: d, offset: offset, opts: opts}
36+
37+
decoder := &Decoder{
38+
d: d,
39+
offset: offset,
40+
opts: opts,
41+
}
42+
43+
return decoder
3744
}
3845

3946
// ReadBool reads the value pointed by the decoder as a bool.
@@ -42,14 +49,14 @@ func NewDecoder(d DataDecoder, offset uint, options ...DecoderOption) *Decoder {
4249
func (d *Decoder) ReadBool() (bool, error) {
4350
size, offset, err := d.decodeCtrlDataAndFollow(KindBool)
4451
if err != nil {
45-
return false, err
52+
return false, d.wrapError(err)
4653
}
4754

4855
if size > 1 {
49-
return false, mmdberrors.NewInvalidDatabaseError(
56+
return false, d.wrapError(mmdberrors.NewInvalidDatabaseError(
5057
"the MaxMind DB file's data section contains bad data (bool size of %v)",
5158
size,
52-
)
59+
))
5360
}
5461

5562
var value bool
@@ -64,16 +71,20 @@ func (d *Decoder) ReadBool() (bool, error) {
6471
func (d *Decoder) ReadString() (string, error) {
6572
val, err := d.readBytes(KindString)
6673
if err != nil {
67-
return "", err
74+
return "", d.wrapError(err)
6875
}
69-
return string(val), err
76+
return string(val), nil
7077
}
7178

7279
// ReadBytes reads the value pointed by the decoder as bytes.
7380
//
7481
// Returns an error if the database is malformed or if the pointed value is not bytes.
7582
func (d *Decoder) ReadBytes() ([]byte, error) {
76-
return d.readBytes(KindBytes)
83+
val, err := d.readBytes(KindBytes)
84+
if err != nil {
85+
return nil, d.wrapError(err)
86+
}
87+
return val, nil
7788
}
7889

7990
// ReadFloat32 reads the value pointed by the decoder as a float32.
@@ -82,19 +93,19 @@ func (d *Decoder) ReadBytes() ([]byte, error) {
8293
func (d *Decoder) ReadFloat32() (float32, error) {
8394
size, offset, err := d.decodeCtrlDataAndFollow(KindFloat32)
8495
if err != nil {
85-
return 0, err
96+
return 0, d.wrapError(err)
8697
}
8798

8899
if size != 4 {
89-
return 0, mmdberrors.NewInvalidDatabaseError(
100+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
90101
"the MaxMind DB file's data section contains bad data (float32 size of %v)",
91102
size,
92-
)
103+
))
93104
}
94105

95106
value, nextOffset, err := d.d.DecodeFloat32(size, offset)
96107
if err != nil {
97-
return 0, err
108+
return 0, d.wrapError(err)
98109
}
99110

100111
d.setNextOffset(nextOffset)
@@ -107,19 +118,19 @@ func (d *Decoder) ReadFloat32() (float32, error) {
107118
func (d *Decoder) ReadFloat64() (float64, error) {
108119
size, offset, err := d.decodeCtrlDataAndFollow(KindFloat64)
109120
if err != nil {
110-
return 0, err
121+
return 0, d.wrapError(err)
111122
}
112123

113124
if size != 8 {
114-
return 0, mmdberrors.NewInvalidDatabaseError(
125+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
115126
"the MaxMind DB file's data section contains bad data (float64 size of %v)",
116127
size,
117-
)
128+
))
118129
}
119130

120131
value, nextOffset, err := d.d.DecodeFloat64(size, offset)
121132
if err != nil {
122-
return 0, err
133+
return 0, d.wrapError(err)
123134
}
124135

125136
d.setNextOffset(nextOffset)
@@ -132,19 +143,19 @@ func (d *Decoder) ReadFloat64() (float64, error) {
132143
func (d *Decoder) ReadInt32() (int32, error) {
133144
size, offset, err := d.decodeCtrlDataAndFollow(KindInt32)
134145
if err != nil {
135-
return 0, err
146+
return 0, d.wrapError(err)
136147
}
137148

138149
if size > 4 {
139-
return 0, mmdberrors.NewInvalidDatabaseError(
150+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
140151
"the MaxMind DB file's data section contains bad data (int32 size of %v)",
141152
size,
142-
)
153+
))
143154
}
144155

145156
value, nextOffset, err := d.d.DecodeInt32(size, offset)
146157
if err != nil {
147-
return 0, err
158+
return 0, d.wrapError(err)
148159
}
149160

150161
d.setNextOffset(nextOffset)
@@ -158,19 +169,19 @@ func (d *Decoder) ReadInt32() (int32, error) {
158169
func (d *Decoder) ReadUInt16() (uint16, error) {
159170
size, offset, err := d.decodeCtrlDataAndFollow(KindUint16)
160171
if err != nil {
161-
return 0, err
172+
return 0, d.wrapError(err)
162173
}
163174

164175
if size > 2 {
165-
return 0, mmdberrors.NewInvalidDatabaseError(
176+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
166177
"the MaxMind DB file's data section contains bad data (uint16 size of %v)",
167178
size,
168-
)
179+
))
169180
}
170181

171182
value, nextOffset, err := d.d.DecodeUint16(size, offset)
172183
if err != nil {
173-
return 0, err
184+
return 0, d.wrapError(err)
174185
}
175186

176187
d.setNextOffset(nextOffset)
@@ -183,19 +194,19 @@ func (d *Decoder) ReadUInt16() (uint16, error) {
183194
func (d *Decoder) ReadUInt32() (uint32, error) {
184195
size, offset, err := d.decodeCtrlDataAndFollow(KindUint32)
185196
if err != nil {
186-
return 0, err
197+
return 0, d.wrapError(err)
187198
}
188199

189200
if size > 4 {
190-
return 0, mmdberrors.NewInvalidDatabaseError(
201+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
191202
"the MaxMind DB file's data section contains bad data (uint32 size of %v)",
192203
size,
193-
)
204+
))
194205
}
195206

196207
value, nextOffset, err := d.d.DecodeUint32(size, offset)
197208
if err != nil {
198-
return 0, err
209+
return 0, d.wrapError(err)
199210
}
200211

201212
d.setNextOffset(nextOffset)
@@ -208,19 +219,19 @@ func (d *Decoder) ReadUInt32() (uint32, error) {
208219
func (d *Decoder) ReadUInt64() (uint64, error) {
209220
size, offset, err := d.decodeCtrlDataAndFollow(KindUint64)
210221
if err != nil {
211-
return 0, err
222+
return 0, d.wrapError(err)
212223
}
213224

214225
if size > 8 {
215-
return 0, mmdberrors.NewInvalidDatabaseError(
226+
return 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
216227
"the MaxMind DB file's data section contains bad data (uint64 size of %v)",
217228
size,
218-
)
229+
))
219230
}
220231

221232
value, nextOffset, err := d.d.DecodeUint64(size, offset)
222233
if err != nil {
223-
return 0, err
234+
return 0, d.wrapError(err)
224235
}
225236

226237
d.setNextOffset(nextOffset)
@@ -233,22 +244,22 @@ func (d *Decoder) ReadUInt64() (uint64, error) {
233244
func (d *Decoder) ReadUInt128() (hi, lo uint64, err error) {
234245
size, offset, err := d.decodeCtrlDataAndFollow(KindUint128)
235246
if err != nil {
236-
return 0, 0, err
247+
return 0, 0, d.wrapError(err)
237248
}
238249

239250
if size > 16 {
240-
return 0, 0, mmdberrors.NewInvalidDatabaseError(
251+
return 0, 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
241252
"the MaxMind DB file's data section contains bad data (uint128 size of %v)",
242253
size,
243-
)
254+
))
244255
}
245256

246257
if offset+size > uint(len(d.d.Buffer())) {
247-
return 0, 0, mmdberrors.NewInvalidDatabaseError(
258+
return 0, 0, d.wrapError(mmdberrors.NewInvalidDatabaseError(
248259
"the MaxMind DB file's data section contains bad data (offset+size %d exceeds buffer length %d)",
249260
offset+size,
250261
len(d.d.Buffer()),
251-
)
262+
))
252263
}
253264

254265
for _, b := range d.d.Buffer()[offset : offset+size] {
@@ -276,7 +287,7 @@ func (d *Decoder) ReadMap() iter.Seq2[[]byte, error] {
276287
return func(yield func([]byte, error) bool) {
277288
size, offset, err := d.decodeCtrlDataAndFollow(KindMap)
278289
if err != nil {
279-
yield(nil, err)
290+
yield(nil, d.wrapError(err))
280291
return
281292
}
282293

@@ -285,7 +296,7 @@ func (d *Decoder) ReadMap() iter.Seq2[[]byte, error] {
285296
for range size {
286297
key, keyEndOffset, err := d.d.DecodeKey(currentOffset)
287298
if err != nil {
288-
yield(nil, err)
299+
yield(nil, d.wrapErrorAtOffset(err, currentOffset))
289300
return
290301
}
291302

@@ -300,7 +311,7 @@ func (d *Decoder) ReadMap() iter.Seq2[[]byte, error] {
300311
// Skip the value to get to next key-value pair
301312
valueEndOffset, err := d.d.NextValueOffset(keyEndOffset, 1)
302313
if err != nil {
303-
yield(nil, err)
314+
yield(nil, d.wrapError(err))
304315
return
305316
}
306317
currentOffset = valueEndOffset
@@ -318,7 +329,7 @@ func (d *Decoder) ReadSlice() iter.Seq[error] {
318329
return func(yield func(error) bool) {
319330
size, offset, err := d.decodeCtrlDataAndFollow(KindSlice)
320331
if err != nil {
321-
yield(err)
332+
yield(d.wrapError(err))
322333
return
323334
}
324335

@@ -344,7 +355,7 @@ func (d *Decoder) ReadSlice() iter.Seq[error] {
344355
// Advance to next element
345356
nextOffset, err := d.d.NextValueOffset(currentOffset, 1)
346357
if err != nil {
347-
yield(err)
358+
yield(d.wrapError(err))
348359
return
349360
}
350361
currentOffset = nextOffset
@@ -362,7 +373,7 @@ func (d *Decoder) SkipValue() error {
362373
// We can reuse the existing nextValueOffset logic by jumping to the next value
363374
nextOffset, err := d.d.NextValueOffset(d.offset, 1)
364375
if err != nil {
365-
return err
376+
return d.wrapError(err)
366377
}
367378
d.reset(nextOffset)
368379
return nil
@@ -373,7 +384,7 @@ func (d *Decoder) SkipValue() error {
373384
func (d *Decoder) PeekKind() (Kind, error) {
374385
kindNum, _, _, err := d.d.DecodeCtrlData(d.offset)
375386
if err != nil {
376-
return 0, err
387+
return 0, d.wrapError(err)
377388
}
378389

379390
// Follow pointers to get the actual kind
@@ -384,14 +395,14 @@ func (d *Decoder) PeekKind() (Kind, error) {
384395
var size uint
385396
kindNum, size, dataOffset, err = d.d.DecodeCtrlData(dataOffset)
386397
if err != nil {
387-
return 0, err
398+
return 0, d.wrapError(err)
388399
}
389400
if kindNum != KindPointer {
390401
break
391402
}
392403
dataOffset, _, err = d.d.DecodePointer(size, dataOffset)
393404
if err != nil {
394-
return 0, err
405+
return 0, d.wrapError(err)
395406
}
396407
}
397408
}
@@ -431,14 +442,14 @@ func (d *Decoder) decodeCtrlDataAndFollow(expectedKind Kind) (uint, uint, error)
431442
var err error
432443
kindNum, size, dataOffset, err = d.d.DecodeCtrlData(dataOffset)
433444
if err != nil {
434-
return 0, 0, err
445+
return 0, 0, err // Don't wrap here, let caller wrap
435446
}
436447

437448
if kindNum == KindPointer {
438449
var nextOffset uint
439450
dataOffset, nextOffset, err = d.d.DecodePointer(size, dataOffset)
440451
if err != nil {
441-
return 0, 0, err
452+
return 0, 0, err // Don't wrap here, let caller wrap
442453
}
443454
d.setNextOffset(nextOffset)
444455
continue
@@ -455,7 +466,7 @@ func (d *Decoder) decodeCtrlDataAndFollow(expectedKind Kind) (uint, uint, error)
455466
func (d *Decoder) readBytes(kind Kind) ([]byte, error) {
456467
size, offset, err := d.decodeCtrlDataAndFollow(kind)
457468
if err != nil {
458-
return nil, err
469+
return nil, err // Return unwrapped - caller will wrap
459470
}
460471

461472
if offset+size > uint(len(d.d.Buffer())) {

internal/decoder/error_context.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package decoder
2+
3+
import (
4+
"github.com/oschwald/maxminddb-golang/v2/internal/mmdberrors"
5+
)
6+
7+
// errorContext provides zero-allocation error context tracking for Decoder.
8+
// This is only used when an error occurs, ensuring no performance impact
9+
// on the happy path.
10+
type errorContext struct {
11+
path *mmdberrors.PathBuilder // Only allocated when needed
12+
}
13+
14+
// BuildPath implements mmdberrors.ErrorContextTracker.
15+
// This is only called when an error occurs, so allocation is acceptable.
16+
func (e *errorContext) BuildPath() string {
17+
if e.path == nil {
18+
return "" // No path tracking enabled
19+
}
20+
return e.path.Build()
21+
}
22+
23+
// wrapError wraps an error with context information when an error occurs.
24+
// Zero allocation on happy path - only allocates when error != nil.
25+
func (d *Decoder) wrapError(err error) error {
26+
if err == nil {
27+
return nil
28+
}
29+
// Only wrap with context when an error actually occurs
30+
return mmdberrors.WrapWithContext(err, d.offset, nil)
31+
}
32+
33+
// wrapErrorAtOffset wraps an error with context at a specific offset.
34+
// Used when the error occurs at a different offset than the decoder's current position.
35+
func (*Decoder) wrapErrorAtOffset(err error, offset uint) error {
36+
if err == nil {
37+
return nil
38+
}
39+
return mmdberrors.WrapWithContext(err, offset, nil)
40+
}
41+
42+
// Example of how to integrate into existing decoder methods:
43+
// Instead of:
44+
// return mmdberrors.NewInvalidDatabaseError("message")
45+
// Use:
46+
// return d.wrapError(mmdberrors.NewInvalidDatabaseError("message"))
47+
//
48+
// This adds zero overhead when no error occurs, but provides rich context
49+
// when errors do happen.

0 commit comments

Comments
 (0)