http 5.2.0 → 6.0.2

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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +110 -13
  4. data/http.gemspec +38 -35
  5. data/lib/http/base64.rb +22 -0
  6. data/lib/http/chainable/helpers.rb +62 -0
  7. data/lib/http/chainable/verbs.rb +136 -0
  8. data/lib/http/chainable.rb +249 -129
  9. data/lib/http/client.rb +158 -127
  10. data/lib/http/connection/internals.rb +141 -0
  11. data/lib/http/connection.rb +128 -97
  12. data/lib/http/content_type.rb +61 -6
  13. data/lib/http/errors.rb +41 -1
  14. data/lib/http/feature.rb +67 -6
  15. data/lib/http/features/auto_deflate.rb +124 -17
  16. data/lib/http/features/auto_inflate.rb +38 -15
  17. data/lib/http/features/caching/entry.rb +178 -0
  18. data/lib/http/features/caching/in_memory_store.rb +63 -0
  19. data/lib/http/features/caching.rb +216 -0
  20. data/lib/http/features/digest_auth.rb +234 -0
  21. data/lib/http/features/instrumentation.rb +97 -17
  22. data/lib/http/features/logging.rb +183 -5
  23. data/lib/http/features/normalize_uri.rb +17 -0
  24. data/lib/http/features/raise_error.rb +37 -0
  25. data/lib/http/form_data/composite_io.rb +106 -0
  26. data/lib/http/form_data/file.rb +95 -0
  27. data/lib/http/form_data/multipart/param.rb +62 -0
  28. data/lib/http/form_data/multipart.rb +106 -0
  29. data/lib/http/form_data/part.rb +52 -0
  30. data/lib/http/form_data/readable.rb +58 -0
  31. data/lib/http/form_data/urlencoded.rb +175 -0
  32. data/lib/http/form_data/version.rb +8 -0
  33. data/lib/http/form_data.rb +102 -0
  34. data/lib/http/headers/known.rb +3 -0
  35. data/lib/http/headers/normalizer.rb +50 -0
  36. data/lib/http/headers.rb +185 -92
  37. data/lib/http/mime_type/adapter.rb +24 -9
  38. data/lib/http/mime_type/json.rb +19 -4
  39. data/lib/http/mime_type.rb +21 -3
  40. data/lib/http/options/definitions.rb +189 -0
  41. data/lib/http/options.rb +172 -125
  42. data/lib/http/redirector.rb +80 -75
  43. data/lib/http/request/body.rb +87 -6
  44. data/lib/http/request/builder.rb +184 -0
  45. data/lib/http/request/proxy.rb +83 -0
  46. data/lib/http/request/writer.rb +78 -17
  47. data/lib/http/request.rb +216 -99
  48. data/lib/http/response/body.rb +103 -18
  49. data/lib/http/response/inflater.rb +35 -7
  50. data/lib/http/response/parser.rb +98 -4
  51. data/lib/http/response/status/reasons.rb +2 -4
  52. data/lib/http/response/status.rb +141 -31
  53. data/lib/http/response.rb +219 -61
  54. data/lib/http/retriable/delay_calculator.rb +91 -0
  55. data/lib/http/retriable/errors.rb +35 -0
  56. data/lib/http/retriable/performer.rb +197 -0
  57. data/lib/http/session.rb +280 -0
  58. data/lib/http/timeout/global.rb +147 -34
  59. data/lib/http/timeout/null.rb +155 -9
  60. data/lib/http/timeout/per_operation.rb +139 -18
  61. data/lib/http/uri/normalizer.rb +82 -0
  62. data/lib/http/uri/parsing.rb +182 -0
  63. data/lib/http/uri.rb +289 -124
  64. data/lib/http/version.rb +2 -1
  65. data/lib/http.rb +11 -1
  66. data/sig/http.rbs +1619 -0
  67. metadata +42 -175
  68. data/.github/workflows/ci.yml +0 -67
  69. data/.gitignore +0 -15
  70. data/.rspec +0 -1
  71. data/.rubocop/layout.yml +0 -8
  72. data/.rubocop/metrics.yml +0 -4
  73. data/.rubocop/style.yml +0 -32
  74. data/.rubocop.yml +0 -11
  75. data/.rubocop_todo.yml +0 -206
  76. data/.yardopts +0 -2
  77. data/CHANGELOG.md +0 -41
  78. data/CHANGES_OLD.md +0 -1002
  79. data/CONTRIBUTING.md +0 -26
  80. data/Gemfile +0 -50
  81. data/Guardfile +0 -18
  82. data/Rakefile +0 -64
  83. data/SECURITY.md +0 -17
  84. data/lib/http/headers/mixin.rb +0 -34
  85. data/logo.png +0 -0
  86. data/spec/lib/http/client_spec.rb +0 -556
  87. data/spec/lib/http/connection_spec.rb +0 -88
  88. data/spec/lib/http/content_type_spec.rb +0 -47
  89. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  90. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  91. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  92. data/spec/lib/http/features/logging_spec.rb +0 -65
  93. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  94. data/spec/lib/http/headers_spec.rb +0 -527
  95. data/spec/lib/http/options/body_spec.rb +0 -15
  96. data/spec/lib/http/options/features_spec.rb +0 -33
  97. data/spec/lib/http/options/form_spec.rb +0 -15
  98. data/spec/lib/http/options/headers_spec.rb +0 -24
  99. data/spec/lib/http/options/json_spec.rb +0 -15
  100. data/spec/lib/http/options/merge_spec.rb +0 -68
  101. data/spec/lib/http/options/new_spec.rb +0 -30
  102. data/spec/lib/http/options/proxy_spec.rb +0 -20
  103. data/spec/lib/http/options_spec.rb +0 -13
  104. data/spec/lib/http/redirector_spec.rb +0 -529
  105. data/spec/lib/http/request/body_spec.rb +0 -211
  106. data/spec/lib/http/request/writer_spec.rb +0 -121
  107. data/spec/lib/http/request_spec.rb +0 -234
  108. data/spec/lib/http/response/body_spec.rb +0 -85
  109. data/spec/lib/http/response/parser_spec.rb +0 -74
  110. data/spec/lib/http/response/status_spec.rb +0 -253
  111. data/spec/lib/http/response_spec.rb +0 -262
  112. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  113. data/spec/lib/http/uri_spec.rb +0 -71
  114. data/spec/lib/http_spec.rb +0 -506
  115. data/spec/regression_specs.rb +0 -24
  116. data/spec/spec_helper.rb +0 -88
  117. data/spec/support/black_hole.rb +0 -13
  118. data/spec/support/capture_warning.rb +0 -10
  119. data/spec/support/dummy_server/servlet.rb +0 -190
  120. data/spec/support/dummy_server.rb +0 -43
  121. data/spec/support/fakeio.rb +0 -21
  122. data/spec/support/fuubar.rb +0 -21
  123. data/spec/support/http_handling_shared.rb +0 -190
  124. data/spec/support/proxy_server.rb +0 -39
  125. data/spec/support/servers/config.rb +0 -11
  126. data/spec/support/servers/runner.rb +0 -19
  127. data/spec/support/simplecov.rb +0 -19
  128. data/spec/support/ssl_helper.rb +0 -104
