This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
# 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]
|
||||
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 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
|
||||
|
||||
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"
|
||||
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 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
|
||||
Reference in New Issue
Block a user