Skip to content

Commit ba6725f

Browse files
committed
GODRIVER-1803 add error helpers (#554)
1 parent 91351a0 commit ba6725f

File tree

4 files changed

+217
-3
lines changed

4 files changed

+217
-3
lines changed

mongo/doc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
// like InsertMany and BulkWrite, can return an error representing multiple errors, and in those cases the ServerError
8787
// functions will return true if any of the contained errors satisfy the check.
8888
//
89+
// There are also helper functions to check for certain specific types of errors:
90+
// IsDuplicateKeyError(error)
91+
// IsNetworkError(error)
92+
// IsTimeout(error)
93+
//
8994
// Potential DNS Issues
9095
//
9196
// Building with Go 1.11+ and using connection strings with the "mongodb+srv"[1] scheme is

mongo/errors.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package mongo
88

99
import (
1010
"bytes"
11+
"context"
1112
"errors"
1213
"fmt"
14+
"net"
1315
"strings"
1416

1517
"go.mongodb.org/mongo-driver/bson"
@@ -71,6 +73,60 @@ func replaceErrors(err error) error {
7173
return err
7274
}
7375

76+
// IsDuplicateKeyError returns true if err is a duplicate key error
77+
func IsDuplicateKeyError(err error) bool {
78+
// handles SERVER-7164 and SERVER-11493
79+
for ; err != nil; err = unwrap(err) {
80+
if e, ok := err.(ServerError); ok {
81+
return e.HasErrorCode(11000) || e.HasErrorCode(11001) || e.HasErrorCode(12582) ||
82+
e.HasErrorCodeWithMessage(16460, " E11000 ")
83+
}
84+
}
85+
return false
86+
}
87+
88+
// IsTimeout returns true if err is from a timeout
89+
func IsTimeout(err error) bool {
90+
for ; err != nil; err = unwrap(err) {
91+
// check unwrappable errors together
92+
if err == context.DeadlineExceeded {
93+
return true
94+
}
95+
if ne, ok := err.(net.Error); ok {
96+
return ne.Timeout()
97+
}
98+
//timeout error labels
99+
if se, ok := err.(ServerError); ok {
100+
if se.HasErrorLabel("NetworkTimeoutError") || se.HasErrorLabel("ExceededTimeLimitError") {
101+
return true
102+
}
103+
}
104+
}
105+
106+
return false
107+
}
108+
109+
// unwrap returns the inner error if err implements Unwrap(), otherwise it returns nil.
110+
func unwrap(err error) error {
111+
u, ok := err.(interface {
112+
Unwrap() error
113+
})
114+
if !ok {
115+
return nil
116+
}
117+
return u.Unwrap()
118+
}
119+
120+
// IsNetworkError returns true if err is a network error
121+
func IsNetworkError(err error) bool {
122+
for ; err != nil; err = unwrap(err) {
123+
if e, ok := err.(ServerError); ok {
124+
return e.HasErrorLabel("NetworkError")
125+
}
126+
}
127+
return false
128+
}
129+
74130
// MongocryptError represents an libmongocrypt error during client-side encryption.
75131
type MongocryptError struct {
76132
Code int32

mongo/integration/errors_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,36 @@ import (
2323
"go.mongodb.org/mongo-driver/mongo/options"
2424
)
2525

26+
type netErr struct {
27+
timeout bool
28+
}
29+
30+
func (n netErr) Error() string {
31+
return "error"
32+
}
33+
34+
func (n netErr) Timeout() bool {
35+
return n.timeout
36+
}
37+
38+
func (n netErr) Temporary() bool {
39+
return false
40+
}
41+
42+
var _ net.Error = (*netErr)(nil)
43+
44+
type wrappedError struct {
45+
err error
46+
}
47+
48+
func (we wrappedError) Error() string {
49+
return we.err.Error()
50+
}
51+
52+
func (we wrappedError) Unwrap() error {
53+
return we.err
54+
}
55+
2656
func TestErrors(t *testing.T) {
2757
mt := mtest.New(t, noClientOpts)
2858
defer mt.Close()
@@ -289,4 +319,124 @@ func TestErrors(t *testing.T) {
289319
})
290320
}
291321
})
322+
mt.Run("error helpers", func(mt *mtest.T) {
323+
//IsDuplicateKeyError
324+
mt.Run("IsDuplicateKeyError", func(mt *mtest.T) {
325+
testCases := []struct {
326+
name string
327+
err error
328+
result bool
329+
}{
330+
{"CommandError true", mongo.CommandError{11000, "", nil, "blah", nil}, true},
331+
{"CommandError false", mongo.CommandError{100, "", nil, "blah", nil}, false},
332+
{
333+
"WriteException true in writeConcernError",
334+
mongo.WriteException{
335+
&mongo.WriteConcernError{"name", 11001, "bar", nil},
336+
mongo.WriteErrors{
337+
mongo.WriteError{0, 100, "baz"},
338+
},
339+
nil,
340+
},
341+
true,
342+
},
343+
{
344+
"WriteException true in writeErrors",
345+
mongo.WriteException{
346+
&mongo.WriteConcernError{"name", 100, "bar", nil},
347+
mongo.WriteErrors{
348+
mongo.WriteError{0, 12582, "baz"},
349+
},
350+
nil,
351+
},
352+
true,
353+
},
354+
{
355+
"WriteException false",
356+
mongo.WriteException{
357+
&mongo.WriteConcernError{"name", 16460, "bar", nil},
358+
mongo.WriteErrors{
359+
mongo.WriteError{0, 100, "blah E11000 blah"},
360+
},
361+
nil,
362+
},
363+
false,
364+
},
365+
{
366+
"BulkWriteException true",
367+
mongo.BulkWriteException{
368+
&mongo.WriteConcernError{"name", 100, "bar", nil},
369+
[]mongo.BulkWriteError{
370+
{mongo.WriteError{0, 16460, "blah E11000 blah"}, &mongo.InsertOneModel{}},
371+
},
372+
[]string{"otherError"},
373+
},
374+
true,
375+
},
376+
{
377+
"BulkWriteException false",
378+
mongo.BulkWriteException{
379+
&mongo.WriteConcernError{"name", 100, "bar", nil},
380+
[]mongo.BulkWriteError{
381+
{mongo.WriteError{0, 110, "blah"}, &mongo.InsertOneModel{}},
382+
},
383+
[]string{"otherError"},
384+
},
385+
false,
386+
},
387+
{"wrapped error", wrappedError{mongo.CommandError{11000, "", nil, "blah", nil}}, true},
388+
{"other error type", errors.New("foo"), false},
389+
}
390+
for _, tc := range testCases {
391+
mt.Run(tc.name, func(mt *mtest.T) {
392+
res := mongo.IsDuplicateKeyError(tc.err)
393+
assert.Equal(mt, res, tc.result, "expected IsDuplicateKeyError %v, got %v", tc.result, res)
394+
})
395+
}
396+
})
397+
//IsNetworkError
398+
mt.Run("IsNetworkError", func(mt *mtest.T) {
399+
const networkLabel = "NetworkError"
400+
const otherLabel = "other"
401+
testCases := []struct {
402+
name string
403+
err error
404+
result bool
405+
}{
406+
{"ServerError true", mongo.CommandError{100, "", []string{networkLabel}, "blah", nil}, true},
407+
{"ServerError false", mongo.CommandError{100, "", []string{otherLabel}, "blah", nil}, false},
408+
{"wrapped error", wrappedError{mongo.CommandError{100, "", []string{networkLabel}, "blah", nil}}, true},
409+
{"other error type", errors.New("foo"), false},
410+
}
411+
for _, tc := range testCases {
412+
mt.Run(tc.name, func(mt *mtest.T) {
413+
res := mongo.IsNetworkError(tc.err)
414+
assert.Equal(mt, res, tc.result, "expected IsNetworkError %v, got %v", tc.result, res)
415+
})
416+
}
417+
})
418+
//IsTimeout
419+
mt.Run("IsTimeout", func(mt *mtest.T) {
420+
testCases := []struct {
421+
name string
422+
err error
423+
result bool
424+
}{
425+
{"context timeout", mongo.CommandError{100, "", []string{"other"}, "blah", context.DeadlineExceeded}, true},
426+
{"ServerError NetworkTimeoutError", mongo.CommandError{100, "", []string{"NetworkTimeoutError"}, "blah", nil}, true},
427+
{"ServerError ExceededTimeLimitError", mongo.CommandError{100, "", []string{"ExceededTimeLimitError"}, "blah", nil}, true},
428+
{"ServerError false", mongo.CommandError{100, "", []string{"other"}, "blah", nil}, false},
429+
{"net error true", mongo.CommandError{100, "", []string{"other"}, "blah", netErr{true}}, true},
430+
{"net error false", netErr{false}, false},
431+
{"wrapped error", wrappedError{mongo.CommandError{100, "", []string{"other"}, "blah", context.DeadlineExceeded}}, true},
432+
{"other error", errors.New("foo"), false},
433+
}
434+
for _, tc := range testCases {
435+
mt.Run(tc.name, func(mt *mtest.T) {
436+
res := mongo.IsTimeout(tc.err)
437+
assert.Equal(mt, res, tc.result, "expected IsTimeout %v, got %v", tc.result, res)
438+
})
439+
}
440+
})
441+
})
292442
}

