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 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
|
# frozen_string_literal: true
module Octokit
# Custom error class for rescuing from all GitHub errors
class Error < StandardError
attr_reader :context
# Returns the appropriate Octokit::Error subclass based
# on status and response message
#
# @param [Hash] response HTTP response
# @return [Octokit::Error]
# rubocop:disable Metrics/CyclomaticComplexity
def self.from_response(response)
status = response[:status].to_i
body = response[:body].to_s
headers = response[:response_headers]
if klass = case status
when 400 then Octokit::BadRequest
when 401 then error_for_401(headers)
when 403 then error_for_403(body)
when 404 then error_for_404(body)
when 405 then Octokit::MethodNotAllowed
when 406 then Octokit::NotAcceptable
when 409 then Octokit::Conflict
when 410 then Octokit::Deprecated
when 415 then Octokit::UnsupportedMediaType
when 422 then error_for_422(body)
when 451 then Octokit::UnavailableForLegalReasons
when 400..499 then Octokit::ClientError
when 500 then Octokit::InternalServerError
when 501 then Octokit::NotImplemented
when 502 then Octokit::BadGateway
when 503 then Octokit::ServiceUnavailable
when 500..599 then Octokit::ServerError
end
klass.new(response)
end
end
# rubocop:enable Metrics/CyclomaticComplexity
def build_error_context
if RATE_LIMITED_ERRORS.include?(self.class)
@context = Octokit::RateLimit.from_response(@response)
end
end
def initialize(response = nil)
@response = response
super(build_error_message)
build_error_context
end
# Documentation URL returned by the API for some errors
#
# @return [String]
def documentation_url
data[:documentation_url] if data.is_a? Hash
end
# Returns most appropriate error for 401 HTTP status code
# @private
# rubocop:disable Naming/VariableNumber
def self.error_for_401(headers)
# rubocop:enbale Naming/VariableNumber
if Octokit::OneTimePasswordRequired.required_header(headers)
Octokit::OneTimePasswordRequired
else
Octokit::Unauthorized
end
end
# Returns most appropriate error for 403 HTTP status code
# @private
def self.error_for_403(body)
# rubocop:enable Naming/VariableNumber
case body
when /rate limit exceeded/i, /exceeded a secondary rate limit/i
Octokit::TooManyRequests
when /login attempts exceeded/i
Octokit::TooManyLoginAttempts
when /(returns|for) blobs (up to|between) [0-9-]+ MB/i
Octokit::TooLargeContent
when /abuse/i
Octokit::AbuseDetected
when /repository access blocked/i
Octokit::RepositoryUnavailable
when /email address must be verified/i
Octokit::UnverifiedEmail
when /account was suspended/i
Octokit::AccountSuspended
when /billing issue/i
Octokit::BillingIssue
when /Resource protected by organization SAML enforcement/i
Octokit::SAMLProtected
when /suspended your access|This installation has been suspended/i
Octokit::InstallationSuspended
else
Octokit::Forbidden
end
end
# Return most appropriate error for 404 HTTP status code
# @private
# rubocop:disable Naming/VariableNumber
def self.error_for_404(body)
# rubocop:enable Naming/VariableNumber
if body =~ /Branch not protected/i
Octokit::BranchNotProtected
else
Octokit::NotFound
end
end
# Return most appropriate error for 422 HTTP status code
# @private
# rubocop:disable Naming/VariableNumber
def self.error_for_422(body)
# rubocop:enable Naming/VariableNumber
if body =~ /PullRequestReviewComment/i && body =~ /(commit_id|end_commit_oid) is not part of the pull request/i
Octokit::CommitIsNotPartOfPullRequest
elsif body =~ /Path diff too large/i
Octokit::PathDiffTooLarge
else
Octokit::UnprocessableEntity
end
end
# Array of validation errors
# @return [Array<Hash>] Error info
def errors
if data.is_a?(Hash)
data[:errors] || []
else
[]
end
end
# Status code returned by the GitHub server.
#
# @return [Integer]
def response_status
@response[:status]
end
# Headers returned by the GitHub server.
#
# @return [Hash]
def response_headers
@response[:response_headers]
end
# Body returned by the GitHub server.
#
# @return [String]
def response_body
@response[:body]
end
private
def data
@data ||=
if (body = @response[:body]) && !body.empty?
if body.is_a?(String) &&
@response[:response_headers] &&
@response[:response_headers][:content_type] =~ /json/
Sawyer::Agent.serializer.decode(body)
else
body
end
end
end
def response_message
case data
when Hash
data[:message]
when String
data
end
end
def response_error
"Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
end
def response_error_summary
return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
summary = +"\nError summary:\n"
return summary << data[:errors] if data[:errors].is_a?(String)
summary << data[:errors].map do |error|
if error.is_a? Hash
error.map { |k, v| " #{k}: #{v}" }
else
" #{error}"
end
end.join("\n")
summary
end
def build_error_message
return nil if @response.nil?
message = +"#{@response[:method].to_s.upcase} "
message << "#{redact_url(@response[:url].to_s.dup)}: "
message << "#{@response[:status]} - "
message << response_message.to_s unless response_message.nil?
message << response_error.to_s unless response_error.nil?
message << response_error_summary.to_s unless response_error_summary.nil?
message << " // See: #{documentation_url}" unless documentation_url.nil?
message
end
def redact_url(url_string)
%w[client_secret access_token api_key].each do |token|
if url_string.include? token
url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)")
end
end
url_string
end
end
# Raised on errors in the 400-499 range
class ClientError < Error; end
# Raised when GitHub returns a 400 HTTP status code
class BadRequest < ClientError; end
# Raised when GitHub returns a 401 HTTP status code
class Unauthorized < ClientError; end
# Raised when GitHub returns a 401 HTTP status code
# and headers include "X-GitHub-OTP"
class OneTimePasswordRequired < ClientError
# @private
OTP_DELIVERY_PATTERN = /required; (\w+)/i.freeze
# @private
def self.required_header(headers)
OTP_DELIVERY_PATTERN.match headers['X-GitHub-OTP'].to_s
end
# Delivery method for the user's OTP
#
# @return [String]
def password_delivery
@password_delivery ||= delivery_method_from_header
end
private
def delivery_method_from_header
if match = self.class.required_header(@response[:response_headers])
match[1]
end
end
end
# Raised when GitHub returns a 403 HTTP status code
class Forbidden < ClientError; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'rate limit exceeded'
class TooManyRequests < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'login attempts exceeded'
class TooManyLoginAttempts < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'returns blobs up to [0-9]+ MB'
class TooLargeContent < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'abuse'
class AbuseDetected < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'repository access blocked'
class RepositoryUnavailable < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'email address must be verified'
class UnverifiedEmail < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'account was suspended'
class AccountSuspended < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'billing issue'
class BillingIssue < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'Resource protected by organization SAML enforcement'
class SAMLProtected < Forbidden; end
# Raised when GitHub returns a 403 HTTP status code
# and body matches 'suspended your access'
class InstallationSuspended < Forbidden; end
# Raised when GitHub returns a 404 HTTP status code
class NotFound < ClientError; end
# Raised when GitHub returns a 404 HTTP status code
# and body matches 'Branch not protected'
class BranchNotProtected < ClientError; end
# Raised when GitHub returns a 405 HTTP status code
class MethodNotAllowed < ClientError; end
# Raised when GitHub returns a 406 HTTP status code
class NotAcceptable < ClientError; end
# Raised when GitHub returns a 409 HTTP status code
class Conflict < ClientError; end
# Raised when GHES Manage return a 410 HTTP status code
class Deprecated < ClientError; end
# Raised when GitHub returns a 414 HTTP status code
class UnsupportedMediaType < ClientError; end
# Raised when GitHub returns a 422 HTTP status code
class UnprocessableEntity < ClientError; end
# Raised when GitHub returns a 422 HTTP status code
# and body matches 'PullRequestReviewComment' and 'commit_id (or end_commit_oid) is not part of the pull request'
class CommitIsNotPartOfPullRequest < UnprocessableEntity; end
# Raised when GitHub returns a 422 HTTP status code and body matches 'Path diff too large'.
# It could occur when attempting to post review comments on a "too large" file.
class PathDiffTooLarge < UnprocessableEntity; end
# Raised when GitHub returns a 451 HTTP status code
class UnavailableForLegalReasons < ClientError; end
# Raised on errors in the 500-599 range
class ServerError < Error; end
# Raised when GitHub returns a 500 HTTP status code
class InternalServerError < ServerError; end
# Raised when GitHub returns a 501 HTTP status code
class NotImplemented < ServerError; end
# Raised when GitHub returns a 502 HTTP status code
class BadGateway < ServerError; end
# Raised when GitHub returns a 503 HTTP status code
class ServiceUnavailable < ServerError; end
# Raised when client fails to provide valid Content-Type
class MissingContentType < ArgumentError; end
# Raised when a method requires an application client_id
# and secret but none is provided
class ApplicationCredentialsRequired < StandardError; end
# Raised when a repository is created with an invalid format
class InvalidRepository < ArgumentError; end
RATE_LIMITED_ERRORS = [Octokit::TooManyRequests, Octokit::AbuseDetected].freeze
end
|