Skip to content

Commit 14e3aeb

Browse files
p-mongop
andauthored
RUBY-2509 Expose server error code name in OperationFailure exception message
(#2189) * RUBY-2509 Expose server error code name in OperationFailure exception message * fix up the api docs Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent 95de4ad commit 14e3aeb

18 files changed

+301
-44
lines changed

lib/mongo/auth/base.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,14 @@ def dispatch_msg(connection, conversation, msg)
118118
# raises Unauthorized if not.
119119
def validate_reply!(connection, conversation, doc)
120120
if doc[:ok] != 1
121-
extra = [doc[:code], doc[:codeName]].compact.join(': ')
122-
msg = doc[:errmsg]
123-
unless extra.empty?
124-
msg += " (#{extra})"
125-
end
121+
message = Error::Parser.build_message(
122+
code: doc[:code],
123+
code_name: doc[:codeName],
124+
message: doc[:errmsg],
125+
)
126126
raise Unauthorized.new(user,
127127
used_mechanism: self.class.const_get(:MECHANISM),
128-
message: msg,
128+
message: message,
129129
server: connection.server,
130130
)
131131
end

lib/mongo/error.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,19 @@ class Error < StandardError
2929
# An error field, MongoDB < 2.6
3030
#
3131
# @since 2.0.0
32+
# @deprecated
3233
ERR = '$err'.freeze
3334

3435
# An error field, MongoDB < 2.4
3536
#
3637
# @since 2.0.0
38+
# @deprecated
3739
ERROR = 'err'.freeze
3840

3941
# The standard error message field, MongoDB 3.0+
4042
#
4143
# @since 2.0.0
44+
# @deprecated
4245
ERRMSG = 'errmsg'.freeze
4346

4447
# The constant for the writeErrors array.

lib/mongo/error/bulk_write_error.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,19 @@
1515
module Mongo
1616
class Error
1717

18-
# Exception raised if there are write errors upon executing the bulk
18+
# Exception raised if there are write errors upon executing a bulk
1919
# operation.
2020
#
21+
# Unlike OperationFailure, BulkWriteError does not currently expose
22+
# individual error components (such as the error code). The result document
23+
# (which can be obtained using the +result+ attribute) provides detailed
24+
# error information and can be examined by the application if desired.
25+
#
26+
# @note A bulk operation that resulted in a BulkWriteError may have
27+
# written some of the documents to the database. If the bulk write
28+
# was unordered, writes may have also continued past the write that
29+
# produced a BulkWriteError.
30+
#
2131
# @since 2.0.0
2232
class BulkWriteError < Error
2333

@@ -47,11 +57,15 @@ def build_message
4757
return nil unless errors
4858

4959
fragment = errors.first(10).map do |error|
50-
"#{error['errmsg']} (#{error['code']})"
51-
end.join(', ')
60+
"[#{error['code']}]: #{error['errmsg']}"
61+
end.join('; ')
5262

5363
fragment += '...' if errors.length > 10
5464

65+
if errors.length > 1
66+
fragment = "Multiple errors: #{fragment}"
67+
end
68+
5569
fragment
5670
end
5771
end

lib/mongo/error/operation_failure.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ class OperationFailure < Error
8989
# @since 2.6.0
9090
attr_reader :code_name
9191

92+
# @return [ String ] The server-returned error message
93+
# parsed from the response.
94+
#
95+
# @api experimental
96+
attr_reader :server_message
97+
9298
# Whether the error is a retryable error according to the legacy
9399
# read retry logic.
94100
#
@@ -215,6 +221,11 @@ def write_concern_error?
215221
# @since 2.10.0
216222
attr_reader :write_concern_error_code_name
217223

224+
# @return [ BSON::Document | nil ] The server-returned error document.
225+
#
226+
# @api experimental
227+
attr_reader :document
228+
218229
# Create the operation failure.
219230
#
220231
# @example Create the error object
@@ -229,6 +240,10 @@ def write_concern_error?
229240
#
230241
# @option options [ Integer ] :code Error code.
231242
# @option options [ String ] :code_name Error code name.
243+
# @option options [ BSON::Document ] :document The server-returned
244+
# error document.
245+
# @option options [ String ] server_message The server-returned
246+
# error message parsed from the response.
232247
# @option options [ Hash ] :write_concern_error_document The
233248
# server-supplied write concern error document, if any.
234249
# @option options [ Integer ] :write_concern_error_code Error code for
@@ -253,6 +268,8 @@ def initialize(message = nil, result = nil, options = {})
253268
@write_concern_error_labels = options[:write_concern_error_labels] || []
254269
@labels = options[:labels] || []
255270
@wtimeout = !!options[:wtimeout]
271+
@document = options[:document]
272+
@server_message = options[:server_message]
256273
end
257274

258275
# Whether the error is a write concern timeout.
@@ -281,8 +298,10 @@ def max_time_ms_expired?
281298
#
282299
# @since 2.10.0
283300
def unsupported_retryable_write?
284-
# code 20 is IllegalOperation
285-
code == 20 && message.start_with?("Transaction numbers")
301+
# code 20 is IllegalOperation.
302+
# Note that the document is expected to be a BSON::Document, thus
303+
# either having string keys or providing indifferent access.
304+
code == 20 && server_message&.start_with?("Transaction numbers") || false
286305
end
287306
end
288307
end

lib/mongo/error/parser.rb

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,53 @@ class Error
4242
# Class for parsing the various forms that errors can come in from MongoDB
4343
# command responses.
4444
#
45+
# The errors can be reported by the server in a number of ways:
46+
# - {ok:0} response indicates failure. In newer servers, code, codeName
47+
# and errmsg fields should be set. In older servers some may not be set.
48+
# - {ok:1} response with a write concern error (writeConcernError top-level
49+
# field). This indicates that the node responding successfully executed
50+
# the request, but not enough other nodes successfully executed the
51+
# request to satisfy the write concern.
52+
# - {ok:1} response with writeErrors top-level field. This can be obtained
53+
# in a bulk write but also in a non-bulk write. In a non-bulk write
54+
# there should be exactly one error in the writeErrors list.
55+
# The case of multiple errors is handled by BulkWrite::Result.
56+
# - {ok:1} response with writeConcernErrors top-level field. This can
57+
# only be obtained in a bulk write and is handled by BulkWrite::Result,
58+
# not by this class.
59+
#
60+
# Note that writeErrors do not have codeName fields - they just provide
61+
# codes and messages. writeConcernErrors may similarly not provide code
62+
# names.
63+
#
4564
# @since 2.0.0
65+
# @api private
4666
class Parser
4767
include SdamErrorDetection
4868

49-
# @return [ BSON::Document ] document The returned document.
69+
# @return [ BSON::Document ] The returned document.
5070
attr_reader :document
5171

52-
# @return [ String ] message The error message parsed from the document.
72+
# @return [ String ] The full error message to be used in the
73+
# raised exception.
5374
attr_reader :message
5475

55-
# @return [ Array<Protocol::Message> ] replies The message replies.
76+
# @return [ String ] The server-returned error message
77+
# parsed from the response.
78+
attr_reader :server_message
79+
80+
# @return [ Array<Protocol::Message> ] The message replies.
5681
attr_reader :replies
5782

58-
# @return [ Integer ] code The error code parsed from the document.
83+
# @return [ Integer ] The error code parsed from the document.
5984
# @since 2.6.0
6085
attr_reader :code
6186

62-
# @return [ String ] code_name The error code name parsed from the document.
87+
# @return [ String ] The error code name parsed from the document.
6388
# @since 2.6.0
6489
attr_reader :code_name
6590

66-
# @return [ Array<String> ] labels The set of labels associated with the error.
91+
# @return [ Array<String> ] The set of labels associated with the error.
6792
# @since 2.7.0
6893
attr_reader :labels
6994

@@ -145,33 +170,61 @@ def write_concern_error_labels
145170
write_concern_error_document && write_concern_error_document['errorLabels']
146171
end
147172

173+
class << self
174+
def build_message(code: nil, code_name: nil, message: nil)
175+
if code_name && code
176+
"[#{code}:#{code_name}]: #{message}"
177+
elsif code_name
178+
# This surely should never happen, if there's a code name
179+
# there ought to also be the code provided.
180+
# Handle this case for completeness.
181+
"[#{code_name}]: #{message}"
182+
elsif code
183+
"[#{code}]: #{message}"
184+
else
185+
message
186+
end
187+
end
188+
end
189+
148190
private
149191

150192
def parse!
193+
if document['ok'] != 1 && document['writeErrors']
194+
raise ArgumentError, "writeErrors should only be given in successful responses"
195+
end
196+
151197
@message = ""
152-
parse_single(@message, ERR)
153-
parse_single(@message, ERROR)
154-
parse_single(@message, ERRMSG)
198+
parse_single(@message, '$err')
199+
parse_single(@message, 'err')
200+
parse_single(@message, 'errmsg')
155201
parse_multiple(@message, 'writeErrors')
156202
if write_concern_error_document
157-
parse_single(@message, ERRMSG, write_concern_error_document)
203+
parse_single(@message, 'errmsg', write_concern_error_document)
158204
end
159205
parse_flag(@message)
160206
parse_code
161207
parse_labels
162208
parse_wtimeout
209+
210+
@server_message = @message
211+
@message = self.class.build_message(
212+
code: code,
213+
code_name: code_name,
214+
message: @message,
215+
)
163216
end
164217

165218
def parse_single(message, key, doc = document)
166219
if error = doc[key]
167-
append(message ,"#{error} (#{doc[CODE]})")
220+
append(message, error)
168221
end
169222
end
170223

171224
def parse_multiple(message, key)
172225
if errors = document[key]
173226
errors.each do |error|
174-
parse_single(message, ERRMSG, error)
227+
parse_single(message, 'errmsg', error)
175228
end
176229
end
177230
end

lib/mongo/operation/list_collections/result.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ def validate!
8888
code: parser.code,
8989
code_name: parser.code_name,
9090
labels: parser.labels,
91-
wtimeout: parser.wtimeout)
91+
wtimeout: parser.wtimeout,
92+
document: parser.document,
93+
server_message: parser.server_message,
94+
)
9295
end
9396
end
9497