@@ -2,20 +2,28 @@
2
2
 
3
3
  require "forwardable"
4
4
 
5
+ require "http/connection/internals"
5
6
  require "http/headers"
6
7
 
7
8
  module HTTP
8
9
  # A connection to the HTTP server
9
10
  class Connection
10
11
  extend Forwardable
12
+ include Internals
11
13
 
12
14
  # Allowed values for CONNECTION header
13
15
  KEEP_ALIVE = "Keep-Alive"
16
+ # Connection: close header value
14
17
  CLOSE = "close"
15
18
 
16
19
  # Attempt to read this much data
17
20
  BUFFER_SIZE = 16_384
18
21
 
22
+ # Maximum response body size (in bytes) to auto-flush when reusing
23
+ # a connection. Bodies larger than this cause the connection to close
24
+ # instead, to avoid blocking on huge downloads.
25
+ MAX_FLUSH_SIZE = 1_048_576
26
+
19
27
  # HTTP/1.0
20
28
  HTTP_1_0 = "1.0"
21
29
 
@@ -23,27 +31,30 @@ module HTTP
23
31
  HTTP_1_1 = "1.1"
24
32
 
25
33
  # Returned after HTTP CONNECT (via proxy)
34
+ #
35
+ # @example
36
+ # connection.proxy_response_headers
37
+ #
38
+ # @return [HTTP::Headers, nil]
39
+ # @api public
26
40
  attr_reader :proxy_response_headers
27
41
 
42
+ # Initialize a new connection to an HTTP server
43
+ #
44
+ # @example
45
+ # Connection.new(req, options)
46
+ #
28
47
  # @param [HTTP::Request] req
