/
error.rb
170 lines (141 loc) · 5.25 KB
/
error.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# frozen_string_literal: true
module Gitlab
module Error
# Custom error class for rescuing from all Gitlab errors.
class Error < StandardError; end
# Raised when API endpoint credentials not configured.
class MissingCredentials < Error; end
# Raised when impossible to parse response body.
class Parsing < Error; end
# Custom error class for rescuing from HTTP response errors.
class ResponseError < Error
POSSIBLE_MESSAGE_KEYS = %i[message error_description error].freeze
def initialize(response)
@response = response
super(build_error_message)
end
# Status code returned in the HTTP response.
#
# @return [Integer]
def response_status
@response.code
end
# Body content returned in the HTTP response
#
# @return [String]
def response_message
@response.parsed_response.message
end
# Additional error context returned by some API endpoints
#
# @return [String]
def error_code
if @response.respond_to?(:error_code)
@response.error_code
else
''
end
end
private
# Human friendly message.
#
# @return [String]
def build_error_message
parsed_response = classified_response
message = check_error_keys(parsed_response)
"Server responded with code #{@response.code}, message: " \
"#{handle_message(message)}. " \
"Request URI: #{@response.request.base_uri}#{@response.request.path}"
end
# Error keys vary across the API, find the first key that the parsed_response
# object responds to and return that, otherwise return the original.
def check_error_keys(resp)
key = POSSIBLE_MESSAGE_KEYS.find { |k| resp.respond_to?(k) }
key ? resp.send(key) : resp
end
# Parse the body based on the classification of the body content type
#
# @return parsed response
def classified_response
if @response.respond_to?('headers')
@response.headers['content-type'] == 'text/plain' ? { message: @response.to_s } : @response.parsed_response
else
@response.parsed_response
end
rescue Gitlab::Error::Parsing
# Return stringified response when receiving a
# parsing error to avoid obfuscation of the
# api error.
#
# note: The Gitlab API does not always return valid
# JSON when there are errors.
@response.to_s
end
# Handle error response message in case of nested hashes
def handle_message(message)
case message
when Gitlab::ObjectifiedHash
message.to_h.sort.map do |key, val|
"'#{key}' #{(val.is_a?(Hash) ? val.sort.map { |k, v| "(#{k}: #{v.join(' ')})" } : [val].flatten).join(' ')}"
end.join(', ')
when Array
message.join(' ')
else
message
end
end
end
# Raised when API endpoint returns the HTTP status code 400.
class BadRequest < ResponseError; end
# Raised when API endpoint returns the HTTP status code 401.
class Unauthorized < ResponseError; end
# Raised when API endpoint returns the HTTP status code 403.
class Forbidden < ResponseError; end
# Raised when API endpoint returns the HTTP status code 404.
class NotFound < ResponseError; end
# Raised when API endpoint returns the HTTP status code 405.
class MethodNotAllowed < ResponseError; end
# Raised when API endpoint returns the HTTP status code 406.
class NotAcceptable < ResponseError; end
# Raised when API endpoint returns the HTTP status code 409.
class Conflict < ResponseError; end
# Raised when API endpoint returns the HTTP status code 422.
class Unprocessable < ResponseError; end
# Raised when API endpoint returns the HTTP status code 429.
class TooManyRequests < ResponseError; end
# Raised when API endpoint returns the HTTP status code 500.
class InternalServerError < ResponseError; end
# Raised when API endpoint returns the HTTP status code 502.
class BadGateway < ResponseError; end
# Raised when API endpoint returns the HTTP status code 503.
class ServiceUnavailable < ResponseError; end
# Raised when API endpoint returns the HTTP status code 522.
class ConnectionTimedOut < ResponseError; end
# HTTP status codes mapped to error classes.
STATUS_MAPPINGS = {
400 => BadRequest,
401 => Unauthorized,
403 => Forbidden,
404 => NotFound,
405 => MethodNotAllowed,
406 => NotAcceptable,
409 => Conflict,
422 => Unprocessable,
429 => TooManyRequests,
500 => InternalServerError,
502 => BadGateway,
503 => ServiceUnavailable,
522 => ConnectionTimedOut
}.freeze
# Returns error class that should be raised for this response. Returns nil
# if the response status code is not 4xx or 5xx.
#
# @param [HTTParty::Response] response The response object.
# @return [Class<Error::ResponseError>, nil]
def self.klass(response)
error_klass = STATUS_MAPPINGS[response.code]
return error_klass if error_klass
ResponseError if response.server_error? || response.client_error?
end
end
end