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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -24
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +57 -0
- data/{CHANGES.md → CHANGES_OLD.md} +1 -1
- data/Gemfile +1 -0
- data/README.md +4 -2
- data/SECURITY.md +13 -1
- data/http.gemspec +8 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/client.rb +1 -1
- data/lib/http/connection.rb +12 -3
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/instrumentation.rb +6 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +15 -5
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/null.rb +8 -5
- data/lib/http/uri.rb +18 -2
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/client_spec.rb +1 -0
- data/spec/lib/http/connection_spec.rb +23 -1
- data/spec/lib/http/features/instrumentation_spec.rb +19 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/options/headers_spec.rb +5 -1
- data/spec/lib/http/redirector_spec.rb +6 -5
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http_spec.rb +49 -3
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +19 -6
- data/spec/support/dummy_server.rb +2 -1
- metadata +28 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: da85aeba1d2d3dce86f59a678ade3f74df35514397cb7488f9aaab9ea5a57220
|
4
|
+
data.tar.gz: 28e5143890592f55f51fa65874f787f3c3e9eac5f9f6103c589082414eff378e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b14bb800aefa920d2511b962967253fc034847bb07f8d181bb9cec6dba136ca0c51655fd0da716986897be5ef83e6023705578016db7bbd12bbd1f2ed4bbeb4
|
7
|
+
data.tar.gz: dc996c20d358b382fbb2b1d93af7a706f858167392c8db3d8bf11e114419c0c7ed0a61350753f9c13422ca48b469af0b359f80694fe423066a02439066bfdd07
|
data/.github/workflows/ci.yml
CHANGED
@@ -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@
|
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@
|
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@
|
57
|
+
- uses: actions/checkout@v4
|
76
58
|
|
77
59
|
- uses: ruby/setup-ruby@v1
|
78
60
|
with:
|
data/.rubocop/rspec.yml
ADDED
data/.rubocop.yml
CHANGED
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
|
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:
|
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:
|
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:
|
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
|
data/Gemfile
CHANGED
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
|
-
-
|
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]:
|
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
|
-
|
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
|
-
|
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}/
|
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
|
data/lib/http/base64.rb
ADDED
data/lib/http/chainable.rb
CHANGED
@@ -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 #{
|
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
|
data/lib/http/connection.rb
CHANGED
@@ -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
|
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
|
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
|
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
|