http 5.3.1 → 6.0.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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /data/{spec → test}/support/capture_warning.rb +0 -0
@@ -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,24 +117,37 @@ 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]
108
149
  # @raise [ResponseHeaderError] when unable to read response headers
150
+ # @api public
109
151
  def read_headers!
110
152
  until @parser.headers?
111
153
  result = read_more(BUFFER_SIZE)
@@ -116,7 +158,12 @@ module HTTP
116
158
  end
117
159
 
118
160
  # Callback for when we've reached the end of a response
161
+ #
162
+ # @example
163
+ # connection.finish_response
164
+ #
119
165
  # @return [void]
166
+ # @api public
120
167
  def finish_response
121
168
  close unless keep_alive?
122
169
 
@@ -128,7 +175,12 @@ module HTTP
128
175
  end
129
176
 
130
177
  # Close the connection
178
+ #
179
+ # @example
180
+ # connection.close
181
+ #
131
182
  # @return [void]
183
+ # @api public
132
184
  def close
133
185
  @socket.close unless @socket&.closed?
134
186
 
@@ -136,101 +188,78 @@ module HTTP
136
188
  @pending_request = false
137
189
  end
138
190
 
191
+ # Whether there are no pending requests or responses
192
+ #
193
+ # @example
194
+ # connection.finished_request?
195
+ #
196
+ # @return [Boolean]
197
+ # @api public
139
198
  def finished_request?
140
199
  !@pending_request && !@pending_response
141
200
  end
142
201
 
143
202
  # Whether we're keeping the conn alive
203
+ #
204
+ # @example
205
+ # connection.keep_alive?
206
+ #
144
207
  # @return [Boolean]
208
+ # @api public
145
209
  def keep_alive?
146
- !!@keep_alive && !@socket.closed?
210
+ @keep_alive && !@socket.closed?
147
211
  end
148
212
 
149
213
  # Whether our connection has expired
214
+ #
215
+ # @example
216
+ # connection.expired?
217
+ #
150
218
  # @return [Boolean]
219
+ # @api public
151
220
  def expired?
152
221
  !@conn_expires_at || @conn_expires_at < Time.now
153
222
  end
154
223
 
155
224
  private
156
225
 
157
- # Sets up SSL context and starts TLS if needed.
158
- # @param (see #initialize)
226
+ # Initialize connection state
159
227
  # @return [void]
160
- def start_tls(req, options)
161
- return unless req.uri.https? && !failed_proxy_connect?
162
-
163
- ssl_context = options.ssl_context
164
-
165
- unless ssl_context
166
- ssl_context = OpenSSL::SSL::SSLContext.new
167
- ssl_context.set_params(options.ssl || {})
168
- end
169
-
170
- @socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context)
171
- end
172
-
173
- # Open tunnel through proxy
174
- def send_proxy_connect_request(req)
175
- return unless req.uri.https? && req.using_proxy?
176
-
177
- @pending_request = true
178
-
179
- req.connect_using_proxy @socket
180
-
181
- @pending_request = false
182
- @pending_response = true
183
-
184
- read_headers!
185
- @proxy_response_headers = @parser.headers
186
-
187
- if @parser.status_code != 200
188
- @failed_proxy_connect = true
189
- return
190
- end
191
-
192
- @parser.reset
193
- @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
194
237
  end
195
238
 
196
- # 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
+ #
197
244
  # @return [void]
198
- def reset_timer
199
- @conn_expires_at = Time.now + @keep_alive_timeout if @persistent
200
- end
245
+ # @api private
246
+ def check_premature_eof(eof)
247
+ return unless eof && !@parser.finished? && body_framed?
201
248
 
202
- # Store whether the connection should be kept alive.
203
- # Once we reset the parser, we lose all of this state.
204
- # @return [void]
205
- def set_keep_alive
206
- return @keep_alive = false unless @persistent
207
-
208
- @keep_alive =
209
- case @parser.http_version
210
- when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
211
- @parser.headers[Headers::CONNECTION] == KEEP_ALIVE
212
- when HTTP_1_1 # HTTP/1.1 is opt-out
213
- @parser.headers[Headers::CONNECTION] != CLOSE
214
- else # Anything else we assume doesn't supportit
215
- false
216
- end
249
+ close
250
+ raise ConnectionError, "response body ended prematurely"
217
251
  end
218
252
 
219
- # Feeds some more data into parser
253
+ # Connect socket and set up proxy/TLS
220
254
  # @return [void]
221
- # @raise [SocketReadError] when unable to read from socket
222
- def read_more(size)
223
- return if @parser.finished?
224
-
225
- value = @socket.readpartial(size, @buffer)
226
- if value == :eof
227
- @parser << ""
228
- :eof
229
- elsif value
230
- @parser << value
231
- end
232
- rescue IOError, SocketError, SystemCallError => e
233
- raise SocketReadError, "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
234
263
  end
235
264
  end
236
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
@@ -9,7 +9,9 @@ module HTTP
9
9
 
10
10
  # Types of Connection errors
11
11
  class ResponseHeaderError < ConnectionError; end
12
+ # Error raised when reading from a socket fails
12
13
  class SocketReadError < ConnectionError; end
14
+ # Error raised when writing to a socket fails
13
15
  class SocketWriteError < ConnectionError; end
14
16
 
15
17
  # Generic Request error
@@ -23,8 +25,23 @@ module HTTP
23
25
 
24
26
  # When status code indicates an error
25
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
26
35
  attr_reader :response
27
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
28
45
  def initialize(response)
29
46
  @response = response
30
47
 
@@ -32,10 +49,17 @@ module HTTP
32
49
  end
33
50
  end
34
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
+
35
59
  # Generic Timeout error
36
60
  class TimeoutError < Error; end
37
61
 
38
- # Timeout when first establishing the conncetion
62
+ # Timeout when first establishing the connection
39
63
  class ConnectTimeoutError < TimeoutError; end
40
64
 
41
65
  # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
data/lib/http/feature.rb CHANGED
@@ -1,25 +1,85 @@
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"
81
+ require "http/features/caching"
82
+ require "http/features/digest_auth"
23
83
  require "http/features/instrumentation"
24
84
  require "http/features/logging"
25
85
  require "http/features/normalize_uri"