http 5.0.0.pre2 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +7 -124
- data/.rubocop_todo.yml +192 -0
- data/CHANGES.md +114 -1
- data/Gemfile +18 -11
- data/LICENSE.txt +1 -1
- data/README.md +13 -16
- data/Rakefile +2 -10
- data/http.gemspec +3 -3
- data/lib/http/chainable.rb +15 -14
- data/lib/http/client.rb +26 -15
- data/lib/http/connection.rb +7 -3
- data/lib/http/content_type.rb +10 -5
- data/lib/http/feature.rb +1 -1
- data/lib/http/features/auto_inflate.rb +0 -2
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +3 -3
- data/lib/http/mime_type/adapter.rb +2 -0
- data/lib/http/options.rb +2 -2
- data/lib/http/redirector.rb +1 -1
- data/lib/http/request/writer.rb +5 -1
- data/lib/http/request.rb +22 -5
- data/lib/http/response/body.rb +5 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +74 -62
- data/lib/http/response/status.rb +2 -2
- data/lib/http/response.rb +22 -4
- data/lib/http/timeout/global.rb +41 -35
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +56 -59
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +109 -41
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -1
- data/spec/lib/http/features/instrumentation_spec.rb +21 -16
- data/spec/lib/http/features/logging_spec.rb +2 -5
- data/spec/lib/http/headers_spec.rb +3 -3
- data/spec/lib/http/redirector_spec.rb +44 -0
- data/spec/lib/http/request/writer_spec.rb +12 -1
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +30 -1
- data/spec/lib/http/response_spec.rb +62 -10
- data/spec/lib/http_spec.rb +20 -2
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +14 -2
- data/spec/support/dummy_server.rb +1 -1
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/simplecov.rb +19 -0
- metadata +23 -17
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -38
data/lib/http/connection.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
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
|
@@ -68,8 +67,13 @@ module HTTP
|
|
68
67
|
# @param [Request] req Request to send to the server
|
69
68
|
# @return [nil]
|
70
69
|
def send_request(req)
|
71
|
-
|
72
|
-
|
70
|
+
if @pending_response
|
71
|
+
raise StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
|
72
|
+
end
|
73
|
+
|
74
|
+
if @pending_request
|
75
|
+
raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
|
76
|
+
end
|
73
77
|
|
74
78
|
@pending_request = true
|
75
79
|
|
data/lib/http/content_type.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HTTP
|
4
|
-
ContentType
|
4
|
+
class ContentType
|
5
5
|
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
|
6
6
|
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
|
7
7
|
|
8
|
+
attr_accessor :mime_type, :charset
|
9
|
+
|
8
10
|
class << self
|
9
11
|
# Parse string and return ContentType struct
|
10
12
|
def parse(str)
|
@@ -15,15 +17,18 @@ module HTTP
|
|
15
17
|
|
16
18
|
# :nodoc:
|
17
19
|
def mime_type(str)
|
18
|
-
|
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
|
-
|
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/feature.rb
CHANGED
@@ -29,7 +29,7 @@ module HTTP
|
|
29
29
|
def wrap_request(request)
|
30
30
|
# Emit a separate "start" event, so a logger can print the request
|
31
31
|
# being run without waiting for a response
|
32
|
-
instrumenter.instrument("start_#{name}", :request => request)
|
32
|
+
instrumenter.instrument("start_#{name}", :request => request)
|
33
33
|
instrumenter.start(name, :request => request)
|
34
34
|
request
|
35
35
|
end
|
@@ -9,6 +9,20 @@ module HTTP
|
|
9
9
|
# HTTP.use(logging: {logger: Logger.new(STDOUT)}).get("https://example.com/")
|
10
10
|
#
|
11
11
|
class Logging < Feature
|
12
|
+
HTTP::Options.register_feature(:logging, self)
|
13
|
+
|
14
|
+
class NullLogger
|
15
|
+
%w[fatal error warn info debug].each do |level|
|
16
|
+
define_method(level.to_sym) do |*_args|
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
define_method(:"#{level}?") do
|
21
|
+
true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
12
26
|
attr_reader :logger
|
13
27
|
|
14
28
|
def initialize(logger: NullLogger.new)
|
@@ -17,39 +31,23 @@ module HTTP
|
|
17
31
|
|
18
32
|
def wrap_request(request)
|
19
33
|
logger.info { "> #{request.verb.to_s.upcase} #{request.uri}" }
|
20
|
-
logger.debug
|
21
|
-
headers = request.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
22
|
-
body = request.body.source
|
34
|
+
logger.debug { "#{stringify_headers(request.headers)}\n\n#{request.body.source}" }
|
23
35
|
|
24
|
-
headers + "\n\n" + body.to_s
|
25
|
-
end
|
26
36
|
request
|
27
37
|
end
|
28
38
|
|
29
39
|
def wrap_response(response)
|
30
40
|
logger.info { "< #{response.status}" }
|
31
|
-
logger.debug
|
32
|
-
headers = response.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
33
|
-
body = response.body.to_s
|
41
|
+
logger.debug { "#{stringify_headers(response.headers)}\n\n#{response.body}" }
|
34
42
|
|
35
|
-
headers + "\n\n" + body
|
36
|
-
end
|
37
43
|
response
|
38
44
|
end
|
39
45
|
|
40
|
-
|
41
|
-
%w[fatal error warn info debug].each do |level|
|
42
|
-
define_method(level.to_sym) do |*_args|
|
43
|
-
nil
|
44
|
-
end
|
46
|
+
private
|
45
47
|
|
46
|
-
|
47
|
-
|
48
|
-
end
|
49
|
-
end
|
48
|
+
def stringify_headers(headers)
|
49
|
+
headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
50
50
|
end
|
51
|
-
|
52
|
-
HTTP::Options.register_feature(:logging, self)
|
53
51
|
end
|
54
52
|
end
|
55
53
|
end
|
data/lib/http/headers.rb
CHANGED
@@ -17,7 +17,7 @@ module HTTP
|
|
17
17
|
|
18
18
|
# Matches valid header field name according to RFC.
|
19
19
|
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
20
|
-
COMPLIANT_NAME_RE = /\A[A-Za-z0-9
|
20
|
+
COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/.freeze
|
21
21
|
|
22
22
|
# Class constructor.
|
23
23
|
def initialize
|
@@ -111,7 +111,7 @@ module HTTP
|
|
111
111
|
#
|
112
112
|
# @return [Hash]
|
113
113
|
def to_h
|
114
|
-
|
114
|
+
keys.map { |k| [k, self[k]] }.to_h
|
115
115
|
end
|
116
116
|
alias to_hash to_h
|
117
117
|
|
@@ -164,7 +164,7 @@ module HTTP
|
|
164
164
|
|
165
165
|
# @!method hash
|
166
166
|
# Compute a hash-code for this headers container.
|
167
|
-
# Two
|
167
|
+
# Two containers with the same content will have the same hash code.
|
168
168
|
#
|
169
169
|
# @see http://www.ruby-doc.org/core/Object.html#method-i-hash
|
170
170
|
# @return [Fixnum]
|
@@ -14,6 +14,7 @@ module HTTP
|
|
14
14
|
def_delegators :instance, :encode, :decode
|
15
15
|
end
|
16
16
|
|
17
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition
|
17
18
|
%w[encode decode].each do |operation|
|
18
19
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
19
20
|
def #{operation}(*)
|
@@ -21,6 +22,7 @@ module HTTP
|
|
21
22
|
end
|
22
23
|
RUBY
|
23
24
|
end
|
25
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/http/options.rb
CHANGED
@@ -32,7 +32,7 @@ module HTTP
|
|
32
32
|
|
33
33
|
def def_option(name, reader_only: false, &interpreter)
|
34
34
|
defined_options << name.to_sym
|
35
|
-
interpreter ||=
|
35
|
+
interpreter ||= ->(v) { v }
|
36
36
|
|
37
37
|
if reader_only
|
38
38
|
attr_reader name
|
@@ -47,7 +47,7 @@ module HTTP
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
def initialize(options = {})
|
50
|
+
def initialize(options = {})
|
51
51
|
defaults = {
|
52
52
|
:response => :auto,
|
53
53
|
:proxy => {},
|
data/lib/http/redirector.rb
CHANGED
@@ -39,7 +39,7 @@ module HTTP
|
|
39
39
|
# @param [Hash] opts
|
40
40
|
# @option opts [Boolean] :strict (true) redirector hops policy
|
41
41
|
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
|
42
|
-
def initialize(opts = {})
|
42
|
+
def initialize(opts = {})
|
43
43
|
@strict = opts.fetch(:strict, true)
|
44
44
|
@max_hops = opts.fetch(:max_hops, 5).to_i
|
45
45
|
end
|
data/lib/http/request/writer.rb
CHANGED
@@ -47,7 +47,11 @@ module HTTP
|
|
47
47
|
# Adds the headers to the header array for the given request body we are working
|
48
48
|
# with
|
49
49
|
def add_body_type_headers
|
50
|
-
return if @headers[Headers::CONTENT_LENGTH] || chunked?
|
50
|
+
return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
|
51
|
+
@body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
|
52
|
+
@request_header[0].start_with?("#{method} ")
|
53
|
+
end
|
54
|
+
)
|
51
55
|
|
52
56
|
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
|
53
57
|
end
|
data/lib/http/request.rb
CHANGED
@@ -46,7 +46,10 @@ module HTTP
|
|
46
46
|
:patch,
|
47
47
|
|
48
48
|
# draft-reschke-webdav-search: WebDAV Search
|
49
|
-
:search
|
49
|
+
:search,
|
50
|
+
|
51
|
+
# RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
|
52
|
+
:mkcalendar
|
50
53
|
].freeze
|
51
54
|
|
52
55
|
# Allowed schemes
|
@@ -101,12 +104,26 @@ module HTTP
|
|
101
104
|
headers = self.headers.dup
|
102
105
|
headers.delete(Headers::HOST)
|
103
106
|
|
107
|
+
new_body = body.source
|
108
|
+
if verb == :get
|
109
|
+
# request bodies should not always be resubmitted when following a redirect
|
110
|
+
# some servers will close the connection after receiving the request headers
|
111
|
+
# which may cause Errno::ECONNRESET: Connection reset by peer
|
112
|
+
# see https://github.com/httprb/http/issues/649
|
113
|
+
# new_body = Request::Body.new(nil)
|
114
|
+
new_body = nil
|
115
|
+
# the CONTENT_TYPE header causes problems if set on a get request w/ an empty body
|
116
|
+
# the server might assume that there should be content if it is set to multipart
|
117
|
+
# rack raises EmptyContentError if this happens
|
118
|
+
headers.delete(Headers::CONTENT_TYPE)
|
119
|
+
end
|
120
|
+
|
104
121
|
self.class.new(
|
105
122
|
:verb => verb,
|
106
123
|
:uri => @uri.join(uri),
|
107
124
|
:headers => headers,
|
108
125
|
:proxy => proxy,
|
109
|
-
:body =>
|
126
|
+
:body => new_body,
|
110
127
|
:version => version,
|
111
128
|
:uri_normalizer => uri_normalizer
|
112
129
|
)
|
@@ -213,7 +230,7 @@ module HTTP
|
|
213
230
|
|
214
231
|
# @return [String] Default host (with port if needed) header value.
|
215
232
|
def default_host_header_value
|
216
|
-
PORTS[@scheme]
|
233
|
+
PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
217
234
|
end
|
218
235
|
|
219
236
|
def prepare_body(body)
|
@@ -223,8 +240,8 @@ module HTTP
|
|
223
240
|
def prepare_headers(headers)
|
224
241
|
headers = HTTP::Headers.coerce(headers || {})
|
225
242
|
|
226
|
-
headers[Headers::HOST]
|
227
|
-
headers[Headers::USER_AGENT]
|
243
|
+
headers[Headers::HOST] ||= default_host_header_value
|
244
|
+
headers[Headers::USER_AGENT] ||= USER_AGENT
|
228
245
|
|
229
246
|
headers
|
230
247
|
end
|
data/lib/http/response/body.rb
CHANGED
@@ -28,7 +28,8 @@ module HTTP
|
|
28
28
|
def readpartial(*args)
|
29
29
|
stream!
|
30
30
|
chunk = @stream.readpartial(*args)
|
31
|
-
|
31
|
+
|
32
|
+
String.new(chunk, :encoding => @encoding) if chunk
|
32
33
|
end
|
33
34
|
|
34
35
|
# Iterate over the body, allowing it to be enumerable
|
@@ -46,11 +47,11 @@ module HTTP
|
|
46
47
|
|
47
48
|
begin
|
48
49
|
@streaming = false
|
49
|
-
@contents = String.new(""
|
50
|
+
@contents = String.new("", :encoding => @encoding)
|
50
51
|
|
51
52
|
while (chunk = @stream.readpartial)
|
52
|
-
@contents <<
|
53
|
-
chunk
|
53
|
+
@contents << String.new(chunk, :encoding => @encoding)
|
54
|
+
chunk = nil # deallocate string
|
54
55
|
end
|
55
56
|
rescue
|
56
57
|
@contents = nil
|
data/lib/http/response/parser.rb
CHANGED
@@ -1,69 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "llhttp"
|
4
4
|
|
5
5
|
module HTTP
|
6
6
|
class Response
|
7
7
|
# @api private
|
8
|
-
#
|
9
|
-
# NOTE(ixti): This class is a subject of future refactoring, thus don't
|
10
|
-
# expect this class API to be stable until this message disappears and
|
11
|
-
# class is not marked as private anymore.
|
12
8
|
class Parser
|
13
|
-
attr_reader :headers
|
9
|
+
attr_reader :parser, :headers, :status_code, :http_version
|
14
10
|
|
15
11
|
def initialize
|
16
|
-
@
|
17
|
-
@parser =
|
18
|
-
|
12
|
+
@handler = Handler.new(self)
|
13
|
+
@parser = LLHttp::Parser.new(@handler, :type => :response)
|
19
14
|
reset
|
20
15
|
end
|
21
16
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
raise IOError, "Could not parse data"
|
17
|
+
def reset
|
18
|
+
@parser.reset
|
19
|
+
@handler.reset
|
20
|
+
@header_finished = false
|
21
|
+
@message_finished = false
|
22
|
+
@headers = Headers.new
|
23
|
+
@chunk = nil
|
24
|
+
@status_code = nil
|
25
|
+
@http_version = nil
|
32
26
|
end
|
33
|
-
alias << add
|
34
27
|
|
35
|
-
def
|
36
|
-
|
37
|
-
end
|
28
|
+
def add(data)
|
29
|
+
parser << data
|
38
30
|
|
39
|
-
|
40
|
-
|
31
|
+
self
|
32
|
+
rescue LLHttp::Error => e
|
33
|
+
raise IOError, e.message
|
41
34
|
end
|
42
35
|
|
43
|
-
|
44
|
-
|
36
|
+
alias << add
|
37
|
+
|
38
|
+
def mark_header_finished
|
39
|
+
@header_finished = true
|
40
|
+
@status_code = @parser.status_code
|
41
|
+
@http_version = "#{@parser.http_major}.#{@parser.http_minor}"
|
45
42
|
end
|
46
43
|
|
47
|
-
|
48
|
-
|
49
|
-
|
44
|
+
def headers?
|
45
|
+
@header_finished
|
46
|
+
end
|
50
47
|
|
51
|
-
def
|
52
|
-
|
53
|
-
@field << field
|
48
|
+
def add_header(name, value)
|
49
|
+
@headers.add(name, value)
|
54
50
|
end
|
55
51
|
|
56
|
-
def
|
57
|
-
@
|
58
|
-
@field_value << value
|
52
|
+
def mark_message_finished
|
53
|
+
@message_finished = true
|
59
54
|
end
|
60
55
|
|
61
|
-
def
|
62
|
-
|
63
|
-
@finished[:headers] = true
|
56
|
+
def finished?
|
57
|
+
@message_finished
|
64
58
|
end
|
65
59
|
|
66
|
-
def
|
60
|
+
def add_body(chunk)
|
67
61
|
if @chunk
|
68
62
|
@chunk << chunk
|
69
63
|
else
|
@@ -85,32 +79,50 @@ module HTTP
|
|
85
79
|
chunk
|
86
80
|
end
|
87
81
|
|
88
|
-
|
89
|
-
|
90
|
-
|
82
|
+
class Handler < LLHttp::Delegate
|
83
|
+
def initialize(target)
|
84
|
+
@target = target
|
85
|
+
super()
|
86
|
+
reset
|
87
|
+
end
|
91
88
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
@reading_header_value = false
|
98
|
-
@field = +""
|
99
|
-
@field_value = +""
|
100
|
-
@chunk = nil
|
101
|
-
end
|
89
|
+
def reset
|
90
|
+
@reading_header_value = false
|
91
|
+
@field_value = +""
|
92
|
+
@field = +""
|
93
|
+
end
|
102
94
|
|
103
|
-
|
104
|
-
|
105
|
-
|
95
|
+
def on_header_field(field)
|
96
|
+
append_header if @reading_header_value
|
97
|
+
@field << field
|
98
|
+
end
|
99
|
+
|
100
|
+
def on_header_value(value)
|
101
|
+
@reading_header_value = true
|
102
|
+
@field_value << value
|
103
|
+
end
|
106
104
|
|
107
|
-
|
105
|
+
def on_headers_complete
|
106
|
+
append_header if @reading_header_value
|
107
|
+
@target.mark_header_finished
|
108
|
+
end
|
109
|
+
|
110
|
+
def on_body(body)
|
111
|
+
@target.add_body(body)
|
112
|
+
end
|
108
113
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
+
def on_message_complete
|
115
|
+
@target.mark_message_finished
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def append_header
|
121
|
+
@target.add_header(@field, @field_value)
|
122
|
+
@reading_header_value = false
|
123
|
+
@field_value = +""
|
124
|
+
@field = +""
|
125
|
+
end
|
114
126
|
end
|
115
127
|
end
|
116
128
|
end
|