http 5.0.0.pre → 5.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -1
- data/.travis.yml +6 -4
- data/CHANGES.md +83 -0
- data/Gemfile +2 -1
- data/README.md +7 -6
- data/http.gemspec +11 -4
- data/lib/http/chainable.rb +8 -3
- data/lib/http/client.rb +32 -34
- data/lib/http/connection.rb +5 -5
- data/lib/http/content_type.rb +2 -2
- data/lib/http/feature.rb +3 -0
- data/lib/http/features/auto_deflate.rb +13 -7
- data/lib/http/features/auto_inflate.rb +6 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/headers.rb +48 -11
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/mime_type/adapter.rb +1 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +4 -7
- data/lib/http/redirector.rb +3 -1
- data/lib/http/request.rb +32 -29
- data/lib/http/request/body.rb +26 -1
- data/lib/http/request/writer.rb +3 -2
- data/lib/http/response.rb +17 -15
- data/lib/http/response/body.rb +1 -0
- data/lib/http/response/parser.rb +20 -6
- data/lib/http/response/status.rb +2 -1
- data/lib/http/timeout/global.rb +1 -3
- data/lib/http/timeout/per_operation.rb +1 -0
- data/lib/http/uri.rb +13 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +96 -14
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
- data/spec/lib/http/features/instrumentation_spec.rb +7 -6
- data/spec/lib/http/features/logging_spec.rb +6 -5
- data/spec/lib/http/headers_spec.rb +52 -17
- 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 +15 -1
- data/spec/lib/http/request/body_spec.rb +22 -0
- data/spec/lib/http/request/writer_spec.rb +13 -1
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +45 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +11 -22
- data/spec/lib/http_spec.rb +30 -1
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server.rb +6 -6
- data/spec/support/dummy_server/servlet.rb +8 -4
- data/spec/support/http_handling_shared.rb +4 -4
- data/spec/support/ssl_helper.rb +4 -4
- metadata +23 -16
data/lib/http/content_type.rb
CHANGED
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module HTTP
|
4
4
|
ContentType = Struct.new(:mime_type, :charset) do
|
5
|
-
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
6
|
-
CHARSET_RE = /;\s*charset=([^;]+)/i
|
5
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}.freeze
|
6
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i.freeze
|
7
7
|
|
8
8
|
class << self
|
9
9
|
# Parse string and return ContentType struct
|
data/lib/http/feature.rb
CHANGED
@@ -13,6 +13,8 @@ module HTTP
|
|
13
13
|
def wrap_response(response)
|
14
14
|
response
|
15
15
|
end
|
16
|
+
|
17
|
+
def on_error(request, error); end
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
@@ -20,3 +22,4 @@ require "http/features/auto_inflate"
|
|
20
22
|
require "http/features/auto_deflate"
|
21
23
|
require "http/features/logging"
|
22
24
|
require "http/features/instrumentation"
|
25
|
+
require "http/features/normalize_uri"
|
@@ -10,7 +10,7 @@ module HTTP
|
|
10
10
|
class AutoDeflate < Feature
|
11
11
|
attr_reader :method
|
12
12
|
|
13
|
-
def initialize(
|
13
|
+
def initialize(**)
|
14
14
|
super
|
15
15
|
|
16
16
|
@method = @opts.key?(:method) ? @opts[:method].to_s : "gzip"
|
@@ -20,14 +20,20 @@ module HTTP
|
|
20
20
|
|
21
21
|
def wrap_request(request)
|
22
22
|
return request unless method
|
23
|
+
return request if request.body.size.zero?
|
24
|
+
|
25
|
+
# We need to delete Content-Length header. It will be set automatically by HTTP::Request::Writer
|
26
|
+
request.headers.delete(Headers::CONTENT_LENGTH)
|
27
|
+
request.headers[Headers::CONTENT_ENCODING] = method
|
23
28
|
|
24
29
|
Request.new(
|
25
|
-
:version
|
26
|
-
:verb
|
27
|
-
:uri
|
28
|
-
:headers
|
29
|
-
:proxy
|
30
|
-
:body
|
30
|
+
:version => request.version,
|
31
|
+
:verb => request.verb,
|
32
|
+
:uri => request.uri,
|
33
|
+
:headers => request.headers,
|
34
|
+
:proxy => request.proxy,
|
35
|
+
:body => deflated_body(request.body),
|
36
|
+
:uri_normalizer => request.uri_normalizer
|
31
37
|
)
|
32
38
|
end
|
33
39
|
|
@@ -12,12 +12,13 @@ module HTTP
|
|
12
12
|
return response unless supported_encoding?(response)
|
13
13
|
|
14
14
|
options = {
|
15
|
-
:status
|
16
|
-
:version
|
17
|
-
:headers
|
15
|
+
:status => response.status,
|
16
|
+
:version => response.version,
|
17
|
+
:headers => response.headers,
|
18
18
|
:proxy_headers => response.proxy_headers,
|
19
|
-
:connection
|
20
|
-
:body
|
19
|
+
:connection => response.connection,
|
20
|
+
:body => stream_for(response.connection),
|
21
|
+
:request => response.request
|
21
22
|
}
|
22
23
|
|
23
24
|
options[:uri] = response.uri if response.uri
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http/uri"
|
4
|
+
|
5
|
+
module HTTP
|
6
|
+
module Features
|
7
|
+
class NormalizeUri < Feature
|
8
|
+
attr_reader :normalizer
|
9
|
+
|
10
|
+
def initialize(normalizer: HTTP::URI::NORMALIZER)
|
11
|
+
@normalizer = normalizer
|
12
|
+
end
|
13
|
+
|
14
|
+
HTTP::Options.register_feature(:normalize_uri, self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/http/headers.rb
CHANGED
@@ -13,14 +13,18 @@ module HTTP
|
|
13
13
|
include Enumerable
|
14
14
|
|
15
15
|
# Matches HTTP header names when in "Canonical-Http-Format"
|
16
|
-
CANONICAL_NAME_RE =
|
16
|
+
CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/.freeze
|
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 =
|
20
|
+
COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#\$%&'*+\-.^_`|~]+\z/.freeze
|
21
21
|
|
22
22
|
# Class constructor.
|
23
23
|
def initialize
|
24
|
+
# The @pile stores each header value using a three element array:
|
25
|
+
# 0 - the normalized header key, used for lookup
|
26
|
+
# 1 - the header key as it will be sent with a request
|
27
|
+
# 2 - the value
|
24
28
|
@pile = []
|
25
29
|
end
|
26
30
|
|
@@ -45,12 +49,31 @@ module HTTP
|
|
45
49
|
|
46
50
|
# Appends header.
|
47
51
|
#
|
48
|
-
# @param [
|
52
|
+
# @param [String, Symbol] name header name. When specified as a string, the
|
53
|
+
# name is sent as-is. When specified as a symbol, the name is converted
|
54
|
+
# to a string of capitalized words separated by a dash. Word boundaries
|
55
|
+
# are determined by an underscore (`_`) or a dash (`-`).
|
56
|
+
# Ex: `:content_type` is sent as `"Content-Type"`, and `"auth_key"` (string)
|
57
|
+
# is sent as `"auth_key"`.
|
49
58
|
# @param [Array<#to_s>, #to_s] value header value(s) to be appended
|
50
59
|
# @return [void]
|
51
60
|
def add(name, value)
|
52
|
-
|
53
|
-
|
61
|
+
lookup_name = normalize_header(name.to_s)
|
62
|
+
wire_name = case name
|
63
|
+
when String
|
64
|
+
name
|
65
|
+
when Symbol
|
66
|
+
lookup_name
|
67
|
+
else
|
68
|
+
raise HTTP::HeaderError, "HTTP header must be a String or Symbol: #{name.inspect}"
|
69
|
+
end
|
70
|
+
Array(value).each do |v|
|
71
|
+
@pile << [
|
72
|
+
lookup_name,
|
73
|
+
wire_name,
|
74
|
+
validate_value(v)
|
75
|
+
]
|
76
|
+
end
|
54
77
|
end
|
55
78
|
|
56
79
|
# Returns list of header values if any.
|
@@ -58,7 +81,7 @@ module HTTP
|
|
58
81
|
# @return [Array<String>]
|
59
82
|
def get(name)
|
60
83
|
name = normalize_header name.to_s
|
61
|
-
@pile.select { |k, _| k == name }.map { |_, v| v }
|
84
|
+
@pile.select { |k, _| k == name }.map { |_, _, v| v }
|
62
85
|
end
|
63
86
|
|
64
87
|
# Smart version of {#get}.
|
@@ -96,7 +119,7 @@ module HTTP
|
|
96
119
|
#
|
97
120
|
# @return [Array<[String, String]>]
|
98
121
|
def to_a
|
99
|
-
@pile.map { |
|
122
|
+
@pile.map { |item| item[1..2] }
|
100
123
|
end
|
101
124
|
|
102
125
|
# Returns human-readable representation of `self` instance.
|
@@ -110,7 +133,7 @@ module HTTP
|
|
110
133
|
#
|
111
134
|
# @return [Array<String>]
|
112
135
|
def keys
|
113
|
-
@pile.map { |k, _| k }.uniq
|
136
|
+
@pile.map { |_, k, _| k }.uniq
|
114
137
|
end
|
115
138
|
|
116
139
|
# Compares headers to another Headers or Array of key/value pairs
|
@@ -118,7 +141,8 @@ module HTTP
|
|
118
141
|
# @return [Boolean]
|
119
142
|
def ==(other)
|
120
143
|
return false unless other.respond_to? :to_a
|
121
|
-
|
144
|
+
|
145
|
+
to_a == other.to_a
|
122
146
|
end
|
123
147
|
|
124
148
|
# Calls the given block once for each key/value pair in headers container.
|
@@ -127,7 +151,8 @@ module HTTP
|
|
127
151
|
# @return [Headers] self-reference
|
128
152
|
def each
|
129
153
|
return to_enum(__method__) unless block_given?
|
130
|
-
|
154
|
+
|
155
|
+
@pile.each { |item| yield(item[1..2]) }
|
131
156
|
self
|
132
157
|
end
|
133
158
|
|
@@ -150,7 +175,7 @@ module HTTP
|
|
150
175
|
# @api private
|
151
176
|
def initialize_copy(orig)
|
152
177
|
super
|
153
|
-
@pile =
|
178
|
+
@pile = @pile.map(&:dup)
|
154
179
|
end
|
155
180
|
|
156
181
|
# Merges `other` headers into `self`.
|
@@ -209,5 +234,17 @@ module HTTP
|
|
209
234
|
|
210
235
|
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
|
211
236
|
end
|
237
|
+
|
238
|
+
# Ensures there is no new line character in the header value
|
239
|
+
#
|
240
|
+
# @param [String] value
|
241
|
+
# @raise [HeaderError] if value includes new line character
|
242
|
+
# @return [String] stringified header value
|
243
|
+
def validate_value(value)
|
244
|
+
v = value.to_s
|
245
|
+
return v unless v.include?("\n")
|
246
|
+
|
247
|
+
raise HeaderError, "Invalid HTTP header field value: #{v.inspect}"
|
248
|
+
end
|
212
249
|
end
|
213
250
|
end
|
data/lib/http/headers/known.rb
CHANGED
@@ -5,6 +5,9 @@ module HTTP
|
|
5
5
|
# Content-Types that are acceptable for the response.
|
6
6
|
ACCEPT = "Accept"
|
7
7
|
|
8
|
+
# Content-codings that are acceptable in the response.
|
9
|
+
ACCEPT_ENCODING = "Accept-Encoding"
|
10
|
+
|
8
11
|
# The age the object has been in a proxy cache in seconds.
|
9
12
|
AGE = "Age"
|
10
13
|
|
data/lib/http/mime_type/json.rb
CHANGED
data/lib/http/options.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/ClassLength
|
4
|
-
|
5
3
|
require "http/headers"
|
6
4
|
require "openssl"
|
7
5
|
require "socket"
|
8
6
|
require "http/uri"
|
9
7
|
|
10
8
|
module HTTP
|
11
|
-
class Options
|
9
|
+
class Options # rubocop:disable Metrics/ClassLength
|
12
10
|
@default_socket_class = TCPSocket
|
13
11
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
14
12
|
@default_timeout_class = HTTP::Timeout::Null
|
@@ -18,9 +16,8 @@ module HTTP
|
|
18
16
|
attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
|
19
17
|
attr_reader :available_features
|
20
18
|
|
21
|
-
def new(options = {})
|
22
|
-
|
23
|
-
super
|
19
|
+
def new(options = {})
|
20
|
+
options.is_a?(self) ? options : super
|
24
21
|
end
|
25
22
|
|
26
23
|
def defined_options
|
@@ -114,7 +111,7 @@ module HTTP
|
|
114
111
|
unless (feature = self.class.available_features[name])
|
115
112
|
argument_error! "Unsupported feature: #{name}"
|
116
113
|
end
|
117
|
-
feature.new(opts_or_feature)
|
114
|
+
feature.new(**opts_or_feature)
|
118
115
|
end
|
119
116
|
end
|
120
117
|
end
|
data/lib/http/redirector.rb
CHANGED
@@ -58,7 +58,8 @@ module HTTP
|
|
58
58
|
|
59
59
|
@response.flush
|
60
60
|
|
61
|
-
|
61
|
+
# XXX(ixti): using `Array#inject` to return `nil` if no Location header.
|
62
|
+
@request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
|
62
63
|
@response = yield @request
|
63
64
|
end
|
64
65
|
|
@@ -89,6 +90,7 @@ module HTTP
|
|
89
90
|
|
90
91
|
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
91
92
|
raise StateError, "can't follow #{@response.status} redirect" if @strict
|
93
|
+
|
92
94
|
verb = :get
|
93
95
|
end
|
94
96
|
|
data/lib/http/request.rb
CHANGED
@@ -54,10 +54,10 @@ module HTTP
|
|
54
54
|
|
55
55
|
# Default ports of supported schemes
|
56
56
|
PORTS = {
|
57
|
-
:http
|
58
|
-
:https
|
59
|
-
:ws
|
60
|
-
:wss
|
57
|
+
:http => 80,
|
58
|
+
:https => 443,
|
59
|
+
:ws => 80,
|
60
|
+
:wss => 443
|
61
61
|
}.freeze
|
62
62
|
|
63
63
|
# Method is given as a lowercase symbol e.g. :get, :post
|
@@ -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 = (body = opts[:body]).is_a?(Request::Body) ? body : Request::Body.new(body)
|
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
|
|
@@ -165,8 +168,8 @@ module HTTP
|
|
165
168
|
# Headers to send with proxy connect request
|
166
169
|
def proxy_connect_headers
|
167
170
|
connect_headers = HTTP::Headers.coerce(
|
168
|
-
Headers::HOST
|
169
|
-
Headers::USER_AGENT
|
171
|
+
Headers::HOST => headers[Headers::HOST],
|
172
|
+
Headers::USER_AGENT => headers[Headers::USER_AGENT]
|
170
173
|
)
|
171
174
|
|
172
175
|
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
|
@@ -213,17 +216,17 @@ module HTTP
|
|
213
216
|
PORTS[@scheme] != port ? "#{host}:#{port}" : host
|
214
217
|
end
|
215
218
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
+
def prepare_body(body)
|
220
|
+
body.is_a?(Request::Body) ? body : Request::Body.new(body)
|
221
|
+
end
|
219
222
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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
|
227
230
|
end
|
228
231
|
end
|
229
232
|
end
|
data/lib/http/request/body.rb
CHANGED
@@ -19,6 +19,7 @@ module HTTP
|
|
19
19
|
@source.bytesize
|
20
20
|
elsif @source.respond_to?(:read)
|
21
21
|
raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
|
22
|
+
|
22
23
|
@source.size
|
23
24
|
elsif @source.nil?
|
24
25
|
0
|
@@ -35,10 +36,12 @@ module HTTP
|
|
35
36
|
yield @source
|
36
37
|
elsif @source.respond_to?(:read)
|
37
38
|
IO.copy_stream(@source, ProcIO.new(block))
|
38
|
-
|
39
|
+
rewind(@source)
|
39
40
|
elsif @source.is_a?(Enumerable)
|
40
41
|
@source.each(&block)
|
41
42
|
end
|
43
|
+
|
44
|
+
self
|
42
45
|
end
|
43
46
|
|
44
47
|
# Request bodies are equivalent when they have the same source.
|
@@ -48,6 +51,28 @@ module HTTP
|
|
48
51
|
|
49
52
|
private
|
50
53
|
|
54
|
+
def rewind(io)
|
55
|
+
io.rewind if io.respond_to? :rewind
|
56
|
+
rescue Errno::ESPIPE, Errno::EPIPE
|
57
|
+
# Pipe IOs respond to `:rewind` but fail when you call it.
|
58
|
+
#
|
59
|
+
# Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
|
60
|
+
# but *EPIPE* on jRuby.
|
61
|
+
#
|
62
|
+
# - **ESPIPE** -- "Illegal seek."
|
63
|
+
# Invalid seek operation (such as on a pipe).
|
64
|
+
#
|
65
|
+
# - **EPIPE** -- "Broken pipe."
|
66
|
+
# There is no process reading from the other end of a pipe. Every
|
67
|
+
# library function that returns this error code also generates
|
68
|
+
# a SIGPIPE signal; this signal terminates the program if not handled
|
69
|
+
# or blocked. Thus, your program will never actually see EPIPE unless
|
70
|
+
# it has handled or blocked SIGPIPE.
|
71
|
+
#
|
72
|
+
# See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
51
76
|
def validate_source_type!
|
52
77
|
return if @source.is_a?(String)
|
53
78
|
return if @source.respond_to?(:read)
|