29
48
  # @param [HTTP::Options] options
49
+ # @return [Connection]
30
50
  # @raise [HTTP::ConnectionError] when failed to connect
51
+ # @api public
31
52
  def initialize(req, options)
32
- @persistent = options.persistent?
33
- @keep_alive_timeout = options.keep_alive_timeout.to_f
34
- @pending_request = false
35
- @pending_response = false
36
- @failed_proxy_connect = false
37
- @buffer = "".b
38
-
39
- @parser = Response::Parser.new
40
-
41
- @socket = options.timeout_class.new(options.timeout_options)
42
- @socket.connect(options.socket_class, req.socket_host, req.socket_port, options.nodelay)
43
-
44
- send_proxy_connect_request(req)
45
- start_tls(req, options)
46
- reset_timer
53
+ init_state(options)
54
+ connect_socket(req, options)
55
+ rescue IO::TimeoutError => e
56
+ close
57
+ raise ConnectTimeoutError, e.message, e.backtrace
47
58
  rescue IOError, SocketError, SystemCallError => e
48
59
  raise ConnectionError, "failed to connect: #{e}", e.backtrace
49
60
  rescue TimeoutError
@@ -60,19 +71,37 @@ module HTTP
60
71
  # @see (HTTP::Response::Parser#headers)
61
72
  def_delegator :@parser, :headers
62
73
 
74
+ # Whether the proxy CONNECT request failed
75
+ #
76
+ # @example
77
+ # connection.failed_proxy_connect?
78
+ #
63
79
  # @return [Boolean] whenever proxy connect failed
80
+ # @api public
64
81
  def failed_proxy_connect?
65
82
  @failed_proxy_connect
66
83
  end
67
84
 
85
+ # Set the pending response for auto-flushing before the next request
86
+ #
87
+ # @example
88
+ # connection.pending_response = response
89
+ #
90
+ # @param [HTTP::Response, false] response
91
+ # @return [void]
92
+ # @api public
93
+ attr_writer :pending_response
94
+
68
95
  # Send a request to the server
69
96
  #
97
+ # @example
98
+ # connection.send_request(req)
99
+ #
70
100
  # @param [Request] req Request to send to the server
71
101
  # @return [nil]
102
+ # @api public
72
103
  def send_request(req)
73
- if @pending_response
74
- raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
75
- end
104
+ flush_pending_response if @pending_response
76
105
 
77
106
  if @pending_request
78
107
  raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
@@ -88,34 +117,53 @@ module HTTP
88
117
 
89
118
  # Read a chunk of the body
90
119
  #
120
+ # @example
121
+ # connection.readpartial
122
+ #
123
+ # @param [Integer] size maximum bytes to read
124
+ # @param [String, nil] outbuf buffer to fill with data
91
125
  # @return [String] data chunk
92
- # @return [nil] when no more data left
93
- def readpartial(size = BUFFER_SIZE)
94
- return unless @pending_response
126
+ # @raise [EOFError] when no more data left
127
+ # @api public
128
+ def readpartial(size = BUFFER_SIZE, outbuf = nil)
129
+ raise EOFError unless @pending_response
95
130
 
96
131
  chunk = @parser.read(size)
97
- return chunk if chunk
98
-
99
- finished = (read_more(size) == :eof) || @parser.finished?
100
- chunk = @parser.read(size)
101
- finish_response if finished
132
+ unless chunk
133
+ eof = read_more(size) == :eof
134
+ check_premature_eof(eof)
135
+ finished = eof || @parser.finished?
136
+ chunk = @parser.read(size) || "".b
137
+ finish_response if finished
138
+ end
102
139
 
103
- chunk || "".b
140
+ outbuf ? outbuf.replace(chunk) : chunk
104
141
  end
105
142
 
106
143
  # Reads data from socket up until headers are loaded
144
+ #
145
+ # @example
146
+ # connection.read_headers!
147
+ #
107
148
  # @return [void]
149
+ # @raise [ResponseHeaderError] when unable to read response headers
150
+ # @api public
108
151
  def read_headers!
109
152
  until @parser.headers?
110
153
  result = read_more(BUFFER_SIZE)
111
- raise ConnectionError, "couldn't read response headers" if result == :eof
154
+ raise ResponseHeaderError, "couldn't read response headers" if result == :eof
112
155
  end
113
156
 
114
157
  set_keep_alive
115
158
  end
116
159
 
