http 3.3.0 → 4.4.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +3 -1
- data/.travis.yml +10 -7
- data/CHANGES.md +135 -0
- data/README.md +14 -10
- data/Rakefile +1 -1
- data/http.gemspec +12 -5
- data/lib/http.rb +1 -2
- data/lib/http/chainable.rb +20 -29
- data/lib/http/client.rb +25 -19
- data/lib/http/connection.rb +5 -9
- data/lib/http/feature.rb +14 -0
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +33 -6
- data/lib/http/features/instrumentation.rb +64 -0
- data/lib/http/features/logging.rb +55 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/options.rb +27 -21
- data/lib/http/redirector.rb +2 -1
- data/lib/http/request.rb +38 -30
- data/lib/http/request/body.rb +30 -1
- data/lib/http/request/writer.rb +21 -7
- data/lib/http/response.rb +7 -15
- data/lib/http/response/parser.rb +56 -16
- data/lib/http/timeout/global.rb +12 -14
- data/lib/http/timeout/per_operation.rb +5 -7
- data/lib/http/uri.rb +13 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +34 -7
- data/spec/lib/http/features/auto_inflate_spec.rb +38 -22
- data/spec/lib/http/features/instrumentation_spec.rb +56 -0
- data/spec/lib/http/features/logging_spec.rb +67 -0
- data/spec/lib/http/redirector_spec.rb +13 -0
- data/spec/lib/http/request/body_spec.rb +51 -0
- data/spec/lib/http/request/writer_spec.rb +20 -0
- data/spec/lib/http/request_spec.rb +6 -0
- data/spec/lib/http/response/parser_spec.rb +45 -0
- data/spec/lib/http/response_spec.rb +3 -4
- data/spec/lib/http_spec.rb +45 -65
- data/spec/regression_specs.rb +7 -0
- data/spec/support/dummy_server/servlet.rb +5 -0
- data/spec/support/http_handling_shared.rb +60 -64
- metadata +32 -21
- data/.ruby-version +0 -1
data/lib/http/request.rb
CHANGED
@@ -66,6 +66,8 @@ module HTTP
|
|
66
66
|
# Scheme is normalized to be a lowercase symbol e.g. :http, :https
|
67
67
|
attr_reader :scheme
|
68
68
|
|
69
|
+
attr_reader :uri_normalizer
|
70
|
+
|
69
71
|
# "Request URI" as per RFC 2616
|
70
72
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
|
71
73
|
attr_reader :uri
|
@@ -73,25 +75,25 @@ module HTTP
|
|
73
75
|
|
74
76
|
# @option opts [String] :version
|
75
77
|
# @option opts [#to_s] :verb HTTP request method
|
78
|
+
# @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER)
|
76
79
|
# @option opts [HTTP::URI, #to_s] :uri
|
77
80
|
# @option opts [Hash] :headers
|
78
81
|
# @option opts [Hash] :proxy
|
79
82
|
# @option opts [String, Enumerable, IO, nil] :body
|
80
83
|
def initialize(opts)
|
81
|
-
@verb
|
82
|
-
@
|
84
|
+
@verb = opts.fetch(:verb).to_s.downcase.to_sym
|
85
|
+
@uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER
|
86
|
+
|
87
|
+
@uri = @uri_normalizer.call(opts.fetch(:uri))
|
83
88
|
@scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
|
84
89
|
|
85
90
|
raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
86
91
|
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
87
92
|
|
88
93
|
@proxy = opts[:proxy] || {}
|
89
|
-
@body = request_body(opts[:body], opts)
|
90
94
|
@version = opts[:version] || "1.1"
|
91
|
-
@headers =
|
92
|
-
|
93
|
-
@headers[Headers::HOST] ||= default_host_header_value
|
94
|
-
@headers[Headers::USER_AGENT] ||= USER_AGENT
|
95
|
+
@headers = prepare_headers(opts[:headers])
|
96
|
+
@body = prepare_body(opts[:body])
|
95
97
|
end
|
96
98
|
|
97
99
|
# Returns new Request with updated uri
|
@@ -100,12 +102,13 @@ module HTTP
|
|
100
102
|
headers.delete(Headers::HOST)
|
101
103
|
|
102
104
|
self.class.new(
|
103
|
-
:verb
|
104
|
-
:uri
|
105
|
-
:headers
|
106
|
-
:proxy
|
107
|
-
:body
|
108
|
-
:version
|
105
|
+
:verb => verb,
|
106
|
+
:uri => @uri.join(uri),
|
107
|
+
:headers => headers,
|
108
|
+
:proxy => proxy,
|
109
|
+
:body => body.source,
|
110
|
+
:version => version,
|
111
|
+
:uri_normalizer => uri_normalizer
|
109
112
|
)
|
110
113
|
end
|
111
114
|
|
@@ -184,15 +187,20 @@ module HTTP
|
|
184
187
|
using_proxy? ? proxy[:proxy_port] : port
|
185
188
|
end
|
186
189
|
|
187
|
-
|
188
|
-
|
189
|
-
#
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
190
|
+
# Human-readable representation of base request info.
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
#
|
194
|
+
# req.inspect
|
195
|
+
# # => #<HTTP::Request/1.1 GET https://example.com>
|
196
|
+
#
|
197
|
+
# @return [String]
|
198
|
+
def inspect
|
199
|
+
"#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>"
|
194
200
|
end
|
195
201
|
|
202
|
+
private
|
203
|
+
|
196
204
|
# @!attribute [r] host
|
197
205
|
# @return [String]
|
198
206
|
def_delegator :@uri, :host
|
@@ -208,17 +216,17 @@ module HTTP
|
|
208
216
|
PORTS[@scheme] != port ? "#{host}:#{port}" : host
|
209
217
|
end
|
210
218
|
|
211
|
-
|
212
|
-
|
213
|
-
|
219
|
+
def prepare_body(body)
|
220
|
+
body.is_a?(Request::Body) ? body : Request::Body.new(body)
|
221
|
+
end
|
214
222
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
223
|
+
def prepare_headers(headers)
|
224
|
+
headers = HTTP::Headers.coerce(headers || {})
|
225
|
+
|
226
|
+
headers[Headers::HOST] ||= default_host_header_value
|
227
|
+
headers[Headers::USER_AGENT] ||= USER_AGENT
|
228
|
+
|
229
|
+
headers
|
222
230
|
end
|
223
231
|
end
|
224
232
|
end
|
data/lib/http/request/body.rb
CHANGED
@@ -35,14 +35,43 @@ module HTTP
|
|
35
35
|
yield @source
|
36
36
|
elsif @source.respond_to?(:read)
|
37
37
|
IO.copy_stream(@source, ProcIO.new(block))
|
38
|
-
|
38
|
+
rewind(@source)
|
39
39
|
elsif @source.is_a?(Enumerable)
|
40
40
|
@source.each(&block)
|
41
41
|
end
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Request bodies are equivalent when they have the same source.
|
47
|
+
def ==(other)
|
48
|
+
self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
|
42
49
|
end
|
43
50
|
|
44
51
|
private
|
45
52
|
|
53
|
+
def rewind(io)
|
54
|
+
io.rewind if io.respond_to? :rewind
|
55
|
+
rescue Errno::ESPIPE, Errno::EPIPE
|
56
|
+
# Pipe IOs respond to `:rewind` but fail when you call it.
|
57
|
+
#
|
58
|
+
# Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
|
59
|
+
# but *EPIPE* on jRuby.
|
60
|
+
#
|
61
|
+
# - **ESPIPE** -- "Illegal seek."
|
62
|
+
# Invalid seek operation (such as on a pipe).
|
63
|
+
#
|
64
|
+
# - **EPIPE** -- "Broken pipe."
|
65
|
+
# There is no process reading from the other end of a pipe. Every
|
66
|
+
# library function that returns this error code also generates
|
67
|
+
# a SIGPIPE signal; this signal terminates the program if not handled
|
68
|
+
# or blocked. Thus, your program will never actually see EPIPE unless
|
69
|
+
# it has handled or blocked SIGPIPE.
|
70
|
+
#
|
71
|
+
# See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
46
75
|
def validate_source_type!
|
47
76
|
return if @source.is_a?(String)
|
48
77
|
return if @source.respond_to?(:read)
|
data/lib/http/request/writer.rb
CHANGED
@@ -60,22 +60,32 @@ module HTTP
|
|
60
60
|
@request_header.join(CRLF) + CRLF * 2
|
61
61
|
end
|
62
62
|
|
63
|
+
# Writes HTTP request data into the socket.
|
63
64
|
def send_request
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
|
65
|
+
each_chunk { |chunk| write chunk }
|
66
|
+
rescue Errno::EPIPE
|
67
|
+
# server doesn't need any more data
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Yields chunks of request data that should be sent to the socket.
|
72
|
+
#
|
73
|
+
# It's important to send the request in a single write call when possible
|
74
|
+
# in order to play nicely with Nagle's algorithm. Making two writes in a
|
75
|
+
# row triggers a pathological case where Nagle is expecting a third write
|
76
|
+
# that never happens.
|
77
|
+
def each_chunk
|
68
78
|
data = join_headers
|
69
79
|
|
70
80
|
@body.each do |chunk|
|
71
81
|
data << encode_chunk(chunk)
|
72
|
-
|
82
|
+
yield data
|
73
83
|
data.clear
|
74
84
|
end
|
75
85
|
|
76
|
-
|
86
|
+
yield data unless data.empty?
|
77
87
|
|
78
|
-
|
88
|
+
yield CHUNKED_END if chunked?
|
79
89
|
end
|
80
90
|
|
81
91
|
# Returns the chunk encoded for to the specified "Transfer-Encoding" header.
|
@@ -100,6 +110,10 @@ module HTTP
|
|
100
110
|
break unless data.bytesize > length
|
101
111
|
data = data.byteslice(length..-1)
|
102
112
|
end
|
113
|
+
rescue Errno::EPIPE
|
114
|
+
raise
|
115
|
+
rescue IOError, SocketError, SystemCallError => ex
|
116
|
+
raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace
|
103
117
|
end
|
104
118
|
end
|
105
119
|
end
|
data/lib/http/response.rb
CHANGED
@@ -20,6 +20,9 @@ module HTTP
|
|
20
20
|
# @return [Status]
|
21
21
|
attr_reader :status
|
22
22
|
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :version
|
25
|
+
|
23
26
|
# @return [Body]
|
24
27
|
attr_reader :body
|
25
28
|
|
@@ -46,14 +49,13 @@ module HTTP
|
|
46
49
|
@headers = HTTP::Headers.coerce(opts[:headers] || {})
|
47
50
|
@proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
|
48
51
|
|
49
|
-
if opts.include?(:
|
52
|
+
if opts.include?(:body)
|
53
|
+
@body = opts.fetch(:body)
|
54
|
+
else
|
50
55
|
connection = opts.fetch(:connection)
|
51
56
|
encoding = opts[:encoding] || charset || Encoding::BINARY
|
52
|
-
stream = body_stream_for(connection, opts)
|
53
57
|
|
54
|
-
@body = Response::Body.new(
|
55
|
-
else
|
56
|
-
@body = opts.fetch(:body)
|
58
|
+
@body = Response::Body.new(connection, :encoding => encoding)
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -160,15 +162,5 @@ module HTTP
|
|
160
162
|
def inspect
|
161
163
|
"#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
|
162
164
|
end
|
163
|
-
|
164
|
-
private
|
165
|
-
|
166
|
-
def body_stream_for(connection, opts)
|
167
|
-
if opts[:auto_inflate]
|
168
|
-
opts[:auto_inflate].stream_for(connection, self)
|
169
|
-
else
|
170
|
-
connection
|
171
|
-
end
|
172
|
-
end
|
173
165
|
end
|
174
166
|
end
|
data/lib/http/response/parser.rb
CHANGED
@@ -1,41 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "http-parser"
|
4
|
+
|
3
5
|
module HTTP
|
4
6
|
class Response
|
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.
|
5
12
|
class Parser
|
6
13
|
attr_reader :headers
|
7
14
|
|
8
15
|
def initialize
|
9
|
-
@
|
16
|
+
@state = HttpParser::Parser.new_instance { |i| i.type = :response }
|
17
|
+
@parser = HttpParser::Parser.new(self)
|
18
|
+
|
10
19
|
reset
|
11
20
|
end
|
12
21
|
|
22
|
+
# @return [self]
|
13
23
|
def add(data)
|
14
|
-
|
24
|
+
# XXX(ixti): API doc of HttpParser::Parser is misleading, it says that
|
25
|
+
# it returns boolean true if data was parsed successfully, but instead
|
26
|
+
# it's response tells if there was an error; So when it's `true` that
|
27
|
+
# means parse failed, and `false` means parse was successful.
|
28
|
+
# case of success.
|
29
|
+
return self unless @parser.parse(@state, data)
|
30
|
+
|
31
|
+
raise IOError, "Could not parse data"
|
15
32
|
end
|
16
33
|
alias << add
|
17
34
|
|
18
35
|
def headers?
|
19
|
-
|
36
|
+
@finished[:headers]
|
20
37
|
end
|
21
38
|
|
22
39
|
def http_version
|
23
|
-
@
|
40
|
+
@state.http_version
|
24
41
|
end
|
25
42
|
|
26
43
|
def status_code
|
27
|
-
@
|
44
|
+
@state.http_status
|
28
45
|
end
|
29
46
|
|
30
47
|
#
|
31
48
|
# HTTP::Parser callbacks
|
32
49
|
#
|
33
50
|
|
34
|
-
def
|
35
|
-
|
51
|
+
def on_header_field(_response, field)
|
52
|
+
append_header if @reading_header_value
|
53
|
+
@field << field
|
54
|
+
end
|
55
|
+
|
56
|
+
def on_header_value(_response, value)
|
57
|
+
@reading_header_value = true
|
58
|
+
@field_value << value
|
59
|
+
end
|
60
|
+
|
61
|
+
def on_headers_complete(_reposse)
|
62
|
+
append_header if @reading_header_value
|
63
|
+
@finished[:headers] = true
|
36
64
|
end
|
37
65
|
|
38
|
-
def on_body(chunk)
|
66
|
+
def on_body(_response, chunk)
|
39
67
|
if @chunk
|
40
68
|
@chunk << chunk
|
41
69
|
else
|
@@ -57,20 +85,32 @@ module HTTP
|
|
57
85
|
chunk
|
58
86
|
end
|
59
87
|
|
60
|
-
def on_message_complete
|
61
|
-
@finished = true
|
88
|
+
def on_message_complete(_response)
|
89
|
+
@finished[:message] = true
|
62
90
|
end
|
63
91
|
|
64
92
|
def reset
|
65
|
-
@
|
66
|
-
|
67
|
-
@finished
|
68
|
-
@headers
|
69
|
-
@
|
93
|
+
@state.reset!
|
94
|
+
|
95
|
+
@finished = Hash.new(false)
|
96
|
+
@headers = HTTP::Headers.new
|
97
|
+
@reading_header_value = false
|
98
|
+
@field = +""
|
99
|
+
@field_value = +""
|
100
|
+
@chunk = nil
|
70
101
|
end
|
71
102
|
|
72
103
|
def finished?
|
73
|
-
@finished
|
104
|
+
@finished[:message]
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def append_header
|
110
|
+
@headers.add(@field, @field_value)
|
111
|
+
@reading_header_value = false
|
112
|
+
@field_value = +""
|
113
|
+
@field = +""
|
74
114
|
end
|
75
115
|
end
|
76
116
|
end
|
data/lib/http/timeout/global.rb
CHANGED
@@ -3,27 +3,25 @@
|
|
3
3
|
require "timeout"
|
4
4
|
require "io/wait"
|
5
5
|
|
6
|
-
require "http/timeout/
|
6
|
+
require "http/timeout/null"
|
7
7
|
|
8
8
|
module HTTP
|
9
9
|
module Timeout
|
10
|
-
class Global <
|
11
|
-
attr_reader :time_left, :total_timeout
|
12
|
-
|
10
|
+
class Global < Null
|
13
11
|
def initialize(*args)
|
14
12
|
super
|
15
|
-
|
13
|
+
|
14
|
+
@timeout = @time_left = options.fetch(:global_timeout)
|
16
15
|
end
|
17
16
|
|
18
17
|
# To future me: Don't remove this again, past you was smarter.
|
19
18
|
def reset_counter
|
20
|
-
@time_left =
|
21
|
-
@total_timeout = time_left
|
19
|
+
@time_left = @timeout
|
22
20
|
end
|
23
21
|
|
24
22
|
def connect(socket_class, host, port, nodelay = false)
|
25
23
|
reset_timer
|
26
|
-
::Timeout.timeout(time_left, TimeoutError) do
|
24
|
+
::Timeout.timeout(@time_left, TimeoutError) do
|
27
25
|
@socket = socket_class.open(host, port)
|
28
26
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
29
27
|
end
|
@@ -37,11 +35,11 @@ module HTTP
|
|
37
35
|
begin
|
38
36
|
@socket.connect_nonblock
|
39
37
|
rescue IO::WaitReadable
|
40
|
-
IO.select([@socket], nil, nil, time_left)
|
38
|
+
IO.select([@socket], nil, nil, @time_left)
|
41
39
|
log_time
|
42
40
|
retry
|
43
41
|
rescue IO::WaitWritable
|
44
|
-
IO.select(nil, [@socket], nil, time_left)
|
42
|
+
IO.select(nil, [@socket], nil, @time_left)
|
45
43
|
log_time
|
46
44
|
retry
|
47
45
|
end
|
@@ -105,13 +103,13 @@ module HTTP
|
|
105
103
|
|
106
104
|
# Wait for a socket to become readable
|
107
105
|
def wait_readable_or_timeout
|
108
|
-
@socket.to_io.wait_readable(time_left)
|
106
|
+
@socket.to_io.wait_readable(@time_left)
|
109
107
|
log_time
|
110
108
|
end
|
111
109
|
|
112
110
|
# Wait for a socket to become writable
|
113
111
|
def wait_writable_or_timeout
|
114
|
-
@socket.to_io.wait_writable(time_left)
|
112
|
+
@socket.to_io.wait_writable(@time_left)
|
115
113
|
log_time
|
116
114
|
end
|
117
115
|
|
@@ -123,8 +121,8 @@ module HTTP
|
|
123
121
|
|
124
122
|
def log_time
|
125
123
|
@time_left -= (Time.now - @started)
|
126
|
-
if time_left <= 0
|
127
|
-
raise TimeoutError, "Timed out after using the allocated #{
|
124
|
+
if @time_left <= 0
|
125
|
+
raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
|
128
126
|
end
|
129
127
|
|
130
128
|
reset_timer
|