Skip to content

Commit 24febe4

Browse files
authored
GODRIVER-1802 add ServerError interface (#549)
1 parent 8cceae1 commit 24febe4

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

mongo/doc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@
7070
// Additional examples can be found under the examples directory in the driver's repository and
7171
// on the MongoDB website.
7272
//
73+
// Error Handling
74+
//
75+
// Errors from the MongoDB server will implement the ServerError interface, which has functions to check for specific
76+
// error codes, labels, and message substrings. These can be used to check for and handle specific errors. Some methods,
77+
// like InsertMany and BulkWrite, can return an error representing multiple errors, and in those cases the ServerError
78+
// functions will return true if any of the contained errors satisfy the check.
79+
//
7380
// Potential DNS Issues
7481
//
7582
// Building with Go 1.11+ and using connection strings with the "mongodb+srv"[1] scheme is

mongo/errors.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"bytes"
1111
"errors"
1212
"fmt"
13+
"strings"
1314

1415
"go.mongodb.org/mongo-driver/bson"
1516
"go.mongodb.org/mongo-driver/x/mongo/driver"
@@ -112,6 +113,26 @@ func (e MongocryptdError) Unwrap() error {
112113
return e.Wrapped
113114
}
114115

116+
// ServerError is the interface implemented by errors returned from the server. Custom implementations of this
117+
// interface should not be used in production.
118+
type ServerError interface {
119+
error
120+
// HasErrorCode returns true if the error has the specified code.
121+
HasErrorCode(int) bool
122+
// HasErrorLabel returns true if the error contains the specified label.
123+
HasErrorLabel(string) bool
124+
// HasErrorMessage returns true if the error contains the specified message.
125+
HasErrorMessage(string) bool
126+
// HasErrorCodeWithMessage returns true if any of the contained errors have the specified code and message.
127+
HasErrorCodeWithMessage(int, string) bool
128+
129+
serverError()
130+
}
131+
132+
var _ ServerError = CommandError{}
133+
var _ ServerError = WriteException{}
134+
var _ ServerError = BulkWriteException{}
135+
115136
// CommandError represents a server error during execution of a command. This can be returned by any operation.
116137
type CommandError struct {
117138
Code int32
@@ -134,6 +155,11 @@ func (e CommandError) Unwrap() error {
134155
return e.Wrapped
135156
}
136157

158+
// HasErrorCode returns true if the error has the specified code.
159+
func (e CommandError) HasErrorCode(code int) bool {
160+
return int(e.Code) == code
161+
}
162+
137163
// HasErrorLabel returns true if the error contains the specified label.
138164
func (e CommandError) HasErrorLabel(label string) bool {
139165
if e.Labels != nil {
@@ -146,11 +172,24 @@ func (e CommandError) HasErrorLabel(label string) bool {
146172
return false
147173
}
148174

175+
// HasErrorMessage returns true if the error contains the specified message.
176+
func (e CommandError) HasErrorMessage(message string) bool {
177+
return strings.Contains(e.Message, message)
178+
}
179+
180+
// HasErrorCodeWithMessage returns true if the error has the specified code and Message contains the specified message.
181+
func (e CommandError) HasErrorCodeWithMessage(code int, message string) bool {
182+
return int(e.Code) == code && strings.Contains(e.Message, message)
183+
}
184+
149185
// IsMaxTimeMSExpiredError returns true if the error is a MaxTimeMSExpired error.
150186
func (e CommandError) IsMaxTimeMSExpiredError() bool {
151187
return e.Code == 50 || e.Name == "MaxTimeMSExpired"
152188
}
153189

190+
// serverError implements the ServerError interface.
191+
func (e CommandError) serverError() {}
192+
154193
// WriteError is an error that occurred during execution of a write operation. This error type is only returned as part
155194
// of a WriteException or BulkWriteException.
156195
type WriteError struct {
@@ -227,6 +266,19 @@ func (mwe WriteException) Error() string {
227266
return buf.String()
228267
}
229268

269+
// HasErrorCode returns true if the error has the specified code.
270+
func (mwe WriteException) HasErrorCode(code int) bool {
271+
if mwe.WriteConcernError != nil && mwe.WriteConcernError.Code == code {
272+
return true
273+
}
274+
for _, we := range mwe.WriteErrors {
275+
if we.Code == code {
276+
return true
277+
}
278+
}
279+
return false
280+
}
281+
230282
// HasErrorLabel returns true if the error contains the specified label.
231283
func (mwe WriteException) HasErrorLabel(label string) bool {
232284
if mwe.Labels != nil {
@@ -239,6 +291,36 @@ func (mwe WriteException) HasErrorLabel(label string) bool {
239291
return false
240292
}
241293

294+
// HasErrorMessage returns true if the error contains the specified message.
295+
func (mwe WriteException) HasErrorMessage(message string) bool {
296+
if mwe.WriteConcernError != nil && strings.Contains(mwe.WriteConcernError.Message, message) {
297+
return true
298+
}
299+
for _, we := range mwe.WriteErrors {
300+
if strings.Contains(we.Message, message) {
301+
return true
302+
}
303+
}
304+
return false
305+
}
306+
307+
// HasErrorCodeWithMessage returns true if any of the contained errors have the specified code and message.
308+
func (mwe WriteException) HasErrorCodeWithMessage(code int, message string) bool {
309+
if mwe.WriteConcernError != nil &&
310+
mwe.WriteConcernError.Code == code && strings.Contains(mwe.WriteConcernError.Message, message) {
311+
return true
312+
}
313+
for _, we := range mwe.WriteErrors {
314+
if we.Code == code && strings.Contains(we.Message, message) {
315+
return true
316+
}
317+
}
318+
return false
319+
}
320+
321+
// serverError implements the ServerError interface.
322+
func (mwe WriteException) serverError() {}
323+
242324
func convertDriverWriteConcernError(wce *driver.WriteConcernError) *WriteConcernError {
243325
if wce == nil {
244326
return nil
@@ -287,6 +369,19 @@ func (bwe BulkWriteException) Error() string {
287369
return buf.String()
288370
}
289371

372+
// HasErrorCode returns true if any of the errors have the specified code.
373+
func (bwe BulkWriteException) HasErrorCode(code int) bool {
374+
if bwe.WriteConcernError != nil && bwe.WriteConcernError.Code == code {
375+
return true
376+
}
377+
for _, we := range bwe.WriteErrors {
378+
if we.Code == code {
379+
return true
380+
}
381+
}
382+
return false
383+
}
384+
290385
// HasErrorLabel returns true if the error contains the specified label.
291386
func (bwe BulkWriteException) HasErrorLabel(label string) bool {
292387
if bwe.Labels != nil {
@@ -299,6 +394,36 @@ func (bwe BulkWriteException) HasErrorLabel(label string) bool {
299394
return false
300395
}
301396

397+
// HasErrorMessage returns true if the error contains the specified message.
398+
func (bwe BulkWriteException) HasErrorMessage(message string) bool {
399+
if bwe.WriteConcernError != nil && strings.Contains(bwe.WriteConcernError.Message, message) {
400+
return true
401+
}
402+
for _, we := range bwe.WriteErrors {
403+
if strings.Contains(we.Message, message) {
404+
return true
405+
}
406+
}
407+
return false
408+
}
409+
410+
// HasErrorCodeWithMessage returns true if any of the contained errors have the specified code and message.
411+
func (bwe BulkWriteException) HasErrorCodeWithMessage(code int, message string) bool {
412+
if bwe.WriteConcernError != nil &&
413+
bwe.WriteConcernError.Code == code && strings.Contains(bwe.WriteConcernError.Message, message) {
414+
return true
415+
}
416+
for _, we := range bwe.WriteErrors {
417+
if we.Code == code && strings.Contains(we.Message, message) {
418+
return true
419+
}
420+
}
421+
return false
422+
}
423+
424+
// serverError implements the ServerError interface.
425+
func (bwe BulkWriteException) serverError() {}
426+
302427
// returnResult is used to determine if a function calling processWriteError should return
303428
// the result or return nil. Since the processWriteError function is used by many different
304429
// methods, both *One and *Many, we need a way to differentiate if the method should return

mongo/integration/errors_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,186 @@ func TestErrors(t *testing.T) {
107107
assert.True(mt, netErr.Timeout(), "expected error %v to be a network timeout", err)
108108
})
109109
})
110+
mt.Run("ServerError", func(mt *mtest.T) {
111+
matchWrapped := errors.New("wrapped err")
112+
otherWrapped := errors.New("other err")
113+
const matchCode = 100
114+
const otherCode = 120
115+
const label = "testError"
116+
testCases := []struct {
117+
name string
118+
err mongo.ServerError
119+
hasCode bool
120+
hasLabel bool
121+
hasMessage bool
122+
hasCodeWithMessage bool
123+
isResult bool
124+
}{
125+
{
126+
"CommandError all true",
127+
mongo.CommandError{matchCode, "foo", []string{label}, "name", matchWrapped},
128+
true,
129+
true,
130+
true,
131+
true,
132+
true,
133+
},
134+
{
135+
"CommandError all false",
136+
mongo.CommandError{otherCode, "bar", []string{"otherError"}, "name", otherWrapped},
137+
false,
138+
false,
139+
false,
140+
false,
141+
false,
142+
},
143+
{
144+
"CommandError has code not message",
145+
mongo.CommandError{matchCode, "bar", []string{}, "name", nil},
146+
true,
147+
false,
148+
false,
149+
false,
150+
false,
151+
},
152+
{
153+
"WriteException all in writeConcernError",
154+
mongo.WriteException{
155+
&mongo.WriteConcernError{"name", matchCode, "foo", nil},
156+
nil,
157+
[]string{label},
158+
},
159+
true,
160+
true,
161+
true,
162+
true,
163+
false,
164+
},
165+
{
166+
"WriteException all in writeError",
167+
mongo.WriteException{
168+
nil,
169+
mongo.WriteErrors{
170+
mongo.WriteError{0, otherCode, "bar"},
171+
mongo.WriteError{0, matchCode, "foo"},
172+
},
173+
[]string{"otherError"},
174+
},
175+
true,
176+
false,
177+
true,
178+
true,
179+
false,
180+
},
181+
{
182+
"WriteException all false",
183+
mongo.WriteException{
184+
&mongo.WriteConcernError{"name", otherCode, "bar", nil},
185+
mongo.WriteErrors{
186+
mongo.WriteError{0, otherCode, "baz"},
187+
},
188+
[]string{"otherError"},
189+
},
190+
false,
191+
false,
192+
false,
193+
false,
194+
false,
195+
},
196+
{
197+
"WriteException HasErrorCodeAndMessage false",
198+
mongo.WriteException{
199+
&mongo.WriteConcernError{"name", matchCode, "bar", nil},
200+
mongo.WriteErrors{
201+
mongo.WriteError{0, otherCode, "foo"},
202+
},
203+
[]string{"otherError"},
204+
},
205+
true,
206+
false,
207+
true,
208+
false,
209+
false,
210+
},
211+
{
212+
"BulkWriteException all in writeConcernError",
213+
mongo.BulkWriteException{
214+
&mongo.WriteConcernError{"name", matchCode, "foo", nil},
215+
nil,
216+
[]string{label},
217+
},
218+
true,
219+
true,
220+
true,
221+
true,
222+
false,
223+
},
224+
{
225+
"BulkWriteException all in writeError",
226+
mongo.BulkWriteException{
227+
nil,
228+
[]mongo.BulkWriteError{
229+
{mongo.WriteError{0, matchCode, "foo"}, &mongo.InsertOneModel{}},
230+
{mongo.WriteError{0, otherCode, "bar"}, &mongo.InsertOneModel{}},
231+
},
232+
[]string{"otherError"},
233+
},
234+
true,
235+
false,
236+
true,
237+
true,
238+
false,
239+
},
240+
{
241+
"BulkWriteException all false",
242+
mongo.BulkWriteException{
243+
&mongo.WriteConcernError{"name", otherCode, "bar", nil},
244+
[]mongo.BulkWriteError{
245+
{mongo.WriteError{0, otherCode, "baz"}, &mongo.InsertOneModel{}},
246+
},
247+
[]string{"otherError"},
248+
},
249+
false,
250+
false,
251+
false,
252+
false,
253+
false,
254+
},
255+
{
256+
"BulkWriteException HasErrorCodeAndMessage false",
257+
mongo.BulkWriteException{
258+
&mongo.WriteConcernError{"name", matchCode, "bar", nil},
259+
[]mongo.BulkWriteError{
260+
{mongo.WriteError{0, otherCode, "foo"}, &mongo.InsertOneModel{}},
261+
},
262+
[]string{"otherError"},
263+
},
264+
true,
265+
false,
266+
true,
267+
false,
268+
false,
269+
},
270+
}
271+
for _, tc := range testCases {
272+
mt.Run(tc.name, func(mt *mtest.T) {
273+
has := tc.err.HasErrorCode(matchCode)
274+
assert.Equal(mt, has, tc.hasCode, "expected HasErrorCode to return %v, got %v", tc.hasCode, has)
275+
has = tc.err.HasErrorLabel(label)
276+
assert.Equal(mt, has, tc.hasLabel, "expected HasErrorLabel to return %v, got %v", tc.hasLabel, has)
277+
278+
// Check for full message and substring
279+
has = tc.err.HasErrorMessage("foo")
280+
assert.Equal(mt, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has)
281+
has = tc.err.HasErrorMessage("fo")
282+
assert.Equal(mt, has, tc.hasMessage, "expected HasErrorMessage to return %v, got %v", tc.hasMessage, has)
283+
has = tc.err.HasErrorCodeWithMessage(matchCode, "foo")
284+
assert.Equal(mt, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has)
285+
has = tc.err.HasErrorCodeWithMessage(matchCode, "fo")
286+
assert.Equal(mt, has, tc.hasCodeWithMessage, "expected HasErrorCodeWithMessage to return %v, got %v", tc.hasCodeWithMessage, has)
287+
288+
assert.Equal(mt, errors.Is(tc.err, matchWrapped), tc.isResult, "expected errors.Is result to be %v", tc.isResult)
289+
})
290+
}
291+
})
110292
}

0 commit comments

Comments
 (0)