117
160
  # Callback for when we've reached the end of a response
161
+ #
162
+ # @example
163
+ # connection.finish_response
164
+ #
118
165
  # @return [void]
166
+ # @api public
119
167
  def finish_response
120
168
  close unless keep_alive?
121
169
 
@@ -127,7 +175,12 @@ module HTTP
127
175
  end
128
176
 
129
177
  # Close the connection
178
+ #
179
+ # @example
180
+ # connection.close
181
+ #
130
182
  # @return [void]
183
+ # @api public
131
184
  def close
132
185
  @socket.close unless @socket&.closed?
133
186
 
@@ -135,100 +188,78 @@ module HTTP
135
188
  @pending_request = false
136
189
  end
137
190
 
191
+ # Whether there are no pending requests or responses
192
+ #
193
+ # @example
194
+ # connection.finished_request?
195
+ #
196
+ # @return [Boolean]
197
+ # @api public
138
198
  def finished_request?
139
199
  !@pending_request && !@pending_response
140
200
  end
141
201
 
142
202
  # Whether we're keeping the conn alive
203
+ #
204
+ # @example
205
+ # connection.keep_alive?
206
+ #
143
207
  # @return [Boolean]
208
+ # @api public
144
209
  def keep_alive?
145
- !!@keep_alive && !@socket.closed?
210
+ @keep_alive && !@socket.closed?
146
211
  end
147
212
 
148
213
  # Whether our connection has expired
214
+ #
215
+ # @example
216
+ # connection.expired?
217
+ #
149
218
  # @return [Boolean]
219
+ # @api public
150
220
  def expired?
151
221
  !@conn_expires_at || @conn_expires_at < Time.now
152
222
  end
153
223
 
154
224
  private
155
225
 
156
- # Sets up SSL context and starts TLS if needed.
157
- # @param (see #initialize)
226
+ # Initialize connection state
158
227
  # @return [void]
159
- def start_tls(req, options)
160
- return unless req.uri.https? && !failed_proxy_connect?
161
-
162
- ssl_context = options.ssl_context
163
-
164
- unless ssl_context
165
- ssl_context = OpenSSL::SSL::SSLContext.new
166
- ssl_context.set_params(options.ssl || {})
167
- end
168
-
169
- @socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context)
170
- end
171
-
172
- # Open tunnel through proxy
173
- def send_proxy_connect_request(req)
174
- return unless req.uri.https? && req.using_proxy?
175
-
176
- @pending_request = true
177
-
178
- req.connect_using_proxy @socket
179
-
180
- @pending_request = false
181
- @pending_response = true
182
-
183
- read_headers!
184
- @proxy_response_headers = @parser.headers
185
-
186
- if @parser.status_code != 200
187
- @failed_proxy_connect = true
188
- return
189
- end
190
-
191
- @parser.reset
192
- @pending_response = false
228
+ # @api private
229
+ def init_state(options)
230
+ @persistent = options.persistent?
231
+ @keep_alive_timeout = options.keep_alive_timeout.to_f
232
+ @pending_request = false
233
+ @pending_response = false
234
+ @failed_proxy_connect = false
235
+ @buffer = "".b
236
+ @parser = Response::Parser.new
193
237
  end
194
238
 
195
- # Resets expiration of persistent connection.
239
+ # Check for premature end-of-file and raise if detected
240
+ #
241
+ # @example
242
+ # check_premature_eof(:eof)
243
+ #
196
244
  # @return [void]
197
- def reset_timer
198
- @conn_expires_at = Time.now + @keep_alive_timeout if @persistent
199
- end
245
+ # @api private
246
+ def check_premature_eof(eof)
247
+ return unless eof && !@parser.finished? && body_framed?
200
248
 
201
- # Store whether the connection should be kept alive.
202
- # Once we reset the parser, we lose all of this state.
203
- # @return [void]
204
- def set_keep_alive
205
- return @keep_alive = false unless @persistent
206
-
207
- @keep_alive =
208
- case @parser.http_version
209
- when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
210
- @parser.headers[Headers::CONNECTION] == KEEP_ALIVE
211
- when HTTP_1_1 # HTTP/1.1 is opt-out
212
- @parser.headers[Headers::CONNECTION] != CLOSE
213
- else # Anything else we assume doesn't supportit
214
- false
215
- end
249
+ close
250
+ raise ConnectionError, "response body ended prematurely"
216
251
  end
217
252
 