mongo/integration/sdam_error_handling_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ func TestSDAMErrorHandling(t *testing.T) {
8484
defer cancel()
8585
_, err := mt.Coll.InsertOne(timeoutCtx, bson.D{{"test", 1}})
8686
assert.NotNil(mt, err, "expected InsertOne error, got nil")
87+
assert.True(mt, mongo.IsTimeout(err), "expected timeout error, got %v", err)
8788
assert.True(mt, isPoolCleared(), "expected pool to be cleared but was not")
8889
})
8990
mt.RunOpts("pool cleared on non-timeout network error", noClientOpts, func(mt *mtest.T) {
@@ -139,6 +140,7 @@ func TestSDAMErrorHandling(t *testing.T) {
139140

140141
_, err := mt.Coll.InsertOne(mtest.Background, bson.D{{"x", 1}})
141142
assert.NotNil(mt, err, "expected InsertOne error, got nil")
143+
assert.False(mt, mongo.IsTimeout(err), "expected non-timeout error, got %v", err)
142144
assert.True(mt, isPoolCleared(), "expected pool to be cleared but was not")
143145
})
144146
})
@@ -161,6 +163,7 @@ func TestSDAMErrorHandling(t *testing.T) {
161163

162164
_, err := mt.Coll.InsertOne(mtest.Background, bson.D{{"test", 1}})
163165
assert.NotNil(mt, err, "expected InsertOne error, got nil")
166+
assert.False(mt, mongo.IsTimeout(err), "expected non-timeout error, got %v", err)
164167
assert.True(mt, isPoolCleared(), "expected pool to be cleared but was not")
165168
})
166169
mt.Run("pool not cleared on timeout network error", func(mt *mtest.T) {
@@ -176,6 +179,7 @@ func TestSDAMErrorHandling(t *testing.T) {
176179
defer cancel()
177180
_, err = mt.Coll.Find(timeoutCtx, filter)
178181
assert.NotNil(mt, err, "expected Find error, got %v", err)
182+
assert.True(mt, mongo.IsTimeout(err), "expected timeout error, got %v", err)
179183

180184
assert.False(mt, isPoolCleared(), "expected pool to not be cleared but was")
181185
})
@@ -196,9 +200,8 @@ func TestSDAMErrorHandling(t *testing.T) {
196200
}
197201
_, err = mt.Coll.Find(findCtx, filter)
198202
assert.NotNil(mt, err, "expected Find error, got nil")
199-
cmdErr, ok := err.(mongo.CommandError)
200-
assert.True(mt, ok, "expected error of type %T, got %v of type %T", mongo.CommandError{}, err, err)
201-
assert.True(mt, cmdErr.HasErrorLabel("NetworkError"), "expected error %v to have 'NetworkError' label", cmdErr)
203+
assert.False(mt, mongo.IsTimeout(err), "expected non-timeout error, got %v", err)
204+
assert.True(mt, mongo.IsNetworkError(err), "expected network error, got %v", err)
202205
assert.True(mt, errors.Is(err, context.Canceled), "expected error %v to be context.Canceled", err)
203206

204207
assert.False(mt, isPoolCleared(), "expected pool to not be cleared but was")

0 commit comments

Comments
 (0)