lib/mongo/operation/result.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ def error
332332
labels: parser.labels,
333333
wtimeout: parser.wtimeout,
334334
connection_description: connection_description,
335+
document: parser.document,
336+
server_message: parser.server_message,
335337
)
336338
end
337339

lib/mongo/retryable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ def log_retry(e, options = nil)
477477
else
478478
"Retry"
479479
end
480-
Logger.logger.warn "#{message} due to: #{e.class.name} #{e.message}"
480+
Logger.logger.warn "#{message} due to: #{e.class.name}: #{e.message}"
481481
end
482482

483483
# Retry writes on MMAPv1 should raise an actionable error; append actionable
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'spec_helper'
2+
3+
describe 'BulkWriteError message' do
4+
let(:client) { authorized_client }
5+
let(:collection_name) { 'bulk_write_error_message_spec' }
6+
let(:collection) { client[collection_name] }
7+
8+
before do
9+
collection.delete_many
10+
end
11+
12+
context 'a bulk write with one error' do
13+
it 'reports code name, code and message' do
14+
begin
15+
collection.insert_many([
16+
{_id: 1},
17+
{_id: 1},
18+
{_id: 1},
19+
], ordered: true)
20+
fail('Should have raised')
21+
rescue Mongo::Error::BulkWriteError => e
22+
e.message.should =~ %r,\A\[11000\]: (insertDocument :: caused by :: 11000 )?E11000 duplicate key error (collection|index):,
23+
end
24+
end
25+
end
26+
27+
context 'a bulk write with multiple errors' do
28+
it 'reports code name, code and message' do
29+
begin
30+
collection.insert_many([
31+
{_id: 1},
32+
{_id: 1},
33+
{_id: 1},
34+
], ordered: false)
35+
fail('Should have raised')
36+
rescue Mongo::Error::BulkWriteError => e
37+
e.message.should =~ %r,\AMultiple errors: \[11000\]: (insertDocument :: caused by :: 11000 )?E11000 duplicate key error (collection|index):.*\[11000\]: (insertDocument :: caused by :: 11000 )?E11000 duplicate key error (collection|index):,
38+
end
39+
end
40+
end
41+
end

