gapic-common 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,581 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Google LLC
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "gapic/common/error"
18
+ require "gapic/rest/error"
19
+
20
+ module Gapic
21
+ module Rest
22
+ module ResumableUpload
23
+ ##
24
+ # @private
25
+ # HTTP status code to reason phrase mapping.
26
+ #
27
+ # Includes `200` because a well-formed success can still be a protocol failure: an
28
+ # `X-Goog-Upload-Status` that does not match the phase of the request in flight is reported as a
29
+ # {BadResponseError} carrying the 200 it arrived with.
30
+ #
31
+ # @return [Hash<Integer, String>]
32
+ HTTP_STATUS_PHRASES = {
33
+ 200 => "OK",
34
+ 400 => "Bad Request",
35
+ 401 => "Unauthorized",
36
+ 403 => "Forbidden",
37
+ 404 => "Not Found",
38
+ 405 => "Method Not Allowed",
39
+ 408 => "Request Timeout",
40
+ 409 => "Conflict",
41
+ 410 => "Gone",
42
+ 411 => "Length Required",
43
+ 412 => "Precondition Failed",
44
+ 413 => "Payload Too Large",
45
+ 415 => "Unsupported Media Type",
46
+ 416 => "Range Not Satisfiable",
47
+ 429 => "Too Many Requests",
48
+ 499 => "Client Closed Request",
49
+ 500 => "Internal Server Error",
50
+ 502 => "Bad Gateway",
51
+ 503 => "Service Unavailable",
52
+ 504 => "Gateway Timeout"
53
+ }.freeze
54
+
55
+ ##
56
+ # @private
57
+ # Internal formatting helper for terminal error message and attribute extraction.
58
+ #
59
+ module ErrorBuilder
60
+ class << self
61
+ ##
62
+ # @private
63
+ # Formats status representation.
64
+ #
65
+ # @param status [Object, nil] Status value
66
+ # @return [String, nil]
67
+ def format_status status
68
+ return nil if status.nil? || status.to_s.empty?
69
+
70
+ status.to_s
71
+ end
72
+
73
+ ##
74
+ # @private
75
+ # Strips REST error prefix from message string.
76
+ #
77
+ # @param raw_message [String, nil] Raw error message
78
+ # @return [String, nil]
79
+ def clean_message raw_message
80
+ return nil if raw_message.nil? || raw_message.empty?
81
+
82
+ prefix = Gapic::Rest::Error::REST_ERROR_PREFIX
83
+ msg = raw_message.to_s
84
+ msg = msg.sub(/\A#{Regexp.escape prefix}:\s*/, "") if msg.start_with? prefix
85
+ msg = msg.sub(/\A:\s*/, "").strip
86
+ msg.empty? ? nil : msg
87
+ end
88
+
89
+ ##
90
+ # @private
91
+ # Builds error attributes tuple from an HTTP event or wrapped error.
92
+ #
93
+ # @param event [Object] HTTP response event or failure event
94
+ # @param prefix [String] Error message prefix
95
+ # @return [Array] Tuple of [message, status_code, status, details, headers]
96
+ def build_attributes event, prefix: "Resumable upload failed"
97
+ if event.respond_to?(:error) && event.error
98
+ build_from_wrapped_error event, prefix: prefix
99
+ else
100
+ build_from_http_event event, prefix: prefix
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ ##
107
+ # @private
108
+ # Builds error attributes when a wrapped REST error is available.
109
+ #
110
+ # @param event [Object] HTTP response event containing wrapped error
111
+ # @param prefix [String] Error message prefix
112
+ # @return [Array] Tuple of [message, status_code, status, details, headers]
113
+ def build_from_wrapped_error event, prefix:
114
+ err = event.error
115
+ status_code = err.status_code || (event.respond_to?(:status) ? event.status : nil)
116
+ status = err.status
117
+ status_name = format_status(status) || HTTP_STATUS_PHRASES[status_code]
118
+ status_part = status_name ? " #{status_name}" : ""
119
+ inner_msg = clean_message err.message
120
+ msg = if inner_msg
121
+ "#{prefix} with HTTP #{status_code}#{status_part}: #{inner_msg}"
122
+ else
123
+ "#{prefix} with HTTP #{status_code}#{status_part}"
124
+ end
125
+ headers = err.headers || (event.respond_to?(:headers) ? event.headers : nil)
126
+ [msg, status_code, status, err.details, headers]
127
+ end
128
+
129
+ ##
130
+ # @private
131
+ # Builds error attributes directly from raw HTTP response event.
132
+ #
133
+ # @param event [Object] HTTP response event
134
+ # @param prefix [String] Error message prefix
135
+ # @return [Array] Tuple of [message, status_code, status, details, headers]
136
+ def build_from_http_event event, prefix:
137
+ status_code = event.status
138
+ headers = event.respond_to?(:headers) && event.headers ? event.headers : {}
139
+ upload_status = headers["x-goog-upload-status"] || headers["X-Goog-Upload-Status"]
140
+ status_desc = upload_status ? "'#{upload_status}'" : "missing"
141
+ status_name = HTTP_STATUS_PHRASES[status_code]
142
+ status_part = status_name ? " #{status_name}" : ""
143
+ msg = "#{prefix} with HTTP #{status_code}#{status_part} " \
144
+ "(X-Goog-Upload-Status: #{status_desc})"
145
+ [msg, status_code, nil, nil, headers]
146
+ end
147
+ end
148
+ end
149
+
150
+ ##
151
+ # Mixin providing {ResumeHandle} access and uniform formatting for resumable errors.
152
+ #
153
+ # Every error that may carry a resume handle includes this module, so it doubles as the rescue target
154
+ # for "this upload failed but can be retried from where it stopped":
155
+ #
156
+ # @example
157
+ # begin
158
+ # upload.start stream: io, upload_size: size
159
+ # rescue Gapic::Rest::ResumableUpload::HasResumeHandle => e
160
+ # retry_later e.resume_handle if e.resume_handle
161
+ # raise
162
+ # end
163
+ #
164
+ # Included by {RequestFailedError}, {DeadlineExceededError}, {BadResponseError},
165
+ # {UnseekableStreamError} and {StreamMismatchError}.
166
+ #
167
+ # Deliberately **not** included by {UploadRejectedError}, {UploadCancelledError} or {SessionStateError}:
168
+ # the first two mean the session is permanently terminated on the server and the third is a caller misuse,
169
+ # so none of them is retryable. Note also that `resume_handle` may still be `nil` on an including error,
170
+ # for instance when the failure happened before initiation established an upload URL.
171
+ #
172
+ # @!attribute [r] resume_handle
173
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated upload session resume handle
174
+ #
175
+ module HasResumeHandle
176
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil]
177
+ attr_reader :resume_handle
178
+
179
+ ##
180
+ # @private
181
+ # Suffix appended to error message when a resume handle is present.
182
+ # @return [String]
183
+ RESUMABLE_SUFFIX = " (upload session is resumable: see #resume_handle)"
184
+
185
+ ##
186
+ # @private
187
+ # Appends the uniform resumable suffix if resume_handle is non-nil.
188
+ #
189
+ # @param message [String, nil] Error message
190
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Resume handle
191
+ # @return [String, nil]
192
+ def self.append_suffix message, resume_handle
193
+ return message if resume_handle.nil?
194
+ return RESUMABLE_SUFFIX.strip if message.nil? || message.to_s.strip.empty?
195
+ return message if message.end_with? RESUMABLE_SUFFIX
196
+
197
+ "#{message}#{RESUMABLE_SUFFIX}"
198
+ end
199
+ end
200
+
201
+ ##
202
+ # @private
203
+ # Raised when an internal state machine or driver invariant is violated.
204
+ # Produced by {Rules} when an unlisted shape or recipe is encountered, and by
205
+ # {Driver} when a recipe emits a malformed instruction batch.
206
+ #
207
+ class InternalError < Gapic::Common::Error
208
+ end
209
+
210
+ ##
211
+ # @private
212
+ # Raised when an invalid or unmatched event is dispatched for the current protocol state.
213
+ #
214
+ # @!attribute [r] response
215
+ # @return [Gapic::Rest::ResumableUpload::Event::HttpResponse, Object, nil] Associated HTTP response
216
+ # @!attribute [r] state
217
+ # @return [Symbol, nil] Current protocol state
218
+ # @!attribute [r] event
219
+ # @return [Object, nil] Received event
220
+ #
221
+ class InvalidTransitionError < InternalError
222
+ # @return [Gapic::Rest::ResumableUpload::Event::HttpResponse, Object, nil]
223
+ attr_reader :response
224
+
225
+ # @return [Symbol, nil] Current protocol state
226
+ attr_reader :state
227
+
228
+ # @return [Object, nil] Received event
229
+ attr_reader :event
230
+
231
+ ##
232
+ # Initializes a new InvalidTransitionError.
233
+ #
234
+ # @param message [String] Descriptive error message
235
+ # @param state [Symbol, nil] Current protocol state
236
+ # @param event [Object, nil] Received event
237
+ # @param response [Gapic::Rest::ResumableUpload::Event::HttpResponse, Object, nil] Associated HTTP response
238
+ def initialize message, state: nil, event: nil, response: nil
239
+ @state = state
240
+ @event = event
241
+ @response = response || (event if defined?(Event::HttpResponse) && event.is_a?(Event::HttpResponse))
242
+ super message
243
+ end
244
+
245
+ ##
246
+ # Creates an InvalidTransitionError from an event.
247
+ #
248
+ # @param event [Object] Received event
249
+ # @param state [Symbol, nil] Current protocol state
250
+ # @param message [String, nil] Descriptive error message
251
+ # @param response [Object, nil] Associated HTTP response
252
+ # @return [InvalidTransitionError]
253
+ def self.from event, state: nil, message: nil, response: nil
254
+ new(
255
+ message || "Invalid transition for event #{event.inspect}",
256
+ state: state,
257
+ event: event,
258
+ response: response
259
+ )
260
+ end
261
+ end
262
+
263
+ ##
264
+ # Raised when stream rewinding is required but the stream does not support seeking.
265
+ #
266
+ # @!attribute [r] resume_handle
267
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
268
+ #
269
+ class UnseekableStreamError < Gapic::Common::Error
270
+ include HasResumeHandle
271
+
272
+ ##
273
+ # Initializes a new UnseekableStreamError.
274
+ #
275
+ # @param message [String, nil] Descriptive error message
276
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
277
+ def initialize message = nil, resume_handle: nil
278
+ @resume_handle = resume_handle
279
+ super HasResumeHandle.append_suffix(message, resume_handle)
280
+ end
281
+
282
+ ##
283
+ # @private
284
+ # Creates an UnseekableStreamError with optional resume handle.
285
+ #
286
+ # @param message [String, nil] Descriptive error message
287
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
288
+ # @return [UnseekableStreamError]
289
+ def self.from message = nil, resume_handle: nil
290
+ new message, resume_handle: resume_handle
291
+ end
292
+ end
293
+
294
+ ##
295
+ # Raised when stream content or length does not match resumed upload specifications.
296
+ #
297
+ # @!attribute [r] resume_handle
298
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
299
+ #
300
+ class StreamMismatchError < Gapic::Common::Error
301
+ include HasResumeHandle
302
+
303
+ ##
304
+ # Initializes a new StreamMismatchError.
305
+ #
306
+ # @param message [String] Error message
307
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
308
+ def initialize message = "Stream content or length does not match resumed upload", resume_handle: nil
309
+ @resume_handle = resume_handle
310
+ super HasResumeHandle.append_suffix(message, resume_handle)
311
+ end
312
+
313
+ ##
314
+ # @private
315
+ # Creates a StreamMismatchError with optional resume handle.
316
+ #
317
+ # @param message [String, nil] Error message
318
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
319
+ # @return [StreamMismatchError]
320
+ def self.from message = nil, resume_handle: nil
321
+ msg = message || "Stream content or length does not match resumed upload"
322
+ new msg, resume_handle: resume_handle
323
+ end
324
+ end
325
+
326
+ ##
327
+ # Raised when an unrecoverable HTTP response is received.
328
+ #
329
+ # @!attribute [r] response_body
330
+ # @return [String, nil] Response body from backend
331
+ # @!attribute [r] resume_handle
332
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
333
+ #
334
+ class BadResponseError < Gapic::Rest::Error
335
+ include HasResumeHandle
336
+
337
+ # @return [String, nil] Response body from backend
338
+ attr_reader :response_body
339
+
340
+ ##
341
+ # Initializes a new BadResponseError.
342
+ #
343
+ # @param message [String, nil] Error message
344
+ # @param status_code [Integer, nil] HTTP status code
345
+ # @param status [String, nil] Status description
346
+ # @param details [Object, nil] Error details
347
+ # @param headers [Object, nil] Response headers
348
+ # @param response_body [String, nil] Response body
349
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
350
+ def initialize message = nil, status_code = nil, status: nil, details: nil, headers: nil,
351
+ response_body: nil, resume_handle: nil
352
+ @response_body = response_body
353
+ @resume_handle = resume_handle
354
+ super HasResumeHandle.append_suffix(message, resume_handle),
355
+ status_code, status: status, details: details, headers: headers
356
+ end
357
+
358
+ ##
359
+ # @private
360
+ # Creates a BadResponseError from an HTTP response event.
361
+ #
362
+ # @param event [Object] HTTP response event
363
+ # @param response_body [String, nil] Optional response body override
364
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Optional resume handle
365
+ # @param prefix [String] Leading clause of the error message, used to name the protocol phase the
366
+ # response arrived in
367
+ # @return [BadResponseError]
368
+ def self.from event, response_body: nil, resume_handle: nil, prefix: "Resumable upload failed"
369
+ body = response_body || (event.respond_to?(:body) ? event.body : nil)
370
+ message, status_code, status, details, headers = ErrorBuilder.build_attributes event, prefix: prefix
371
+ new message, status_code, status: status, details: details, headers: headers,
372
+ response_body: body, resume_handle: resume_handle
373
+ end
374
+ end
375
+
376
+ ##
377
+ # Raised when the resumable upload backend explicitly rejects the
378
+ # upload session (returns non-2xx with X-Goog-Upload-Status: final).
379
+ #
380
+ # @!attribute [r] response_body
381
+ # @return [String, nil] Response body from backend
382
+ #
383
+ class UploadRejectedError < Gapic::Rest::Error
384
+ # @return [String, nil] Response body from backend
385
+ attr_reader :response_body
386
+
387
+ ##
388
+ # Initializes a new UploadRejectedError.
389
+ #
390
+ # @param message [String, nil] Error message
391
+ # @param status_code [Integer, nil] HTTP status code
392
+ # @param status [String, nil] Status description
393
+ # @param details [Object, nil] Error details
394
+ # @param headers [Object, nil] Response headers
395
+ # @param response_body [String, nil] Response body
396
+ def initialize message = nil, status_code = nil, status: nil, details: nil, headers: nil, response_body: nil
397
+ @response_body = response_body
398
+ super message, status_code, status: status, details: details, headers: headers
399
+ end
400
+
401
+ ##
402
+ # @private
403
+ # Creates an UploadRejectedError from an HTTP response event.
404
+ #
405
+ # @param event [Object] HTTP response event
406
+ # @param response_body [String, nil] Optional response body override
407
+ # @return [UploadRejectedError]
408
+ def self.from event, response_body: nil
409
+ body = response_body || (event.respond_to?(:body) ? event.body : nil)
410
+ message, status_code, status, details, headers =
411
+ ErrorBuilder.build_attributes event, prefix: "Upload rejected by server"
412
+ new message, status_code, status: status, details: details, headers: headers, response_body: body
413
+ end
414
+ end
415
+
416
+ ##
417
+ # Raised when the upload session was cancelled and will accept no further data.
418
+ #
419
+ # Reachable today only for a cancellation this client did not request: the server reports an
420
+ # established session as cancelled while a chunk, a finalize, or a recovery query is in flight,
421
+ # because another process, another client, or a server-side policy ended it. Client-initiated
422
+ # cancellation is not part of the public API yet, and this error is also what that will raise.
423
+ #
424
+ # Deliberately does not include {HasResumeHandle}. A cancelled session is gone server-side, so there
425
+ # is nothing to resume and retrying against it cannot succeed; a new upload must be started instead.
426
+ #
427
+ class UploadCancelledError < Gapic::Common::Error
428
+ ##
429
+ # Initializes a new UploadCancelledError.
430
+ #
431
+ # @param message [String] Cancellation message
432
+ def initialize message = "Upload session was cancelled"
433
+ super message
434
+ end
435
+
436
+ ##
437
+ # @private
438
+ # Creates an UploadCancelledError from a source event or message string.
439
+ #
440
+ # @param source [Object, String, nil] Source event or message
441
+ # @return [UploadCancelledError]
442
+ def self.from source = nil
443
+ if source.is_a?(String) && !source.empty?
444
+ new source
445
+ else
446
+ new
447
+ end
448
+ end
449
+ end
450
+
451
+ ##
452
+ # Raised when an upload exceeds its global monotonic deadline.
453
+ #
454
+ # @!attribute [r] root_cause
455
+ # @return [Object, nil] Root cause exception if deadline exceeded during a retry loop
456
+ # @!attribute [r] resume_handle
457
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
458
+ #
459
+ class DeadlineExceededError < Gapic::Common::Error
460
+ include HasResumeHandle
461
+
462
+ # @return [Object, nil] Root cause exception if deadline exceeded during a retry loop
463
+ attr_reader :root_cause
464
+
465
+ ##
466
+ # Initializes a new DeadlineExceededError.
467
+ #
468
+ # @param message [String] Deadline exceeded message
469
+ # @param root_cause [Object, nil] Root cause exception
470
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
471
+ def initialize message = "Upload deadline exceeded", root_cause: nil, resume_handle: nil
472
+ super HasResumeHandle.append_suffix(message, resume_handle)
473
+ @root_cause = root_cause
474
+ @resume_handle = resume_handle
475
+ end
476
+
477
+ ##
478
+ # @private
479
+ # Creates a DeadlineExceededError with optional resume handle.
480
+ #
481
+ # @param message [String, nil] Deadline exceeded message
482
+ # @param root_cause [Object, nil] Root cause exception
483
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
484
+ # @return [DeadlineExceededError]
485
+ def self.from message = "Upload deadline exceeded", root_cause: nil, resume_handle: nil
486
+ new message, root_cause: root_cause, resume_handle: resume_handle
487
+ end
488
+ end
489
+
490
+ ##
491
+ # Raised when an HTTP request fails (e.g. transport connection failure, request timeout, or retries exhausted).
492
+ #
493
+ # @!attribute [r] cause
494
+ # @return [StandardError, nil] Underlying cause exception
495
+ # @!attribute [r] resume_handle
496
+ # @return [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
497
+ # @!attribute [r] status_code
498
+ # @return [Integer, nil] HTTP status code if cause was a REST error
499
+ # @!attribute [r] status
500
+ # @return [String, nil] Status description if cause was a REST error
501
+ # @!attribute [r] details
502
+ # @return [Object, nil] Error details if cause was a REST error
503
+ # @!attribute [r] headers
504
+ # @return [Object, nil] Response headers if cause was a REST error
505
+ #
506
+ class RequestFailedError < Gapic::Common::Error
507
+ include HasResumeHandle
508
+
509
+ # @return [Integer, nil]
510
+ attr_reader :status_code
511
+
512
+ # @return [String, nil]
513
+ attr_reader :status
514
+
515
+ # @return [Object, nil]
516
+ attr_reader :details
517
+
518
+ # @return [Object, nil]
519
+ attr_reader :headers
520
+
521
+ ##
522
+ # Initializes a new RequestFailedError.
523
+ #
524
+ # @param message [String, nil] Error message
525
+ # @param cause [StandardError, nil] Underlying cause exception
526
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
527
+ # @param status_code [Integer, nil] HTTP status code
528
+ # @param status [String, nil] Status description
529
+ # @param details [Object, nil] Error details
530
+ # @param headers [Object, nil] Response headers
531
+ def initialize message = nil, cause: nil, resume_handle: nil,
532
+ status_code: nil, status: nil, details: nil, headers: nil
533
+ @cause = cause
534
+ @resume_handle = resume_handle
535
+ @status_code = status_code || (cause.respond_to?(:status_code) ? cause.status_code : nil)
536
+ @status = status || (cause.respond_to?(:status) ? cause.status : nil)
537
+ @details = details || (cause.respond_to?(:details) ? cause.details : nil)
538
+ @headers = headers || (cause.respond_to?(:headers) ? cause.headers : nil)
539
+ msg = message || cause&.message || "Request failed"
540
+ super HasResumeHandle.append_suffix(msg, resume_handle)
541
+ end
542
+
543
+ ##
544
+ # Returns the underlying cause exception.
545
+ #
546
+ # @return [StandardError, nil]
547
+ def cause
548
+ @cause || super
549
+ end
550
+
551
+ ##
552
+ # @private
553
+ # Creates a RequestFailedError from a failure event or error.
554
+ #
555
+ # @param event_or_error [Event::RequestFailed, StandardError] Source event or error
556
+ # @param message [String, nil] Optional message override
557
+ # @param resume_handle [Gapic::Rest::ResumableUpload::ResumeHandle, nil] Associated resume handle
558
+ # @return [RequestFailedError]
559
+ def self.from event_or_error, message: nil, resume_handle: nil
560
+ if event_or_error.respond_to? :source_error
561
+ cause = event_or_error.source_error
562
+ msg = message || event_or_error.message || cause&.message || "Request failed"
563
+ new msg, cause: cause, resume_handle: resume_handle
564
+ elsif event_or_error.is_a? Exception
565
+ msg = message || event_or_error.message || "Request failed"
566
+ new msg, cause: event_or_error, resume_handle: resume_handle
567
+ else
568
+ new message || event_or_error.to_s, resume_handle: resume_handle
569
+ end
570
+ end
571
+ end
572
+
573
+ ##
574
+ # Raised when an operation violates the upload session lifecycle rules, e.g. starting a second run
575
+ # on a coordinator while one is still in flight.
576
+ #
577
+ class SessionStateError < Gapic::Common::Error
578
+ end
579
+ end
580
+ end
581
+ end