218
- # Feeds some more data into parser
253
+ # Connect socket and set up proxy/TLS
219
254
  # @return [void]
220
- def read_more(size)
221
- return if @parser.finished?
222
-
223
- value = @socket.readpartial(size, @buffer)
224
- if value == :eof
225
- @parser << ""
226
- :eof
227
- elsif value
228
- @parser << value
229
- end
230
- rescue IOError, SocketError, SystemCallError => e
231
- raise ConnectionError, "error reading from socket: #{e}", e.backtrace
255
+ # @api private
256
+ def connect_socket(req, options)
257
+ @socket = options.timeout_class.new(**options.timeout_options) # steep:ignore
258
+ @socket.connect(options.socket_class, req.socket_host, req.socket_port, nodelay: options.nodelay)
259
+
260
+ send_proxy_connect_request(req)
261
+ start_tls(req, options)
262
+ reset_timer
232
263
  end
233
264
  end
234
265
  end
@@ -1,34 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
+ # Parsed representation of a Content-Type header
4
5
  class ContentType
5
- MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
6
- CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
6
+ # Pattern for extracting MIME type from Content-Type header
7
+ MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
8
+ # Pattern for extracting charset from Content-Type header
9
+ CHARSET_RE = /;\s*charset=([^;]+)/i
7
10
 
8
- attr_accessor :mime_type, :charset
11
+ # MIME type of the content
12
+ #
13
+ # @example
14
+ # content_type.mime_type # => "text/html"
15
+ #
16
+ # @return [String, nil]
17
+ # @api public
18
+ attr_accessor :mime_type
19
+
20
+ # Character set of the content
21
+ #
22
+ # @example
23
+ # content_type.charset # => "utf-8"
24
+ #
25
+ # @return [String, nil]
26
+ # @api public
27
+ attr_accessor :charset
9
28
 
10
29
  class << self
11
- # Parse string and return ContentType struct
30
+ # Parse string and return ContentType object
31
+ #
32
+ # @example
33
+ # HTTP::ContentType.parse("text/html; charset=utf-8")
34
+ #
35
+ # @param [String] str content type header value
36
+ # @return [ContentType]
37
+ # @api public
12
38
  def parse(str)
13
39
  new mime_type(str), charset(str)
14
40
  end
15
41
 
16
42
  private
17
43
 
18
- # :nodoc:
44
+ # Extract MIME type from header string
45
+ # @return [String, nil]
46
+ # @api private
19
47
  def mime_type(str)
20
48
  str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
21
49
  end
22
50
 
23
- # :nodoc:
51
+ # Extract charset from header string
52
+ # @return [String, nil]
53
+ # @api private
24
54
  def charset(str)
25
55
  str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
26
56
  end
27
57
  end
28
58
 
59
+ # Create a new ContentType instance
60
+ #
61
+ # @example
62
+ # HTTP::ContentType.new("text/html", "utf-8")
63
+ #
64
+ # @param [String, nil] mime_type MIME type
65
+ # @param [String, nil] charset character set
66
+ # @return [ContentType]
67
+ # @api public
29
68
  def initialize(mime_type = nil, charset = nil)
30
69
  @mime_type = mime_type
31
70
  @charset = charset
32
71
  end
72
+
73
+ # Pattern matching interface for matching against content type attributes
74
+ #
75
+ # @example
76
+ # case response.content_type
77
+ # in { mime_type: /json/ }
78
+ # "JSON content"
79
+ # end
80
+ #
81
+ # @param keys [Array<Symbol>, nil] keys to extract, or nil for all
82
+ # @return [Hash{Symbol => Object}]
83
+ # @api public
84
+ def deconstruct_keys(keys)
85
+ hash = { mime_type: @mime_type, charset: @charset }
86
+ keys ? hash.slice(*keys) : hash
87
+ end
33
88
  end
34
89
  end
data/lib/http/errors.rb CHANGED
@@ -7,6 +7,13 @@ module HTTP
7
7
  # Generic Connection error
8
8
  class ConnectionError < Error; end
9
9
 
10
+ # Types of Connection errors
11
+ class ResponseHeaderError < ConnectionError; end
12
+ # Error raised when reading from a socket fails
13
+ class SocketReadError < ConnectionError; end
14
+ # Error raised when writing to a socket fails
15
+ class SocketWriteError < ConnectionError; end
16
+
10
17
  # Generic Request error
11
18
  class RequestError < Error; end
12
19
 
@@ -16,10 +23,43 @@ module HTTP
16
23
  # Requested to do something when we're in the wrong state
