http 3.1.0 → 5.3.1

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 (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
data/http.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path("../lib", __FILE__)
3
+ lib = File.expand_path("lib", __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require "http/version"
6
6
 
@@ -25,12 +25,21 @@ Gem::Specification.new do |gem|
25
25
  gem.require_paths = ["lib"]
26
26
  gem.version = HTTP::VERSION
27
27
 
28
- gem.required_ruby_version = ">= 2.2"
28
+ gem.required_ruby_version = ">= 2.6"
29
29
 
30
- gem.add_runtime_dependency "http_parser.rb", "~> 0.6.0"
31
- gem.add_runtime_dependency "http-form_data", "~> 2.0"
30
+ gem.add_runtime_dependency "addressable", "~> 2.8"
32
31
  gem.add_runtime_dependency "http-cookie", "~> 1.0"
33
- gem.add_runtime_dependency "addressable", "~> 2.3"
32
+ gem.add_runtime_dependency "http-form_data", "~> 2.2"
34
33
 
35
- gem.add_development_dependency "bundler", "~> 1.0"
34
+ gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
35
+
36
+ gem.add_development_dependency "bundler", "~> 2.0"
37
+
38
+ gem.metadata = {
39
+ "source_code_uri" => "https://github.com/httprb/http",
40
+ "wiki_uri" => "https://github.com/httprb/http/wiki",
41
+ "bug_tracker_uri" => "https://github.com/httprb/http/issues",
42
+ "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGELOG.md",
43
+ "rubygems_mfa_required" => "true"
44
+ }
36
45
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Base64
5
+ module_function
6
+
7
+ # Equivalent to Base64.strict_encode64
8
+ def encode64(input)
9
+ [input].pack("m0")
10
+ end
11
+ end
12
+ end
@@ -1,111 +1,113 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
-
3
+ require "http/base64"
5
4
  require "http/headers"
6
5
 
7
6
  module HTTP
8
7
  module Chainable
8
+ include HTTP::Base64
9
+
9
10
  # Request a get sans response body
10
11
  # @param uri
11
12
  # @option options [Hash]
12
- def head(uri, options = {}) # rubocop:disable Style/OptionHash
13
+ def head(uri, options = {})
13
14
  request :head, uri, options
14
15
  end
15
16
 
16
17
  # Get a resource
17
18
  # @param uri
18
19
  # @option options [Hash]
19
- def get(uri, options = {}) # rubocop:disable Style/OptionHash
20
+ def get(uri, options = {})
20
21
  request :get, uri, options
21
22
  end
22
23
 
23
24
  # Post to a resource
24
25
  # @param uri
25
26
  # @option options [Hash]
26
- def post(uri, options = {}) # rubocop:disable Style/OptionHash
27
+ def post(uri, options = {})
27
28
  request :post, uri, options
28
29
  end
29
30
 
30
31
  # Put to a resource
31
32
  # @param uri
32
33
  # @option options [Hash]
33
- def put(uri, options = {}) # rubocop:disable Style/OptionHash
34
+ def put(uri, options = {})
34
35
  request :put, uri, options
35
36
  end
36
37
 
37
38
  # Delete a resource
38
39
  # @param uri
39
40
  # @option options [Hash]
40
- def delete(uri, options = {}) # rubocop:disable Style/OptionHash
41
+ def delete(uri, options = {})
41
42
  request :delete, uri, options
42
43
  end
43
44
 
44
45
  # Echo the request back to the client
45
46
  # @param uri
46
47
  # @option options [Hash]
47
- def trace(uri, options = {}) # rubocop:disable Style/OptionHash
48
+ def trace(uri, options = {})
48
49
  request :trace, uri, options
49
50
  end
50
51
 
51
52
  # Return the methods supported on the given URI
52
53
  # @param uri
53
54
  # @option options [Hash]
54
- def options(uri, options = {}) # rubocop:disable Style/OptionHash
55
+ def options(uri, options = {})
55
56
  request :options, uri, options
56
57
  end
57
58
 
58
59
  # Convert to a transparent TCP/IP tunnel
59
60
  # @param uri
60
61
  # @option options [Hash]
61
- def connect(uri, options = {}) # rubocop:disable Style/OptionHash
62
+ def connect(uri, options = {})
62
63
  request :connect, uri, options
63
64
  end
64
65
 
65
66
  # Apply partial modifications to a resource
66
67
  # @param uri
67
68
  # @option options [Hash]
68
- def patch(uri, options = {}) # rubocop:disable Style/OptionHash
69
+ def patch(uri, options = {})
69
70
  request :patch, uri, options
70
71
  end
71
72
 
72
73
  # Make an HTTP request with the given verb
73
- # @param uri
74
- # @option options [Hash]
75
- def request(verb, uri, options = {}) # rubocop:disable Style/OptionHash
76
- branch(options).request verb, uri
74
+ # @param (see Client#request)
75
+ def request(*args)
76
+ branch(default_options).request(*args)
77
+ end
78
+
79
+ # Prepare an HTTP request with the given verb
80
+ # @param (see Client#build_request)
81
+ def build_request(*args)
82
+ branch(default_options).build_request(*args)
77
83
  end
78
84
 
79
85
  # @overload timeout(options = {})
80
- # Syntax sugar for `timeout(:per_operation, options)`
81
- # @overload timeout(klass, options = {})
82
- # Adds a timeout to the request.
83
- # @param [#to_sym] klass
84
- # either :null, :global, or :per_operation
86
+ # Adds per operation timeouts to the request
85
87
  # @param [Hash] options
86
88
  # @option options [Float] :read Read timeout
87
89
  # @option options [Float] :write Write timeout
88
90
  # @option options [Float] :connect Connect timeout
89
- def timeout(klass, options = {}) # rubocop:disable Style/OptionHash
90
- if klass.is_a? Hash
91
- options = klass
92
- klass = :per_operation
93
- end
94
-
95
- klass = case klass.to_sym
96
- when :null then HTTP::Timeout::Null
97
- when :global then HTTP::Timeout::Global
98
- when :per_operation then HTTP::Timeout::PerOperation
99
- else raise ArgumentError, "Unsupported Timeout class: #{klass}"
100
- end
101
-
102
- %i[read write connect].each do |k|
91
+ # @overload timeout(global_timeout)
92
+ # Adds a global timeout to the full request
93
+ # @param [Numeric] global_timeout
94
+ def timeout(options)
95
+ klass, options = case options
96
+ when Numeric then [HTTP::Timeout::Global, {:global => options}]
97
+ when Hash then [HTTP::Timeout::PerOperation, options.dup]
98
+ when :null then [HTTP::Timeout::Null, {}]
99
+ else raise ArgumentError, "Use `.timeout(global_timeout_in_seconds)` or `.timeout(connect: x, write: y, read: z)`."
100
+
101
+ end
102
+
103
+ %i[global read write connect].each do |k|
103
104
  next unless options.key? k
105
+
104
106
  options["#{k}_timeout".to_sym] = options.delete k
105
107
  end
106
108
 
107
109
  branch default_options.merge(
108
- :timeout_class => klass,
110
+ :timeout_class => klass,
109
111
  :timeout_options => options
110
112
  )
111
113
  end
@@ -144,9 +146,10 @@ module HTTP
144
146
  options = {:keep_alive_timeout => timeout}
145
147
  p_client = branch default_options.merge(options).with_persistent host
146
148
  return p_client unless block_given?
149
+
147
150
  yield p_client
148
151
  ensure
149
- p_client.close if p_client
152
+ p_client&.close
150
153
  end
151
154
 
152
155
  # Make a request through an HTTP proxy
@@ -168,10 +171,10 @@ module HTTP
168
171
  alias through via
169
172
 
170
173
  # Make client follow redirects.
171
- # @param opts
174
+ # @param options
172
175
  # @return [HTTP::Client]
173
176
  # @see Redirector#initialize
174
- def follow(options = {}) # rubocop:disable Style/OptionHash
177
+ def follow(options = {})
175
178
  branch default_options.with_follow options
176
179
  end
177
180
 
@@ -209,10 +212,11 @@ module HTTP
209
212
  # @option opts [#to_s] :user
210
213
  # @option opts [#to_s] :pass
211
214
  def basic_auth(opts)
212
- user = opts.fetch :user
213
- pass = opts.fetch :pass
215
+ user = opts.fetch(:user)
216
+ pass = opts.fetch(:pass)
217
+ creds = "#{user}:#{pass}"
214
218
 
215
- auth("Basic " + Base64.strict_encode64("#{user}:#{pass}"))
219
+ auth("Basic #{encode64(creds)}")
216
220
  end
217
221
 
218
222
  # Get options for HTTP
@@ -236,11 +240,37 @@ module HTTP
236
240
  # Turn on given features. Available features are:
237
241
  # * auto_inflate
238
242
  # * auto_deflate
243
+ # * instrumentation
244
+ # * logging
245
+ # * normalize_uri
246
+ # * raise_error
239
247
  # @param features
240
248
  def use(*features)
241
249
  branch default_options.with_features(features)
242
250
  end
243
251
 
252
+ # Returns retriable client instance, which retries requests if they failed
253
+ # due to some socket errors or response status is `5xx`.
254
+ #
255
+ # @example Usage
256
+ #
257
+ # # Retry max 5 times with randomly growing delay between retries
258
+ # HTTP.retriable.get(url)
259
+ #
260
+ # # Retry max 3 times with randomly growing delay between retries
261
+ # HTTP.retriable(times: 3).get(url)
262
+ #
263
+ # # Retry max 3 times with 1 sec delay between retries
264
+ # HTTP.retriable(times: 3, delay: proc { 1 }).get(url)
265
+ #
266
+ # # Retry max 3 times with geometrically progressed delay between retries
267
+ # HTTP.retriable(times: 3, delay: proc { |i| 1 + i*i }).get(url)
268
+ #
269
+ # @param (see Performer#initialize)
270
+ def retriable(**options)
271
+ Retriable::Client.new(Retriable::Performer.new(options), default_options)
272
+ end
273
+
244
274
  private
245
275
 
246
276
  # :nodoc:
data/lib/http/client.rb CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
4
4
 
5
5
  require "http/form_data"
6
6
  require "http/options"
7
+ require "http/feature"
7
8
  require "http/headers"
8
9
  require "http/connection"
9
10
  require "http/redirector"
@@ -15,7 +16,7 @@ module HTTP
15
16
  extend Forwardable
16
17
  include Chainable
17
18
 
18
- HTTP_OR_HTTPS_RE = %r{^https?://}i
19
+ HTTP_OR_HTTPS_RE = %r{^https?://}i.freeze
19
20
 
20
21
  def initialize(default_options = {})
21
22
  @default_options = HTTP::Options.new(default_options)
@@ -24,28 +25,34 @@ module HTTP
24
25
  end
25
26
 
26
27
  # Make an HTTP request
27
- def request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash
28
+ def request(verb, uri, opts = {})
29
+ opts = @default_options.merge(opts)
30
+ req = build_request(verb, uri, opts)
31
+ res = perform(req, opts)
32
+ return res unless opts.follow
33
+
34
+ Redirector.new(opts.follow).perform(req, res) do |request|
35
+ perform(wrap_request(request, opts), opts)
36
+ end
37
+ end
38
+
39
+ # Prepare an HTTP request
40
+ def build_request(verb, uri, opts = {})
28
41
  opts = @default_options.merge(opts)
29
42
  uri = make_request_uri(uri, opts)
30
43
  headers = make_request_headers(opts)
31
44
  body = make_request_body(opts, headers)
32
- proxy = opts.proxy
33
45
 
34
46
  req = HTTP::Request.new(
35
- :verb => verb,
36
- :uri => uri,
37
- :headers => headers,
38
- :proxy => proxy,
39
- :body => body,
40
- :auto_deflate => opts.feature(:auto_deflate)
47
+ :verb => verb,
48
+ :uri => uri,
49
+ :uri_normalizer => opts.feature(:normalize_uri)&.normalizer,
50
+ :proxy => opts.proxy,
51
+ :headers => headers,
52
+ :body => body
41
53
  )
42
54
 
43
- res = perform(req, opts)
44
- return res unless opts.follow
45
-
46
- Redirector.new(opts.follow).perform(req, res) do |request|
47
- perform(request, opts)
48
- end
55
+ wrap_request(req, opts)
49
56
  end
50
57
 
51
58
  # @!method persistent?
@@ -59,23 +66,24 @@ module HTTP
59
66
 
60
67
  @state = :dirty
61
68
 
62
- @connection ||= HTTP::Connection.new(req, options)
63
-
64
- unless @connection.failed_proxy_connect?
65
- @connection.send_request(req)
66
- @connection.read_headers!
69
+ begin
70
+ @connection ||= HTTP::Connection.new(req, options)
71
+
72
+ unless @connection.failed_proxy_connect?
73
+ @connection.send_request(req)
74
+ @connection.read_headers!
75
+ end
76
+ rescue Error => e
77
+ options.features.each_value do |feature|
78
+ feature.on_error(req, e)
79
+ end
80
+ raise
67
81
  end
82
+ res = build_response(req, options)
68
83
 
69
- res = Response.new(
70
- :status => @connection.status_code,
71
- :version => @connection.http_version,
72
- :headers => @connection.headers,
73
- :proxy_headers => @connection.proxy_response_headers,
74
- :connection => @connection,
75
- :encoding => options.encoding,
76
- :auto_inflate => options.feature(:auto_inflate),
77
- :uri => req.uri
78
- )
84
+ res = options.features.inject(res) do |response, (_name, feature)|
85
+ feature.wrap_response(response)
86
+ end
79
87
 
80
88
  @connection.finish_response if req.verb == :head
81
89
  @state = :clean
@@ -87,26 +95,44 @@ module HTTP
87
95
  end
88
96
 
89
97
  def close
90
- @connection.close if @connection
98
+ @connection&.close
91
99
  @connection = nil
92
100
  @state = :clean
93
101
  end
94
102
 
95
103
  private
96
104
 
105
+ def wrap_request(req, opts)
106
+ opts.features.inject(req) do |request, (_name, feature)|
107
+ feature.wrap_request(request)
108
+ end
109
+ end
110
+
111
+ def build_response(req, options)
112
+ Response.new(
113
+ :status => @connection.status_code,
114
+ :version => @connection.http_version,
115
+ :headers => @connection.headers,
116
+ :proxy_headers => @connection.proxy_response_headers,
117
+ :connection => @connection,
118
+ :encoding => options.encoding,
119
+ :request => req
120
+ )
121
+ end
122
+
97
123
  # Verify our request isn't going to be made against another URI
98
124
  def verify_connection!(uri)
99
125
  if default_options.persistent? && uri.origin != default_options.persistent
100
126
  raise StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{uri.origin}"
127
+ end
128
+
101
129
  # We re-create the connection object because we want to let prior requests
102
130
  # lazily load the body as long as possible, and this mimics prior functionality.
103
- elsif @connection && (!@connection.keep_alive? || @connection.expired?)
104
- close
131
+ return close if @connection && (!@connection.keep_alive? || @connection.expired?)
132
+
105
133
  # If we get into a bad state (eg, Timeout.timeout ensure being killed)
106
134
  # close the connection to prevent potential for mixed responses.
107
- elsif @state == :dirty
108
- close
109
- end
135
+ return close if @state == :dirty
110
136
  end
111
137
 
112
138
  # Merges query params if needed
@@ -116,15 +142,11 @@ module HTTP
116
142
  def make_request_uri(uri, opts)
117
143
  uri = uri.to_s
118
144
 
119
- if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE
120
- uri = "#{default_options.persistent}#{uri}"
121
- end
145
+ uri = "#{default_options.persistent}#{uri}" if default_options.persistent? && uri !~ HTTP_OR_HTTPS_RE
122
146
 
123
147
  uri = HTTP::URI.parse uri
124
148
 
125
- if opts.params && !opts.params.empty?
126
- uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a)
127
- end
149
+ uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a) if opts.params && !opts.params.empty?
128
150
 
129
151
  # Some proxies (seen on WEBRick) fail if URL has
130
152
  # empty path (e.g. `http://example.com`) while it's RFC-complaint:
@@ -148,14 +170,6 @@ module HTTP
148
170
  headers[Headers::COOKIE] = cookies
149
171
  end
150
172
 
151
- if (auto_deflate = opts.feature(:auto_deflate))
152
- # We need to delete Content-Length header. It will be set automatically
153
- # by HTTP::Request::Writer
154
- headers.delete(Headers::CONTENT_LENGTH)
155
-
156
- headers[Headers::CONTENT_ENCODING] = auto_deflate.method
157
- end
158
-
159
173
  headers
160
174
  end
161
175
 
@@ -165,14 +179,21 @@ module HTTP
165
179
  when opts.body
166
180
  opts.body
167
181
  when opts.form
168
- form = HTTP::FormData.create opts.form
182
+ form = make_form_data(opts.form)
169
183
  headers[Headers::CONTENT_TYPE] ||= form.content_type
170
184
  form
171
185
  when opts.json
172
186
  body = MimeType[:json].encode opts.json
173
- headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
187
+ headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
174
188
  body
175
189
  end
176
190
  end
191
+
192
+ def make_form_data(form)
193
+ return form if form.is_a? HTTP::FormData::Multipart
194
+ return form if form.is_a? HTTP::FormData::Urlencoded
195
+
196
+ HTTP::FormData.create(form)
197
+ end
177
198
  end
178
199
  end
@@ -3,11 +3,10 @@
3
3
  require "forwardable"
4
4
 
5
5
  require "http/headers"
6
- require "http/response/parser"
7
6
 
8
7
  module HTTP
9
8
  # A connection to the HTTP server
10
- class Connection # rubocop: disable Metrics/ClassLength
9
+ class Connection
11
10
  extend Forwardable
12
11
 
13
12
  # Allowed values for CONNECTION header
@@ -35,6 +34,7 @@ module HTTP
35
34
  @pending_request = false
36
35
  @pending_response = false
37
36
  @failed_proxy_connect = false
37
+ @buffer = "".b
38
38
 
39
39
  @parser = Response::Parser.new
40
40
 
@@ -44,8 +44,11 @@ module HTTP
44
44
  send_proxy_connect_request(req)
45
45
  start_tls(req, options)
46
46
  reset_timer
47
- rescue IOError, SocketError, SystemCallError => ex
48
- raise ConnectionError, "failed to connect: #{ex}", ex.backtrace
47
+ rescue IOError, SocketError, SystemCallError => e
48
+ raise ConnectionError, "failed to connect: #{e}", e.backtrace
49
+ rescue TimeoutError
50
+ close
51
+ raise
49
52
  end
50
53
 
51
54
  # @see (HTTP::Response::Parser#status_code)
@@ -67,8 +70,13 @@ module HTTP
67
70
  # @param [Request] req Request to send to the server
68
71
  # @return [nil]
69
72
  def send_request(req)
70
- raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body." if @pending_response
71
- raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body." if @pending_request
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
76
+
77
+ if @pending_request
78
+ raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
79
+ end
72
80
 
73
81
  @pending_request = true
74
82
 
@@ -92,19 +100,16 @@ module HTTP
92
100
  chunk = @parser.read(size)
93
101
  finish_response if finished
94
102
 
95
- chunk.to_s
103
+ chunk || "".b
96
104
  end
97
105
 
98
106
  # Reads data from socket up until headers are loaded
99
107
  # @return [void]
108
+ # @raise [ResponseHeaderError] when unable to read response headers
100
109
  def read_headers!
101
- loop do
102
- if read_more(BUFFER_SIZE) == :eof
103
- raise ConnectionError, "couldn't read response headers" unless @parser.headers?
104
- break
105
- elsif @parser.headers?
106
- break
107
- end
110
+ until @parser.headers?
111
+ result = read_more(BUFFER_SIZE)
112
+ raise ResponseHeaderError, "couldn't read response headers" if result == :eof
108
113
  end
109
114
 
110
115
  set_keep_alive
@@ -125,12 +130,16 @@ module HTTP
125
130
  # Close the connection
126
131
  # @return [void]
127
132
  def close
128
- @socket.close unless @socket.closed?
133
+ @socket.close unless @socket&.closed?
129
134
 
130
135
  @pending_response = false
131
136
  @pending_request = false
132
137
  end
133
138
 
139
+ def finished_request?
140
+ !@pending_request && !@pending_response
141
+ end
142
+
134
143
  # Whether we're keeping the conn alive
135
144
  # @return [Boolean]
136
145
  def keep_alive?
@@ -209,18 +218,19 @@ module HTTP
209
218
 
210
219
  # Feeds some more data into parser
211
220
  # @return [void]
221
+ # @raise [SocketReadError] when unable to read from socket
212
222
  def read_more(size)
213
223
  return if @parser.finished?
214
224
 
215
- value = @socket.readpartial(size)
225
+ value = @socket.readpartial(size, @buffer)
216
226
  if value == :eof
217
227
  @parser << ""
218
228
  :eof
219
229
  elsif value
220
230
  @parser << value
221
231
  end
222
- rescue IOError, SocketError, SystemCallError => ex
223
- raise ConnectionError, "error reading from socket: #{ex}", ex.backtrace
232
+ rescue IOError, SocketError, SystemCallError => e
233
+ raise SocketReadError, "error reading from socket: #{e}", e.backtrace
224
234
  end
225
235
  end
226
236
  end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- ContentType = Struct.new(:mime_type, :charset) do
5
- MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
6
- CHARSET_RE = /;\s*charset=([^;]+)/i
4
+ class ContentType
5
+ MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
6
+ CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
7
+
8
+ attr_accessor :mime_type, :charset
7
9
 
8
10
  class << self
9
11
  # Parse string and return ContentType struct
@@ -15,15 +17,18 @@ module HTTP
15
17
 
16
18
  # :nodoc:
17
19
  def mime_type(str)
18
- m = str.to_s[MIME_TYPE_RE, 1]
19
- m && m.strip.downcase
20
+ str.to_s[MIME_TYPE_RE, 1]&.strip&.downcase
20
21
  end
21
22
 
22
23
  # :nodoc:
23
24
  def charset(str)
24
- m = str.to_s[CHARSET_RE, 1]
25
- m && m.strip.delete('"')
25
+ str.to_s[CHARSET_RE, 1]&.strip&.delete('"')
26
26
  end
27
27
  end
28
+
29
+ def initialize(mime_type = nil, charset = nil)
30
+ @mime_type = mime_type
31
+ @charset = charset
32
+ end
28
33
  end
29
34
  end
data/lib/http/errors.rb CHANGED
@@ -7,6 +7,11 @@ 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
+ class SocketReadError < ConnectionError; end
13
+ class SocketWriteError < ConnectionError; end
14
+
10
15
  # Generic Request error
11
16
  class RequestError < Error; end
12
17
 
@@ -16,9 +21,23 @@ module HTTP
16
21
  # Requested to do something when we're in the wrong state
17
22
  class StateError < ResponseError; end
18
23
 
24
+ # When status code indicates an error
25
+ class StatusError < ResponseError
26
+ attr_reader :response
27
+
28
+ def initialize(response)
29
+ @response = response
30
+
31
+ super("Unexpected status code #{response.code}")
32
+ end
33
+ end
34
+
19
35
  # Generic Timeout error
20
36
  class TimeoutError < Error; end
21
37
 
38
+ # Timeout when first establishing the conncetion
39
+ class ConnectTimeoutError < TimeoutError; end
40
+
22
41
  # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
23
42
  class HeaderError < Error; end
24
43
  end