File: error.rb

package info (click to toggle)
ruby-octokit 10.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 24,092 kB
  • sloc: ruby: 13,339; sh: 99; makefile: 7; javascript: 3
file content (371 lines) | stat: -rw-r--r-- 11,617 bytes parent folder | download
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