spec/integration/change_stream_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def clear_fail_point_before
9898
it 'watch raises error' do
9999
expect do
100100
client['change-stream'].watch
101-
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(10107\)/)
101+
end.to raise_error(Mongo::Error::OperationFailure, /10107\b.*Failing command due to 'failCommand' failpoint/)
102102
end
103103
end
104104

@@ -283,7 +283,7 @@ def clear_fail_point_before
283283

284284
expect do
285285
enum.next
286-
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(101\)/)
286+
end.to raise_error(Mongo::Error::OperationFailure, /101\b.*Failing command due to 'failCommand' failpoint/)
287287
end
288288
end
289289
end
@@ -414,7 +414,7 @@ def clear_fail_point_before
414414

415415
expect do
416416
enum.try_next
417-
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(10107\)/)
417+
end.to raise_error(Mongo::Error::OperationFailure, /10107\b.*Failing command due to 'failCommand' failpoint/)
418418
end
419419
end
420420

@@ -441,7 +441,7 @@ def clear_fail_point_before
441441

442442
expect do
443443
enum.try_next
444-
end.to raise_error(Mongo::Error::OperationFailure, /Failing command due to 'failCommand' failpoint \(10107\)/)
444+
end.to raise_error(Mongo::Error::OperationFailure, /10107\b.*Failing command due to 'failCommand' failpoint/)
445445
end
446446
end
447447
end

0 commit comments

Comments
 (0)