http 5.1.1 → 5.3.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -24
  3. data/.rubocop/metrics.yml +4 -0
  4. data/.rubocop/rspec.yml +9 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +45 -32
  7. data/CHANGELOG.md +57 -0
  8. data/{CHANGES.md → CHANGES_OLD.md} +1 -1
  9. data/Gemfile +1 -0
  10. data/README.md +4 -2
  11. data/SECURITY.md +13 -1
  12. data/http.gemspec +8 -2
  13. data/lib/http/base64.rb +12 -0
  14. data/lib/http/chainable.rb +27 -3
  15. data/lib/http/client.rb +1 -1
  16. data/lib/http/connection.rb +12 -3
  17. data/lib/http/errors.rb +16 -0
  18. data/lib/http/feature.rb +2 -1
  19. data/lib/http/features/instrumentation.rb +6 -1
  20. data/lib/http/features/raise_error.rb +22 -0
  21. data/lib/http/headers/normalizer.rb +69 -0
  22. data/lib/http/headers.rb +26 -40
  23. data/lib/http/request/writer.rb +2 -1
  24. data/lib/http/request.rb +15 -5
  25. data/lib/http/retriable/client.rb +37 -0
  26. data/lib/http/retriable/delay_calculator.rb +64 -0
  27. data/lib/http/retriable/errors.rb +14 -0
  28. data/lib/http/retriable/performer.rb +153 -0
  29. data/lib/http/timeout/null.rb +8 -5
  30. data/lib/http/uri.rb +18 -2
  31. data/lib/http/version.rb +1 -1
  32. data/lib/http.rb +1 -0
  33. data/spec/lib/http/client_spec.rb +1 -0
  34. data/spec/lib/http/connection_spec.rb +23 -1
  35. data/spec/lib/http/features/instrumentation_spec.rb +19 -0
  36. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  37. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  38. data/spec/lib/http/options/headers_spec.rb +5 -1
  39. data/spec/lib/http/redirector_spec.rb +6 -5
  40. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  41. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  42. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  43. data/spec/lib/http_spec.rb +49 -3
  44. data/spec/spec_helper.rb +1 -0
  45. data/spec/support/dummy_server/servlet.rb +19 -6
  46. data/spec/support/dummy_server.rb +2 -1
  47. metadata +28 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef55bbff996952784d0917931b8eaa6f12a1adfc9cc480daf098cbc8cd8f6107
4
- data.tar.gz: 222f8e8723969f89994fe2a0692c41786e450c200c45b84f5c8418f736254a64
3
+ metadata.gz: da85aeba1d2d3dce86f59a678ade3f74df35514397cb7488f9aaab9ea5a57220
4
+ data.tar.gz: 28e5143890592f55f51fa65874f787f3c3e9eac5f9f6103c589082414eff378e
5
5
  SHA512:
6
- metadata.gz: 49b0c9e508fb02fca9d9e8a26d707202c5ac67bbdf73fce15b616b321545a3a32be96eb9e23f4d389687ad1720372eaba4412e18d800c5d29fab5bb0f4bc0d93
7
- data.tar.gz: 1b2e22b2b33abe8059e78535556a15c53959b321fb225cd84153d5a8ea608ba4d03ead9cf018720f63b1e7c27c477f8c3f1b151cae60f2d50b6811e7dd9f5bcc
6
+ metadata.gz: 6b14bb800aefa920d2511b962967253fc034847bb07f8d181bb9cec6dba136ca0c51655fd0da716986897be5ef83e6023705578016db7bbd12bbd1f2ed4bbeb4
7
+ data.tar.gz: dc996c20d358b382fbb2b1d93af7a706f858167392c8db3d8bf11e114419c0c7ed0a61350753f9c13422ca48b469af0b359f80694fe423066a02439066bfdd07
@@ -2,9 +2,9 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ main ]
5
+ branches: [ main, 5-x-stable ]
6
6
  pull_request:
7
- branches: [ main ]
7
+ branches: [ main, 5-x-stable ]
8
8
 
9
9
  env:
10
10
  BUNDLE_WITHOUT: "development"
@@ -16,11 +16,11 @@ jobs:
16
16
 
17
17
  strategy:
18
18
  matrix:
19
- ruby: [ ruby-2.6, ruby-2.7, ruby-3.0, ruby-3.1 ]
19
+ ruby: [ ruby-2.6, ruby-2.7, ruby-3.0, ruby-3.1, ruby-3.2, ruby-3.3 ]
20
20
  os: [ ubuntu-latest ]