17
24
  class StateError < ResponseError; end
18
25
 
26
+ # When status code indicates an error
27
+ class StatusError < ResponseError
28
+ # The HTTP response that caused the error
29
+ #
30
+ # @example
31
+ # error.response
32
+ #
33
+ # @return [HTTP::Response]
34
+ # @api public
35
+ attr_reader :response
36
+
37
+ # Create a new StatusError from a response
38
+ #
39
+ # @example
40
+ # HTTP::StatusError.new(response)
41
+ #
42
+ # @param [HTTP::Response] response the response with error status
43
+ # @return [StatusError]
44
+ # @api public
45
+ def initialize(response)
46
+ @response = response
47
+
48
+ super("Unexpected status code #{response.code}")
49
+ end
50
+ end
51
+
52
+ # Raised when `Response#parse` fails due to any underlying reason (unexpected
53
+ # MIME type, or decoder fails). See `Exception#cause` for the original exception.
54
+ class ParseError < ResponseError; end
55
+
56
+ # Requested MimeType adapter not found.
57
+ class UnsupportedMimeTypeError < Error; end
58
+
19
59
  # Generic Timeout error
20
60
  class TimeoutError < Error; end
21
61
 
22
- # Timeout when first establishing the conncetion
62
+ # Timeout when first establishing the connection
23
63
  class ConnectTimeoutError < TimeoutError; end
24
64
 
25
65
  # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
data/lib/http/feature.rb CHANGED
@@ -1,25 +1,86 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
+ # Base class for HTTP client features (middleware)
4
5
  class Feature
5
- def initialize(opts = {})
6
- @opts = opts
7
- end
8
-
6
+ # Wraps an HTTP request
7
+ #
8
+ # @example
9
+ # feature.wrap_request(request)
10
+ #
11
+ # @param request [HTTP::Request]
12
+ # @return [HTTP::Request]
13
+ # @api public
9
14
  def wrap_request(request)
10
15
  request
11
16
  end
12
17
 
18
+ # Wraps an HTTP response
19
+ #
20
+ # @example
21
+ # feature.wrap_response(response)
22
+ #
23
+ # @param response [HTTP::Response]
24
+ # @return [HTTP::Response]
25
+ # @api public
13
26
  def wrap_response(response)
14
27
  response
15
28
  end
16
29
 
17
- def on_error(request, error); end
30
+ # Callback invoked before each request attempt
31
+ #
32
+ # Unlike {#wrap_request}, which is called once when the request is built,
33
+ # this hook is called before every attempt, including retries. Use it for
34
+ # per-attempt side effects like starting instrumentation spans.
35
+ #
36
+ # @example
37
+ # feature.on_request(request)
38
+ #
39
+ # @param _request [HTTP::Request]
40
+ # @return [nil]
41
+ # @api public
42
+ def on_request(_request); end
43
+
44
+ # Wraps the HTTP exchange for a single request attempt
45
+ #
46
+ # Called once per attempt (including retries), wrapping the send and
47
+ # receive cycle. The block performs the I/O and returns the response.
48
+ # Override this to add behavior that must span the entire exchange,
49
+ # such as instrumentation spans or circuit breakers.
50
+ #
51
+ # @example Timing a request
52
+ # def around_request(request)
53
+ # start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
54
+ # yield(request).tap { log_duration(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) }
55
+ # end
56
+ #
57
+ # @param request [HTTP::Request]
58
+ # @yield [HTTP::Request] the request to perform
59
+ # @yieldreturn [HTTP::Response]
60
+ # @return [HTTP::Response] must return the response from yield
61
+ # @api public
62
+ def around_request(request)
63
+ yield request
64
+ end
65
+
66
+ # Callback for request errors
67
+ #
68
+ # @example
69
+ # feature.on_error(request, error)
70
+ #
71
+ # @param _request [HTTP::Request]
72
+ # @param _error [Exception]
73
+ # @return [nil]
74
+ # @api public
75
+ def on_error(_request, _error); end
18
76
  end
19
77
  end
20
78
 
21
79
  require "http/features/auto_inflate"
22
80
  require "http/features/auto_deflate"
23
- require "http/features/logging"
81
+ require "http/features/caching"
82
+ require "http/features/digest_auth"
24
83
  require "http/features/instrumentation"
84
+ require "http/features/logging"
25
85
  require "http/features/normalize_uri"
86
+ require "http/features/raise_error"