http 4.4.1 → 5.1.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 +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 +8 -110
- data/.rubocop_todo.yml +205 -0
- data/.yardopts +1 -1
- data/CHANGES.md +188 -3
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +47 -86
- data/Rakefile +2 -10
- data/SECURITY.md +5 -0
- data/http.gemspec +9 -8
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +44 -34
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +3 -0
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -7
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +5 -8
- data/lib/http/redirector.rb +51 -2
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +9 -4
- data/lib/http/request.rb +28 -11
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +74 -62
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +44 -18
- data/lib/http/timeout/global.rb +20 -36
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +32 -55
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +153 -30
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
- data/spec/lib/http/features/instrumentation_spec.rb +27 -21
- data/spec/lib/http/features/logging_spec.rb +8 -10
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +107 -3
- data/spec/lib/http/request/body_spec.rb +3 -3
- data/spec/lib/http/request/writer_spec.rb +25 -2
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +33 -4
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +80 -3
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +5 -5
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +23 -15
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
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
|
data/lib/http/response/status.rb
CHANGED
@@ -58,7 +58,7 @@ module HTTP
|
|
58
58
|
# SYMBOLS[418] # => :im_a_teapot
|
59
59
|
#
|
60
60
|
# @return [Hash<Fixnum => Symbol>]
|
61
|
-
SYMBOLS =
|
61
|
+
SYMBOLS = REASONS.transform_values { |v| symbolize(v) }.freeze
|
62
62
|
|
63
63
|
# Reversed {SYMBOLS} map.
|
64
64
|
#
|
@@ -69,7 +69,7 @@ module HTTP
|
|
69
69
|
# SYMBOL_CODES[:im_a_teapot] # => 418
|
70
70
|
#
|
71
71
|
# @return [Hash<Symbol => Fixnum>]
|
72
|
-
SYMBOL_CODES =
|
72
|
+
SYMBOL_CODES = SYMBOLS.to_h { |k, v| [v, k] }.freeze
|
73
73
|
|
74
74
|
# @return [Fixnum] status code
|
75
75
|
attr_reader :code
|
@@ -132,7 +132,7 @@ module HTTP
|
|
132
132
|
end
|
133
133
|
|
134
134
|
SYMBOLS.each do |code, symbol|
|
135
|
-
class_eval <<-RUBY, __FILE__, __LINE__
|
135
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
136
136
|
def #{symbol}? # def bad_request?
|
137
137
|
#{code} == code # 400 == code
|
138
138
|
end # end
|
@@ -141,6 +141,7 @@ module HTTP
|
|
141
141
|
|
142
142
|
def __setobj__(obj)
|
143
143
|
raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i
|
144
|
+
|
144
145
|
@code = obj.to_i
|
145
146
|
end
|
146
147
|
|
data/lib/http/response.rb
CHANGED
@@ -7,7 +7,6 @@ require "http/content_type"
|
|
7
7
|
require "http/mime_type"
|
8
8
|
require "http/response/status"
|
9
9
|
require "http/response/inflater"
|
10
|
-
require "http/uri"
|
11
10
|
require "http/cookie_jar"
|
12
11
|
require "time"
|
13
12
|
|
@@ -26,8 +25,8 @@ module HTTP
|
|
26
25
|
# @return [Body]
|
27
26
|
attr_reader :body
|
28
27
|
|
29
|
-
# @return [
|
30
|
-
attr_reader :
|
28
|
+
# @return [Request]
|
29
|
+
attr_reader :request
|
31
30
|
|
32
31
|
# @return [Hash]
|
33
32
|
attr_reader :proxy_headers
|
@@ -41,10 +40,11 @@ module HTTP
|
|
41
40
|
# @option opts [HTTP::Connection] :connection
|
42
41
|
# @option opts [String] :encoding Encoding to use when reading body
|
43
42
|
# @option opts [String] :body
|
44
|
-
# @option opts [
|
43
|
+
# @option opts [HTTP::Request] request The request this is in response to.
|
44
|
+
# @option opts [String] :uri (DEPRECATED) used to populate a missing request
|
45
45
|
def initialize(opts)
|
46
46
|
@version = opts.fetch(:version)
|
47
|
-
@
|
47
|
+
@request = init_request(opts)
|
48
48
|
@status = HTTP::Response::Status.new(opts.fetch(:status))
|
49
49
|
@headers = HTTP::Headers.coerce(opts[:headers] || {})
|
50
50
|
@proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
|
@@ -53,7 +53,7 @@ module HTTP
|
|
53
53
|
@body = opts.fetch(:body)
|
54
54
|
else
|
55
55
|
connection = opts.fetch(:connection)
|
56
|
-
encoding
|
56
|
+
encoding = opts[:encoding] || charset || default_encoding
|
57
57
|
|
58
58
|
@body = Response::Body.new(connection, :encoding => encoding)
|
59
59
|
end
|
@@ -61,24 +61,28 @@ module HTTP
|
|
61
61
|
|
62
62
|
# @!method reason
|
63
63
|
# @return (see HTTP::Response::Status#reason)
|
64
|
-
def_delegator
|
64
|
+
def_delegator :@status, :reason
|
65
65
|
|
66
66
|
# @!method code
|
67
67
|
# @return (see HTTP::Response::Status#code)
|
68
|
-
def_delegator
|
68
|
+
def_delegator :@status, :code
|
69
69
|
|
70
70
|
# @!method to_s
|
71
71
|
# (see HTTP::Response::Body#to_s)
|
72
|
-
def_delegator
|
72
|
+
def_delegator :@body, :to_s
|
73
73
|
alias to_str to_s
|
74
74
|
|
75
75
|
# @!method readpartial
|
76
76
|
# (see HTTP::Response::Body#readpartial)
|
77
|
-
def_delegator
|
77
|
+
def_delegator :@body, :readpartial
|
78
78
|
|
79
79
|
# @!method connection
|
80
80
|
# (see HTTP::Response::Body#connection)
|
81
|
-
def_delegator
|
81
|
+
def_delegator :@body, :connection
|
82
|
+
|
83
|
+
# @!method uri
|
84
|
+
# @return (see HTTP::Request#uri)
|
85
|
+
def_delegator :@request, :uri
|
82
86
|
|
83
87
|
# Returns an Array ala Rack: `[status, headers, body]`
|
84
88
|
#
|
@@ -134,8 +138,8 @@ module HTTP
|
|
134
138
|
def_delegator :content_type, :charset
|
135
139
|
|
136
140
|
def cookies
|
137
|
-
@cookies ||= headers.each_with_object CookieJar.new do |
|
138
|
-
jar.parse(v, uri)
|
141
|
+
@cookies ||= headers.get(Headers::SET_COOKIE).each_with_object CookieJar.new do |v, jar|
|
142
|
+
jar.parse(v, uri)
|
139
143
|
end
|
140
144
|
end
|
141
145
|
|
@@ -150,17 +154,39 @@ module HTTP
|
|
150
154
|
|
151
155
|
# Parse response body with corresponding MIME type adapter.
|
152
156
|
#
|
153
|
-
# @param [#to_s]
|
154
|
-
#
|
155
|
-
# @raise [HTTP::Error] if adapter not found
|
157
|
+
# @param type [#to_s] Parse as given MIME type.
|
158
|
+
# @raise (see MimeType.[])
|
156
159
|
# @return [Object]
|
157
|
-
def parse(
|
158
|
-
MimeType[
|
160
|
+
def parse(type = nil)
|
161
|
+
MimeType[type || mime_type].decode to_s
|
159
162
|
end
|
160
163
|
|
161
164
|
# Inspect a response
|
162
165
|
def inspect
|
163
166
|
"#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
|
164
167
|
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
def default_encoding
|
172
|
+
return Encoding::UTF_8 if mime_type == "application/json"
|
173
|
+
|
174
|
+
Encoding::BINARY
|
175
|
+
end
|
176
|
+
|
177
|
+
# Initialize an HTTP::Request from options.
|
178
|
+
#
|
179
|
+
# @return [HTTP::Request]
|
180
|
+
def init_request(opts)
|
181
|
+
raise ArgumentError, ":uri is for backwards compatibilty and conflicts with :request" \
|
182
|
+
if opts[:request] && opts[:uri]
|
183
|
+
|
184
|
+
# For backwards compatibilty
|
185
|
+
if opts[:uri]
|
186
|
+
HTTP::Request.new(:uri => opts[:uri], :verb => :get)
|
187
|
+
else
|
188
|
+
opts.fetch(:request)
|
189
|
+
end
|
190
|
+
end
|
165
191
|
end
|
166
192
|
end
|
data/lib/http/timeout/global.rb
CHANGED
@@ -21,7 +21,7 @@ module HTTP
|
|
21
21
|
|
22
22
|
def connect(socket_class, host, port, nodelay = false)
|
23
23
|
reset_timer
|
24
|
-
::Timeout.timeout(@time_left,
|
24
|
+
::Timeout.timeout(@time_left, ConnectTimeoutError) do
|
25
25
|
@socket = socket_class.open(host, port)
|
26
26
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
27
27
|
end
|
@@ -35,12 +35,10 @@ module HTTP
|
|
35
35
|
begin
|
36
36
|
@socket.connect_nonblock
|
37
37
|
rescue IO::WaitReadable
|
38
|
-
|
39
|
-
log_time
|
38
|
+
wait_readable_or_timeout
|
40
39
|
retry
|
41
40
|
rescue IO::WaitWritable
|
42
|
-
|
43
|
-
log_time
|
41
|
+
wait_writable_or_timeout
|
44
42
|
retry
|
45
43
|
end
|
46
44
|
end
|
@@ -59,22 +57,12 @@ module HTTP
|
|
59
57
|
|
60
58
|
private
|
61
59
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
def write_nonblock(data)
|
68
|
-
@socket.write_nonblock(data)
|
69
|
-
end
|
70
|
-
else
|
71
|
-
def read_nonblock(size, buffer = nil)
|
72
|
-
@socket.read_nonblock(size, buffer, :exception => false)
|
73
|
-
end
|
60
|
+
def read_nonblock(size, buffer = nil)
|
61
|
+
@socket.read_nonblock(size, buffer, :exception => false)
|
62
|
+
end
|
74
63
|
|
75
|
-
|
76
|
-
|
77
|
-
end
|
64
|
+
def write_nonblock(data)
|
65
|
+
@socket.write_nonblock(data, :exception => false)
|
78
66
|
end
|
79
67
|
|
80
68
|
# Perform the given I/O operation with the given argument
|
@@ -82,20 +70,18 @@ module HTTP
|
|
82
70
|
reset_timer
|
83
71
|
|
84
72
|
loop do
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
else return result
|
93
|
-
end
|
94
|
-
rescue IO::WaitReadable
|
95
|
-
wait_readable_or_timeout
|
96
|
-
rescue IO::WaitWritable
|
97
|
-
wait_writable_or_timeout
|
73
|
+
result = yield
|
74
|
+
|
75
|
+
case result
|
76
|
+
when :wait_readable then wait_readable_or_timeout
|
77
|
+
when :wait_writable then wait_writable_or_timeout
|
78
|
+
when NilClass then return :eof
|
79
|
+
else return result
|
98
80
|
end
|
81
|
+
rescue IO::WaitReadable
|
82
|
+
wait_readable_or_timeout
|
83
|
+
rescue IO::WaitWritable
|
84
|
+
wait_writable_or_timeout
|
99
85
|
end
|
100
86
|
rescue EOFError
|
101
87
|
:eof
|
@@ -121,9 +107,7 @@ module HTTP
|
|
121
107
|
|
122
108
|
def log_time
|
123
109
|
@time_left -= (Time.now - @started)
|
124
|
-
if @time_left <= 0
|
125
|
-
raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
|
126
|
-
end
|
110
|
+
raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
|
127
111
|
|
128
112
|
reset_timer
|
129
113
|
end
|
data/lib/http/timeout/null.rb
CHANGED
@@ -12,7 +12,7 @@ module HTTP
|
|
12
12
|
|
13
13
|
attr_reader :options, :socket
|
14
14
|
|
15
|
-
def initialize(options = {})
|
15
|
+
def initialize(options = {})
|
16
16
|
@options = options
|
17
17
|
end
|
18
18
|
|
@@ -36,6 +36,7 @@ module HTTP
|
|
36
36
|
connect_ssl
|
37
37
|
|
38
38
|
return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
39
|
+
return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
|
39
40
|
|
40
41
|
@socket.post_connection_check(host)
|
41
42
|
end
|
@@ -20,7 +20,7 @@ module HTTP
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def connect(socket_class, host, port, nodelay = false)
|
23
|
-
::Timeout.timeout(@connect_timeout,
|
23
|
+
::Timeout.timeout(@connect_timeout, ConnectTimeoutError) do
|
24
24
|
@socket = socket_class.open(host, port)
|
25
25
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
26
26
|
end
|
@@ -34,65 +34,42 @@ module HTTP
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
:eof
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# Read data from the socket
|
60
|
-
def readpartial(size, buffer = nil)
|
61
|
-
timeout = false
|
62
|
-
loop do
|
63
|
-
result = @socket.read_nonblock(size, buffer, :exception => false)
|
64
|
-
|
65
|
-
return :eof if result.nil?
|
66
|
-
return result if result != :wait_readable
|
67
|
-
|
68
|
-
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
69
|
-
# marking the socket for timeout. Why is this not being raised immediately?
|
70
|
-
# it seems there is some race-condition on the network level between calling
|
71
|
-
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
72
|
-
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
73
|
-
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
74
|
-
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
75
|
-
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
76
|
-
# timeout. Else, the first timeout was a proper timeout.
|
77
|
-
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
78
|
-
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
79
|
-
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
80
|
-
end
|
37
|
+
# Read data from the socket
|
38
|
+
def readpartial(size, buffer = nil)
|
39
|
+
timeout = false
|
40
|
+
loop do
|
41
|
+
result = @socket.read_nonblock(size, buffer, :exception => false)
|
42
|
+
|
43
|
+
return :eof if result.nil?
|
44
|
+
return result if result != :wait_readable
|
45
|
+
|
46
|
+
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
47
|
+
|
48
|
+
# marking the socket for timeout. Why is this not being raised immediately?
|
49
|
+
# it seems there is some race-condition on the network level between calling
|
50
|
+
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
51
|
+
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
52
|
+
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
53
|
+
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
54
|
+
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
55
|
+
# timeout. Else, the first timeout was a proper timeout.
|
56
|
+
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
57
|
+
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
58
|
+
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
81
59
|
end
|
60
|
+
end
|
82
61
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
62
|
+
# Write data to the socket
|
63
|
+
def write(data)
|
64
|
+
timeout = false
|
65
|
+
loop do
|
66
|
+
result = @socket.write_nonblock(data, :exception => false)
|
67
|
+
return result unless result == :wait_writable
|
89
68
|
|
90
|
-
|
69
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
91
70
|
|
92
|
-
|
93
|
-
end
|
71
|
+
timeout = true unless @socket.to_io.wait_writable(@write_timeout)
|
94
72
|
end
|
95
|
-
|
96
73
|
end
|
97
74
|
end
|
98
75
|
end
|
data/lib/http/uri.rb
CHANGED
@@ -31,11 +31,11 @@ module HTTP
|
|
31
31
|
uri = HTTP::URI.parse uri
|
32
32
|
|
33
33
|
HTTP::URI.new(
|
34
|
-
:scheme
|
35
|
-
:authority
|
36
|
-
:path
|
37
|
-
:query
|
38
|
-
:fragment
|
34
|
+
:scheme => uri.normalized_scheme,
|
35
|
+
:authority => uri.normalized_authority,
|
36
|
+
:path => uri.normalized_path,
|
37
|
+
:query => uri.query,
|
38
|
+
:fragment => uri.normalized_fragment
|
39
39
|
)
|
40
40
|
end
|
41
41
|
|
data/lib/http/version.rb
CHANGED