21
21
 
22
22
  steps:
23
- - uses: actions/checkout@v3
23
+ - uses: actions/checkout@v4
24
24
 
25
25
  - uses: ruby/setup-ruby@v1
26
26
  with:
@@ -30,14 +30,6 @@ jobs:
30
30
  - name: bundle exec rspec
31
31
  run: bundle exec rspec --format progress --force-colour
32
32
 
33
- - name: Prepare Coveralls test coverage report
34
- uses: coverallsapp/github-action@v1.1.2
35
- with:
36
- github-token: ${{ secrets.GITHUB_TOKEN }}
37
- flag-name: "${{ matrix.ruby }} @${{ matrix.os }}"
38
- path-to-lcov: ./coverage/lcov/lcov.info
39
- parallel: true
40
-
41
33
  test-flaky:
42
34
  runs-on: ${{ matrix.os }}
43
35
 
@@ -47,7 +39,7 @@ jobs:
47
39
  os: [ ubuntu-latest ]
48
40
 
49
41
  steps:
50
- - uses: actions/checkout@v3
42
+ - uses: actions/checkout@v4
51
43
 
52
44
  - uses: ruby/setup-ruby@v1
53
45
  with:
@@ -58,21 +50,11 @@ jobs:
58
50
  continue-on-error: true
59
51
  run: bundle exec rspec --format progress --force-colour
60
52
 
61
- coveralls:
62
- needs: test
63
- runs-on: ubuntu-latest
64
- steps:
65
- - name: Finalize Coveralls test coverage report
66
- uses: coverallsapp/github-action@master
67
- with:
68
- github-token: ${{ secrets.GITHUB_TOKEN }}
69
- parallel-finished: true
70
-
71
53
  lint:
72
54
  runs-on: ubuntu-latest
73
55
 
74
56
  steps:
75
- - uses: actions/checkout@v3
57
+ - uses: actions/checkout@v4
76
58
 
77
59
  - uses: ruby/setup-ruby@v1
78
60
  with:
@@ -0,0 +1,4 @@
1
+ Metrics/BlockLength:
2
+ Exclude:
3
+ - 'spec/**/*.rb'
4
+ - '*.gemspec'
@@ -0,0 +1,9 @@
1
+ RSpec/ExampleLength:
2
+ CountAsOne:
3
+ - array
4
+ - hash
5
+ - heredoc
6
+ - method_call
7
+
8
+ RSpec/MultipleExpectations:
9
+ Max: 5
data/.rubocop.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  inherit_from:
2
2
  - .rubocop_todo.yml
3
3
  - .rubocop/layout.yml
4
+ - .rubocop/metrics.yml
4
5
  - .rubocop/style.yml
5
6
 
