http 5.0.0.pre → 5.0.0.pre2

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -1
  3. data/.travis.yml +6 -4
  4. data/CHANGES.md +83 -0
  5. data/Gemfile +2 -1
  6. data/README.md +7 -6
  7. data/http.gemspec +11 -4
  8. data/lib/http/chainable.rb +8 -3
  9. data/lib/http/client.rb +32 -34
  10. data/lib/http/connection.rb +5 -5
  11. data/lib/http/content_type.rb +2 -2
  12. data/lib/http/feature.rb +3 -0
  13. data/lib/http/features/auto_deflate.rb +13 -7
  14. data/lib/http/features/auto_inflate.rb +6 -5
  15. data/lib/http/features/normalize_uri.rb +17 -0
  16. data/lib/http/headers.rb +48 -11
  17. data/lib/http/headers/known.rb +3 -0
  18. data/lib/http/mime_type/adapter.rb +1 -1
  19. data/lib/http/mime_type/json.rb +1 -0
  20. data/lib/http/options.rb +4 -7
  21. data/lib/http/redirector.rb +3 -1
  22. data/lib/http/request.rb +32 -29
  23. data/lib/http/request/body.rb +26 -1
  24. data/lib/http/request/writer.rb +3 -2
  25. data/lib/http/response.rb +17 -15
  26. data/lib/http/response/body.rb +1 -0
  27. data/lib/http/response/parser.rb +20 -6
  28. data/lib/http/response/status.rb +2 -1
  29. data/lib/http/timeout/global.rb +1 -3
  30. data/lib/http/timeout/per_operation.rb +1 -0
  31. data/lib/http/uri.rb +13 -0
  32. data/lib/http/version.rb +1 -1
  33. data/spec/lib/http/client_spec.rb +96 -14
  34. data/spec/lib/http/connection_spec.rb +8 -5
  35. data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
  36. data/spec/lib/http/features/instrumentation_spec.rb +7 -6
  37. data/spec/lib/http/features/logging_spec.rb +6 -5
  38. data/spec/lib/http/headers_spec.rb +52 -17
  39. data/spec/lib/http/options/headers_spec.rb +1 -1
  40. data/spec/lib/http/options/merge_spec.rb +16 -16
  41. data/spec/lib/http/redirector_spec.rb +15 -1
  42. data/spec/lib/http/request/body_spec.rb +22 -0
  43. data/spec/lib/http/request/writer_spec.rb +13 -1
  44. data/spec/lib/http/request_spec.rb +5 -5
  45. data/spec/lib/http/response/parser_spec.rb +45 -0
  46. data/spec/lib/http/response/status_spec.rb +3 -3
  47. data/spec/lib/http/response_spec.rb +11 -22
  48. data/spec/lib/http_spec.rb +30 -1
  49. data/spec/support/black_hole.rb +1 -1
  50. data/spec/support/dummy_server.rb +6 -6
  51. data/spec/support/dummy_server/servlet.rb +8 -4
  52. data/spec/support/http_handling_shared.rb +4 -4
  53. data/spec/support/ssl_helper.rb +4 -4
  54. metadata +23 -16
@@ -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
@@ -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 => request.version,
26
- :verb => request.verb,
27
- :uri => request.uri,
28
- :headers => request.headers,
29
- :proxy => request.proxy,
30
- :body => deflated_body(request.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 => response.status,
16
- :version => response.version,
17
- :headers => response.headers,
15
+ :status => response.status,
16
+ :version => response.version,
17
+ :headers => response.headers,
18
18
  :proxy_headers => response.proxy_headers,
19
- :connection => response.connection,
20
- :body => stream_for(response.connection)
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
@@ -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 = /^[A-Z][a-z]*(?:-[A-Z][a-z]*)*$/
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 = /^[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
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 [#to_s] name header name
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
- name = normalize_header name.to_s
53
- Array(value).each { |v| @pile << [name, v.to_s] }
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 { |pair| pair.map(&:dup) }
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
- @pile == other.to_a
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
- @pile.each { |arr| yield(arr) }
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 = to_a
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
@@ -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
 
@@ -15,7 +15,7 @@ module HTTP
15
15
  end
16
16
 
17
17
  %w[encode decode].each do |operation|
18
- class_eval <<-RUBY, __FILE__, __LINE__
18
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
19
19
  def #{operation}(*)
20
20
  fail Error, "\#{self.class} does not supports ##{operation}"
21
21
  end
@@ -10,6 +10,7 @@ module HTTP
10
10
  # Encodes object to JSON
11
11
  def encode(obj)
12
12
  return obj.to_json if obj.respond_to?(:to_json)
13
+
13
14
  ::JSON.dump obj
14
15
  end
15
16
 
@@ -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 = {}) # rubocop:disable Style/OptionHash
22
- return options if options.is_a?(self)
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
@@ -58,7 +58,8 @@ module HTTP
58
58
 
59
59
  @response.flush
60
60
 
61
- @request = redirect_to @response.headers[Headers::LOCATION]
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
 
@@ -54,10 +54,10 @@ module HTTP
54
54
 
55
55
  # Default ports of supported schemes
56
56
  PORTS = {
57
- :http => 80,
58
- :https => 443,
59
- :ws => 80,
60
- :wss => 443
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 = opts.fetch(:verb).to_s.downcase.to_sym
82
- @uri = normalize_uri(opts.fetch(:uri))
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 = HTTP::Headers.coerce(opts[: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 => verb,
104
- :uri => @uri.join(uri),
105
- :headers => headers,
106
- :proxy => proxy,
107
- :body => body.source,
108
- :version => 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 => headers[Headers::HOST],
169
- Headers::USER_AGENT => headers[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
- # @return [HTTP::URI] URI with all componentes but query being normalized.
217
- def normalize_uri(uri)
218
- uri = HTTP::URI.parse uri
219
+ def prepare_body(body)
220
+ body.is_a?(Request::Body) ? body : Request::Body.new(body)
221
+ end
219
222
 
220
- HTTP::URI.new(
221
- :scheme => uri.normalized_scheme,
222
- :authority => uri.normalized_authority,
223
- :path => uri.normalized_path,
224
- :query => uri.query,
225
- :fragment => uri.normalized_fragment
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
@@ -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
- @source.rewind if @source.respond_to?(:rewind)
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)