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.
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)