6
7
  AllCops:
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100`
3
- # on 2022-06-16 14:35:44 UTC using RuboCop version 1.30.1.
3
+ # on 2025-06-09 02:44:43 UTC using RuboCop version 1.30.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -14,7 +14,7 @@ Gemspec/DeprecatedAttributeAssignment:
14
14
  Exclude:
15
15
  - 'http.gemspec'
16
16
 
17
- # Offense count: 53
17
+ # Offense count: 55
18
18
  # This cop supports safe autocorrection (--autocorrect).
19
19
  # Configuration parameters: EnforcedStyle.
20
20
  # SupportedStyles: leading, trailing
@@ -30,7 +30,14 @@ Layout/DotPosition:
30
30
  - 'spec/lib/http_spec.rb'
31
31
  - 'spec/support/http_handling_shared.rb'
32
32
 
33
- # Offense count: 176
33
+ # Offense count: 2
34
+ # This cop supports safe autocorrection (--autocorrect).
35
+ # Configuration parameters: IndentationWidth.
36
+ # SupportedStyles: special_inside_parentheses, consistent, align_braces
37
+ Layout/FirstHashElementIndentation:
38
+ EnforcedStyle: consistent
39
+
40
+ # Offense count: 206
34
41
  # This cop supports safe autocorrection (--autocorrect).
35
42
  # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
36
43
  # SupportedStyles: space, no_space, compact
@@ -62,6 +69,18 @@ Lint/MissingSuper:
62
69
  - 'lib/http/features/logging.rb'
63
70
  - 'lib/http/features/normalize_uri.rb'
64
71
 
72
+ # Offense count: 1
73
+ # This cop supports safe autocorrection (--autocorrect).
74
+ Lint/RedundantCopDisableDirective:
75
+ Exclude:
76
+ - 'spec/lib/http/retriable/performer_spec.rb'
77
+
78
+ # Offense count: 6
79
+ # Configuration parameters: AllowComments, AllowNil.
80
+ Lint/SuppressedException:
81
+ Exclude:
82
+ - 'spec/lib/http/retriable/performer_spec.rb'
83
+
65
84
  # Offense count: 8
66
85
  # Configuration parameters: IgnoredMethods, CountRepeatedAttributes, Max.
67
86
  Metrics/AbcSize:
@@ -74,34 +93,6 @@ Metrics/AbcSize:
74
93
  - 'lib/http/request.rb'
75
94
  - 'lib/http/response.rb'
76
95
 
77
- # Offense count: 70
78
- # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
79
- # IgnoredMethods: refine
80
- Metrics/BlockLength:
81
- Exclude:
82
- - '**/*.gemspec'
83
- - 'spec/lib/http/client_spec.rb'
84
- - 'spec/lib/http/connection_spec.rb'
85
- - 'spec/lib/http/content_type_spec.rb'
86
- - 'spec/lib/http/features/auto_deflate_spec.rb'
87
- - 'spec/lib/http/features/auto_inflate_spec.rb'
88
- - 'spec/lib/http/features/instrumentation_spec.rb'
89
- - 'spec/lib/http/features/logging_spec.rb'
90
- - 'spec/lib/http/headers/mixin_spec.rb'
91
- - 'spec/lib/http/headers_spec.rb'
92
- - 'spec/lib/http/options/merge_spec.rb'
93
- - 'spec/lib/http/redirector_spec.rb'
94
- - 'spec/lib/http/request/body_spec.rb'
95
- - 'spec/lib/http/request/writer_spec.rb'
96
- - 'spec/lib/http/request_spec.rb'
97
- - 'spec/lib/http/response/body_spec.rb'
98
- - 'spec/lib/http/response/parser_spec.rb'
99
- - 'spec/lib/http/response/status_spec.rb'
100
- - 'spec/lib/http/response_spec.rb'
101
- - 'spec/lib/http/uri_spec.rb'
102
- - 'spec/lib/http_spec.rb'
103
- - 'spec/support/http_handling_shared.rb'
104
-
105
96
  # Offense count: 4
106
97
  # Configuration parameters: CountComments, Max, CountAsOne.
107
98
  Metrics/ClassLength:
@@ -118,7 +109,7 @@ Metrics/CyclomaticComplexity:
118
109
  - 'lib/http/chainable.rb'
119
110
  - 'lib/http/client.rb'
120
111
 
121
- # Offense count: 18
112
+ # Offense count: 19
122
113
  # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
123
114
  Metrics/MethodLength:
124
115
  Exclude:
@@ -133,6 +124,7 @@ Metrics/MethodLength:
133
124
  - 'lib/http/request.rb'
134
125
  - 'lib/http/response.rb'
135
126
  - 'lib/http/response/body.rb'
127
+ - 'lib/http/retriable/performer.rb'
136
128
  - 'lib/http/timeout/global.rb'
137
129
 
138
130
  # Offense count: 1
@@ -173,6 +165,27 @@ Style/Encoding:
173
165
  - 'spec/lib/http_spec.rb'
174
166
  - 'spec/support/dummy_server/servlet.rb'
175
167
 
168
+ # Offense count: 71
169
+ # This cop supports safe autocorrection (--autocorrect).
170
+ # Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
171
+ # SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
172
+ # SupportedShorthandSyntax: always, never, either
173
+ Style/HashSyntax:
174
+ Exclude:
175
+ - 'spec/lib/http/features/raise_error_spec.rb'
176
+ - 'spec/lib/http/retriable/delay_calculator_spec.rb'
177
+ - 'spec/lib/http/retriable/performer_spec.rb'
178
+ - 'spec/lib/http_spec.rb'
179
+
180
+ # Offense count: 4
181
+ # This cop supports unsafe autocorrection (--autocorrect-all).
182
+ # Configuration parameters: EnforcedStyle.
183
+ # SupportedStyles: literals, strict
184
+ Style/MutableConstant:
185
+ Exclude:
186
+ - 'lib/http/headers/normalizer.rb'
187
+ - 'lib/http/retriable/delay_calculator.rb'
188
+
176
189
  # Offense count: 17
177
190
  # Configuration parameters: SuspiciousParamNames, Allowlist.
178
191
  # SuspiciousParamNames: options, opts, args, params, parameters
data/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [5.3.0] - 2025-06-09
11
+
12
+ ### Added
13
+
14
+ - (backported) Add .retriable feature to Http
15
+ - (backported) Add more specific ConnectionError classes
16
+ - (backported) New feature: RaiseError
17
+
18
+ ### Changed
19
+
20
+ - (backported) Drop depenency on base64
21
+ - (backported) Cache header normalization to reduce object allocation
22
+ - (backported) Use native llhttp on MRI
23
+
24
+
25
+ ## [5.2.0] - 2024-02-05
26
+
27
+ ### Added
28
+
29
+ - Add `Connection#finished_request?`
30
+ ([#743](https://github.com/httprb/http/pull/743))
31
+ - Add `Instrumentation#on_error`
32
+ ([#746](https://github.com/httprb/http/pull/746))
33
+ - Add `base64` dependency (suppresses warnings on Ruby 3.0)
34
+ ([#759](https://github.com/httprb/http/pull/759))
35
+ - Add `PURGE` HTTP verb
36
+ ([#757](https://github.com/httprb/http/pull/757))
37
+ - Add Ruby-3.3 support
38
+
39
+ ### Changed
40
+
41
+ - **BREAKING** Process features in reverse order
42
+ ([#766](https://github.com/httprb/http/pull/766))
43
+ - **BREAKING** Downcase Content-Type charset name
44
+ ([#753](https://github.com/httprb/http/pull/753))
45
+ - **BREAKING** Make URI normalization more conservative
46
+ ([#758](https://github.com/httprb/http/pull/758))
47
+
48
+ ### Fixed
49
+
50
+ - Close sockets on initialize failure
51
+ ([#762](https://github.com/httprb/http/pull/762))
52
+ - Prevent CRLF injection due to broken URL normalizer
53
+ ([#765](https://github.com/httprb/http/pull/765))
54
+
55
+ [unreleased]: https://github.com/httprb/http/compare/v5.3.0...5-x-stable
56
+ [5.3.0]: https://github.com/httprb/http/compare/v5.2.0...v5.3.0
57
+ [5.2.0]: https://github.com/httprb/http/compare/v5.1.1...v5.2.0
@@ -54,7 +54,7 @@
54
54
  Use features on redirected requests.
55
55
  ([@nomis])
56
56
 
57
- * [#678](https://github.com/schwern)
57
+ * [#678](https://github.com/httprb/http/pull/678)
58
58
  Restore `HTTP::Response` `:uri` option for backwards compatibility.
59
59
  ([@schwern])
60
60
 
data/Gemfile CHANGED
@@ -37,6 +37,7 @@ group :test do
37
37
 
38
38
  gem "rspec", "~> 3.10"
39
39
  gem "rspec-its"
40
+ gem "rspec-memory"
40
41
 
41
42
  gem "yardstick"
42
43
  end
data/README.md CHANGED
@@ -110,11 +110,13 @@ and call `#readpartial` on it repeatedly until it returns `nil`:
110
110
  This library aims to support and is [tested against][build-link]
111
111
  the following Ruby versions:
112
112
 
113
+ - JRuby 9.3
113
114
  - Ruby 2.6
114
115
  - Ruby 2.7
115
116
  - Ruby 3.0
116
117
  - Ruby 3.1
117
- - JRuby 9.3
118
+ - Ruby 3.2
119
+ - Ruby 3.3
118
120
 
119
121
  If something doesn't work on one of these versions, it's a bug.
120
122
 
@@ -160,5 +162,5 @@ See LICENSE.txt for further details.
160
162
  [//]: # (links)
161
163
 
162
164
  [documentation]: https://github.com/httprb/http/wiki
163
- [requests]: http://docs.python-requests.org/en/latest/
165
+ [requests]: https://docs.python-requests.org/en/latest/
164
166
  [llhttp]: https://llhttp.org/
data/SECURITY.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Security Policy
2
2
 
3
+ ## Supported Versions
4
+
5
+ Security updates are applied only to the most recent release.
6
+
3
7
  ## Reporting a Vulnerability
4
8
 
5
- Please report security issues to `bascule@gmail.com`
9
+ If you have discovered a security vulnerability in this project, please report
10
+ it privately. **Do not disclose it as a public issue.** This gives us time to
11
+ work with you to fix the issue before public exposure, reducing the chance that
12
+ the exploit will be used before a patch is released.
13
+
14
+ Please disclose it at [security advisory](https://github.com/httprb/http/security/advisories/new).
15
+
16
+ This project is maintained by a team of volunteers on a reasonable-effort basis.
17
+ As such, please give us at least 90 days to work on a fix before public exposure.
data/http.gemspec CHANGED
@@ -30,7 +30,13 @@ Gem::Specification.new do |gem|
30
30
  gem.add_runtime_dependency "addressable", "~> 2.8"
31
31
  gem.add_runtime_dependency "http-cookie", "~> 1.0"
32
32
  gem.add_runtime_dependency "http-form_data", "~> 2.2"
33
- gem.add_runtime_dependency "llhttp-ffi", "~> 0.4.0"
33
+
34
+ # Use native llhttp for MRI (more performant) and llhttp-ffi for other interpreters (better compatibility)
35
+ if RUBY_ENGINE == "ruby"
36
+ gem.add_runtime_dependency "llhttp", "~> 0.5.0"
37
+ else
38
+ gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
39
+ end
34
40
 
35
41
  gem.add_development_dependency "bundler", "~> 2.0"
36
42
 
@@ -38,7 +44,7 @@ Gem::Specification.new do |gem|
38
44
  "source_code_uri" => "https://github.com/httprb/http",
39
45
  "wiki_uri" => "https://github.com/httprb/http/wiki",
40
46
  "bug_tracker_uri" => "https://github.com/httprb/http/issues",
41
- "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGES.md",
47
+ "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGELOG.md",
42
48
  "rubygems_mfa_required" => "true"
43
49
  }
44
50
  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,11 +1,12 @@
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]
@@ -215,7 +216,7 @@ module HTTP
215
216
  pass = opts.fetch(:pass)
216
217
  creds = "#{user}:#{pass}"
217
218
 
218
- auth("Basic #{Base64.strict_encode64(creds)}")
219
+ auth("Basic #{encode64(creds)}")
219
220
  end
220
221
 
221
222
  # Get options for HTTP
@@ -242,11 +243,34 @@ module HTTP
242
243
  # * instrumentation
243
244
  # * logging
244
245
  # * normalize_uri
246
+ # * raise_error
245
247
  # @param features
246
248
  def use(*features)
247
249
  branch default_options.with_features(features)
248
250
  end
249
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
+
250
274
  private
251
275
 
252
276
  # :nodoc:
data/lib/http/client.rb CHANGED
@@ -184,7 +184,7 @@ module HTTP
184
184
  form
185
185
  when opts.json
186
186
  body = MimeType[:json].encode opts.json
187
- headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
187
+ headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
188
188
  body
189
189
  end
190
190
  end
@@ -46,6 +46,9 @@ module HTTP
46
46
  reset_timer
47
47
  rescue IOError, SocketError, SystemCallError => e
48
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)
@@ -102,10 +105,11 @@ module HTTP
102
105
 
103
106
  # Reads data from socket up until headers are loaded
104
107
  # @return [void]
108
+ # @raise [ResponseHeaderError] when unable to read response headers
105
109
  def read_headers!
106
110
  until @parser.headers?
107
111
  result = read_more(BUFFER_SIZE)
108
- raise ConnectionError, "couldn't read response headers" if result == :eof
112
+ raise ResponseHeaderError, "couldn't read response headers" if result == :eof
109
113
  end
110
114
 
111
115
  set_keep_alive
@@ -126,12 +130,16 @@ module HTTP
126
130
  # Close the connection
127
131
  # @return [void]
128
132
  def close
129
- @socket.close unless @socket.closed?
133
+ @socket.close unless @socket&.closed?
130
134
 
131
135
  @pending_response = false
132
136
  @pending_request = false
133
137
  end
134
138
 
139
+ def finished_request?
140
+ !@pending_request && !@pending_response
141
+ end
142
+
135
143
  # Whether we're keeping the conn alive
136
144
  # @return [Boolean]
137
145
  def keep_alive?
@@ -210,6 +218,7 @@ module HTTP
210
218
 
211
219
  # Feeds some more data into parser
212
220
  # @return [void]
221
+ # @raise [SocketReadError] when unable to read from socket
213
222
  def read_more(size)
214
223
  return if @parser.finished?
215
224
 
@@ -221,7 +230,7 @@ module HTTP
221
230
  @parser << value
222
231
  end
223
232
  rescue IOError, SocketError, SystemCallError => e
224
- raise ConnectionError, "error reading from socket: #{e}", e.backtrace
233
+ raise SocketReadError, "error reading from socket: #{e}", e.backtrace
225
234
  end
226
235
  end
227
236
  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,6 +21,17 @@ 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
 
data/lib/http/feature.rb CHANGED
@@ -20,6 +20,7 @@ end
20
20
 
21
21
  require "http/features/auto_inflate"
22
22
  require "http/features/auto_deflate"
23
- require "http/features/logging"
24
23
  require "http/features/instrumentation"
24
+ require "http/features/logging"
25
25
  require "http/features/normalize_uri"
26
+ require "http/features/raise_error"
@@ -19,11 +19,12 @@ module HTTP
19
19
  # and `finish` so the duration of the request can be calculated.
20
20
  #
21
21
  class Instrumentation < Feature
22
- attr_reader :instrumenter, :name
22
+ attr_reader :instrumenter, :name, :error_name
23
23
 
24
24
  def initialize(instrumenter: NullInstrumenter.new, namespace: "http")
25
25
  @instrumenter = instrumenter
26
26
  @name = "request.#{namespace}"
27
+ @error_name = "error.#{namespace}"
27
28
  end
28
29
 
29
30
  def wrap_request(request)
@@ -39,6 +40,10 @@ module HTTP
39
40
  response
40
41
  end
41
42
 
43
+ def on_error(request, error)
44
+ instrumenter.instrument(error_name, :request => request, :error => error)
45
+ end
46
+
42
47
  HTTP::Options.register_feature(:instrumentation, self)
43
48
 
44
49
  class NullInstrumenter
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Features
5
+ class RaiseError < Feature
6
+ def initialize(ignore: [])
7
+ super()
8
+
9
+ @ignore = ignore
10
+ end
11
+
12
+ def wrap_response(response)
13
+ return response if response.code < 400
14
+ return response if @ignore.include?(response.code)
15
+
16
+ raise HTTP::StatusError, response
17
+ end
18
+
19
+ HTTP::Options.register_feature(:raise_error, self)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ class Headers
5
+ class Normalizer
6
+ # Matches HTTP header names when in "Canonical-Http-Format"
7
+ CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/
8
+
9
+ # Matches valid header field name according to RFC.
10
+ # @see http://tools.ietf.org/html/rfc7230#section-3.2
11
+ COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/
12
+
13
+ NAME_PARTS_SEPARATOR_RE = /[\-_]/
14
+
15
+ # @private
16
+ # Normalized header names cache
17
+ class Cache
18
+ MAX_SIZE = 200
19
+
20
+ def initialize
21
+ @store = {}
22
+ end
23
+
24
+ def get(key)
25
+ @store[key]
26
+ end
27
+ alias [] get
28
+
29
+ def set(key, value)
30
+ # Maintain cache size
31
+ @store.delete(@store.each_key.first) while MAX_SIZE <= @store.size
32
+
33
+ @store[key] = value
34
+ end
35
+ alias []= set
36
+ end
37
+
38
+ def initialize
39
+ @cache = Cache.new
40
+ end
41
+
42
+ # Transforms `name` to canonical HTTP header capitalization
43
+ def call(name)
44
+ name = -name.to_s
45
+ value = (@cache[name] ||= -normalize_header(name))
46
+
47
+ value.dup
48
+ end
49
+
50
+ private
51
+
52
+ # Transforms `name` to canonical HTTP header capitalization
53
+ #
54
+ # @param [String] name
55
+ # @raise [HeaderError] if normalized name does not
56
+ # match {COMPLIANT_NAME_RE}
57
+ # @return [String] canonical HTTP header name
58
+ def normalize_header(name)
59
+ return name if CANONICAL_NAME_RE.match?(name)
60
+
61
+ normalized = name.split(NAME_PARTS_SEPARATOR_RE).each(&:capitalize!).join("-")
62
+
63
+ return normalized if COMPLIANT_NAME_RE.match?(normalized)
64
+
65
+ raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
66
+ end
67
+ end
68
+ end
69
+ end