http 3.1.0 → 5.3.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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
data/lib/http/options.rb CHANGED
@@ -1,46 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Metrics/ClassLength, Style/RedundantSelf
4
-
5
3
  require "http/headers"
6
4
  require "openssl"
7
5
  require "socket"
8
6
  require "http/uri"
9
- require "http/feature"
10
- require "http/features/auto_inflate"
11
- require "http/features/auto_deflate"
12
7
 
13
8
  module HTTP
14
- class Options
9
+ class Options # rubocop:disable Metrics/ClassLength
15
10
  @default_socket_class = TCPSocket
16
11
  @default_ssl_socket_class = OpenSSL::SSL::SSLSocket
17
12
  @default_timeout_class = HTTP::Timeout::Null
18
- @available_features = {
19
- :auto_inflate => Features::AutoInflate,
20
- :auto_deflate => Features::AutoDeflate
21
- }
13
+ @available_features = {}
22
14
 
23
15
  class << self
24
16
  attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
25
17
  attr_reader :available_features
26
18
 
27
- def new(options = {}) # rubocop:disable Style/OptionHash
28
- return options if options.is_a?(self)
29
- super
19
+ def new(options = {})
20
+ options.is_a?(self) ? options : super
30
21
  end
31
22
 
32
23
  def defined_options
33
24
  @defined_options ||= []
34
25
  end
35
26
 
27
+ def register_feature(name, impl)
28
+ @available_features[name] = impl
29
+ end
30
+
36
31
  protected
37
32
 
38
- def def_option(name, &interpreter)
33
+ def def_option(name, reader_only: false, &interpreter)
39
34
  defined_options << name.to_sym
40
- interpreter ||= lambda { |v| v }
35
+ interpreter ||= ->(v) { v }
41
36
 
42
- attr_accessor name
43
- protected :"#{name}="
37
+ if reader_only
38
+ attr_reader name
39
+ else
40
+ attr_accessor name
41
+ protected :"#{name}="
42
+ end
44
43
 
45
44
  define_method(:"with_#{name}") do |value|
46
45
  dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
@@ -48,7 +47,7 @@ module HTTP
48
47
  end
49
48
  end
50
49
 
51
- def initialize(options = {}) # rubocop:disable Style/OptionHash
50
+ def initialize(options = {})
52
51
  defaults = {
53
52
  :response => :auto,
54
53
  :proxy => {},
@@ -70,12 +69,12 @@ module HTTP
70
69
  opts_w_defaults.each { |(k, v)| self[k] = v }
71
70
  end
72
71
 
73
- def_option :headers do |headers|
74
- self.headers.merge(headers)
72
+ def_option :headers do |new_headers|
73
+ headers.merge(new_headers)
75
74
  end
76
75
 
77
- def_option :cookies do |cookies|
78
- cookies.each_with_object self.cookies.dup do |(k, v), jar|
76
+ def_option :cookies do |new_cookies|
77
+ new_cookies.each_with_object cookies.dup do |(k, v), jar|
79
78
  cookie = k.is_a?(Cookie) ? k : Cookie.new(k.to_s, v.to_s)
80
79
  jar[cookie.name] = cookie.cookie_value
81
80
  end
@@ -85,7 +84,7 @@ module HTTP
85
84
  self.encoding = Encoding.find(encoding)
86
85
  end
87
86
 
88
- def_option :features do |features|
87
+ def_option :features, :reader_only => true do |new_features|
89
88
  # Normalize features from:
90
89
  #
91
90
  # [{feature_one: {opt: 'val'}}, :feature_two]
@@ -93,7 +92,7 @@ module HTTP
93
92
  # into:
94
93
  #
95
94
  # {feature_one: {opt: 'val'}, feature_two: {}}
96
- features = features.each_with_object({}) do |feature, h|
95
+ normalized_features = new_features.each_with_object({}) do |feature, h|
97
96
  if feature.is_a?(Hash)
98
97
  h.merge!(feature)
99
98
  else
@@ -101,7 +100,7 @@ module HTTP
101
100
  end
102
101
  end
103
102
 
104
- self.features.merge(features)
103
+ features.merge(normalized_features)
105
104
  end
106
105
 
107
106
  def features=(features)
@@ -112,19 +111,21 @@ module HTTP
112
111
  unless (feature = self.class.available_features[name])
113
112
  argument_error! "Unsupported feature: #{name}"
114
113
  end
115
- feature.new(opts_or_feature)
114
+ feature.new(**opts_or_feature)
116
115
  end
117
116
  end
118
117
  end
119
118
 
120
119
  %w[
121
- proxy params form json body follow response
120
+ proxy params form json body response
122
121
  socket_class nodelay ssl_socket_class ssl_context ssl
123
- persistent keep_alive_timeout timeout_class timeout_options
122
+ keep_alive_timeout timeout_class timeout_options
124
123
  ].each do |method_name|
125
124
  def_option method_name
126
125
  end
127
126
 
127
+ def_option :follow, :reader_only => true
128
+
128
129
  def follow=(value)
129
130
  @follow =
130
131
  case
@@ -135,6 +136,8 @@ module HTTP
135
136
  end
136
137
  end
137
138
 
139
+ def_option :persistent, :reader_only => true
140
+
138
141
  def persistent=(value)
139
142
  @persistent = value ? HTTP::URI.parse(value).origin : nil
140
143
  end
@@ -39,9 +39,10 @@ module HTTP
39
39
  # @param [Hash] opts
40
40
  # @option opts [Boolean] :strict (true) redirector hops policy
41
41
  # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
42
- def initialize(opts = {}) # rubocop:disable Style/OptionHash
43
- @strict = opts.fetch(:strict, true)
44
- @max_hops = opts.fetch(:max_hops, 5).to_i
42
+ def initialize(opts = {})
43
+ @strict = opts.fetch(:strict, true)
44
+ @max_hops = opts.fetch(:max_hops, 5).to_i
45
+ @on_redirect = opts.fetch(:on_redirect, nil)
45
46
  end
46
47
 
47
48
  # Follows redirects until non-redirect response found
@@ -49,6 +50,8 @@ module HTTP
49
50
  @request = request
50
51
  @response = response
51
52
  @visited = []
53
+ collect_cookies_from_request
54
+ collect_cookies_from_response
52
55
 
53
56
  while REDIRECT_CODES.include? @response.status.code
54
57
  @visited << "#{@request.verb} #{@request.uri}"
@@ -58,8 +61,14 @@ module HTTP
58
61
 
59
62
  @response.flush
60
63
 
61
- @request = redirect_to @response.headers[Headers::LOCATION]
64
+ # XXX(ixti): using `Array#inject` to return `nil` if no Location header.
65
+ @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
66
+ unless cookie_jar.empty?
67
+ @request.headers.set(Headers::COOKIE, cookie_jar.cookies.map { |c| "#{c.name}=#{c.value}" }.join("; "))
68
+ end
69
+ @on_redirect.call @response, @request if @on_redirect.respond_to?(:call)
62
70
  @response = yield @request
71
+ collect_cookies_from_response
63
72
  end
64
73
 
65
74
  @response
@@ -67,6 +76,48 @@ module HTTP
67
76
 
68
77
  private
69
78
 
79
+ # All known cookies. On the original request, this is only the original cookies, but after that,
80
+ # Set-Cookie headers can add, set or delete cookies.
81
+ def cookie_jar
82
+ # it seems that @response.cookies instance is reused between responses, so we have to "clone"
83
+ @cookie_jar ||= HTTP::CookieJar.new
84
+ end
85
+
86
+ def collect_cookies_from_request
87
+ request_cookie_header = @request.headers["Cookie"]
88
+ cookies =
89
+ if request_cookie_header
90
+ HTTP::Cookie.cookie_value_to_hash(request_cookie_header)
91
+ else
92
+ {}
93
+ end
94
+
95
+ cookies.each do |key, value|
96
+ cookie_jar.add(HTTP::Cookie.new(key, value, :path => @request.uri.path, :domain => @request.host))
97
+ end
98
+ end
99
+
100
+ # Carry cookies from one response to the next. Carrying cookies to the next response ends up
101
+ # carrying them to the next request as well.
102
+ #
103
+ # Note that this isn't part of the IETF standard, but all major browsers support setting cookies
104
+ # on redirect: https://blog.dubbelboer.com/2012/11/25/302-cookie.html
105
+ def collect_cookies_from_response
106
+ # Overwrite previous cookies
107
+ @response.cookies.each do |cookie|
108
+ if cookie.value == ""
109
+ cookie_jar.delete(cookie)
110
+ else
111
+ cookie_jar.add(cookie)
112
+ end
113
+ end
114
+
115
+ # I wish we could just do @response.cookes = cookie_jar
116
+ cookie_jar.each do |cookie|
117
+ @response.cookies.add(cookie)
118
+ end
119
+ end
120
+
70
121
  # Check if we reached max amount of redirect hops
71
122
  # @return [Boolean]
72
123
  def too_many_hops?
@@ -89,6 +140,7 @@ module HTTP
89
140
 
90
141
  if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
91
142
  raise StateError, "can't follow #{@response.status} redirect" if @strict
143
+
92
144
  verb = :get
93
145
  end
94
146
 
@@ -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,13 +36,43 @@ module HTTP
35
36
  yield @source
36
37
  elsif @source.respond_to?(:read)
37
38
  IO.copy_stream(@source, ProcIO.new(block))
39
+ rewind(@source)
38
40
  elsif @source.is_a?(Enumerable)
39
41
  @source.each(&block)
40
42
  end
43
+
44
+ self
45
+ end
46
+
47
+ # Request bodies are equivalent when they have the same source.
48
+ def ==(other)
49
+ self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
41
50
  end
42
51
 
43
52
  private
44
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
+
45
76
  def validate_source_type!
46
77
  return if @source.is_a?(String)
47
78
  return if @source.respond_to?(:read)
@@ -47,7 +47,11 @@ module HTTP
47
47
  # Adds the headers to the header array for the given request body we are working
48
48
  # with
49
49
  def add_body_type_headers
50
- return if @headers[Headers::CONTENT_LENGTH] || chunked?
50
+ return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
51
+ @body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
52
+ @request_header[0].start_with?("#{method} ")
53
+ end
54
+ )
51
55
 
52
56
  @request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
53
57
  end
@@ -57,25 +61,35 @@ module HTTP
57
61
  def join_headers
58
62
  # join the headers array with crlfs, stick two on the end because
59
63
  # that ends the request header
60
- @request_header.join(CRLF) + CRLF * 2
64
+ @request_header.join(CRLF) + (CRLF * 2)
61
65
  end
62
66
 
67
+ # Writes HTTP request data into the socket.
63
68
  def send_request
64
- # It's important to send the request in a single write call when
65
- # possible in order to play nicely with Nagle's algorithm. Making
66
- # two writes in a row triggers a pathological case where Nagle is
67
- # expecting a third write that never happens.
69
+ each_chunk { |chunk| write chunk }
70
+ rescue Errno::EPIPE
71
+ # server doesn't need any more data
72
+ nil
73
+ end
74
+
75
+ # Yields chunks of request data that should be sent to the socket.
76
+ #
77
+ # It's important to send the request in a single write call when possible
78
+ # in order to play nicely with Nagle's algorithm. Making two writes in a
79
+ # row triggers a pathological case where Nagle is expecting a third write
80
+ # that never happens.
81
+ def each_chunk
68
82
  data = join_headers
69
83
 
70
84
  @body.each do |chunk|
71
85
  data << encode_chunk(chunk)
72
- write(data)
86
+ yield data
73
87
  data.clear
74
88
  end
75
89
 
76
- write(data) unless data.empty?
90
+ yield data unless data.empty?
77
91
 
78
- write(CHUNKED_END) if chunked?
92
+ yield CHUNKED_END if chunked?
79
93
  end
80
94
 
81
95
  # Returns the chunk encoded for to the specified "Transfer-Encoding" header.
@@ -94,12 +108,18 @@ module HTTP
94
108
 
95
109
  private
96
110
 
111
+ # @raise [SocketWriteError] when unable to write to socket
97
112
  def write(data)
98
113
  until data.empty?
99
114
  length = @socket.write(data)
100
115
  break unless data.bytesize > length
116
+
101
117
  data = data.byteslice(length..-1)
102
118
  end
119
+ rescue Errno::EPIPE
120
+ raise
121
+ rescue IOError, SocketError, SystemCallError => e
122
+ raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
103
123
  end
104
124
  end
105
125
  end
data/lib/http/request.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "base64"
5
4
  require "time"
6
5
 
6
+ require "http/base64"
7
7
  require "http/errors"
8
8
  require "http/headers"
9
9
  require "http/request/body"
@@ -15,6 +15,7 @@ module HTTP
15
15
  class Request
16
16
  extend Forwardable
17
17
 
18
+ include HTTP::Base64
18
19
  include HTTP::Headers::Mixin
19
20
 
20
21
  # The method given was not understood
@@ -46,7 +47,13 @@ module HTTP
46
47
  :patch,
47
48
 
48
49
  # draft-reschke-webdav-search: WebDAV Search
49
- :search
50
+ :search,
51
+
52
+ # RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
53
+ :mkcalendar,
54
+
55
+ # Implemented by several caching servers, like Squid, Varnish or Fastly
56
+ :purge
50
57
  ].freeze
51
58
 
52
59
  # Allowed schemes
@@ -54,10 +61,10 @@ module HTTP
54
61
 
55
62
  # Default ports of supported schemes
56
63
  PORTS = {
57
- :http => 80,
58
- :https => 443,
59
- :ws => 80,
60
- :wss => 443
64
+ :http => 80,
65
+ :https => 443,
66
+ :ws => 80,
67
+ :wss => 443
61
68
  }.freeze
62
69
 
63
70
  # Method is given as a lowercase symbol e.g. :get, :post
@@ -66,6 +73,8 @@ module HTTP
66
73
  # Scheme is normalized to be a lowercase symbol e.g. :http, :https
67
74
  attr_reader :scheme
68
75
 
76
+ attr_reader :uri_normalizer
77
+
69
78
  # "Request URI" as per RFC 2616
70
79
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
71
80
  attr_reader :uri
@@ -73,25 +82,25 @@ module HTTP
73
82
 
74
83
  # @option opts [String] :version
75
84
  # @option opts [#to_s] :verb HTTP request method
85
+ # @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER)
76
86
  # @option opts [HTTP::URI, #to_s] :uri
77
87
  # @option opts [Hash] :headers
78
88
  # @option opts [Hash] :proxy
79
89
  # @option opts [String, Enumerable, IO, nil] :body
80
90
  def initialize(opts)
81
- @verb = opts.fetch(:verb).to_s.downcase.to_sym
82
- @uri = normalize_uri(opts.fetch(:uri))
91
+ @verb = opts.fetch(:verb).to_s.downcase.to_sym
92
+ @uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER
93
+
94
+ @uri = @uri_normalizer.call(opts.fetch(:uri))
83
95
  @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
84
96
 
85
97
  raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
86
98
  raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
87
99
 
88
100
  @proxy = opts[:proxy] || {}
89
- @body = request_body(opts[:body], opts)
90
101
  @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
102
+ @headers = prepare_headers(opts[:headers])
103
+ @body = prepare_body(opts[:body])
95
104
  end
96
105
 
97
106
  # Returns new Request with updated uri
@@ -99,13 +108,28 @@ module HTTP
99
108
  headers = self.headers.dup
100
109
  headers.delete(Headers::HOST)
101
110
 
111
+ new_body = body.source
112
+ if verb == :get
113
+ # request bodies should not always be resubmitted when following a redirect
114
+ # some servers will close the connection after receiving the request headers
115
+ # which may cause Errno::ECONNRESET: Connection reset by peer
116
+ # see https://github.com/httprb/http/issues/649
117
+ # new_body = Request::Body.new(nil)
118
+ new_body = nil
119
+ # the CONTENT_TYPE header causes problems if set on a get request w/ an empty body
120
+ # the server might assume that there should be content if it is set to multipart
121
+ # rack raises EmptyContentError if this happens
122
+ headers.delete(Headers::CONTENT_TYPE)
123
+ end
124
+
102
125
  self.class.new(
103
- :verb => verb,
104
- :uri => @uri.join(uri),
105
- :headers => headers,
106
- :proxy => proxy,
107
- :body => body,
108
- :version => version
126
+ :verb => verb,
127
+ :uri => @uri.join(uri),
128
+ :headers => headers,
129
+ :proxy => proxy,
130
+ :body => new_body,
131
+ :version => version,
132
+ :uri_normalizer => uri_normalizer
109
133
  )
110
134
  end
111
135
 
@@ -136,7 +160,7 @@ module HTTP
136
160
  end
137
161
 
138
162
  def proxy_authorization_header
139
- digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
163
+ digest = encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
140
164
  "Basic #{digest}"
141
165
  end
142
166
 
@@ -152,7 +176,9 @@ module HTTP
152
176
  uri.omit(:fragment)
153
177
  else
154
178
  uri.request_uri
155
- end
179
+ end.to_s
180
+
181
+ raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
156
182
 
157
183
  "#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
158
184
  end
@@ -165,8 +191,8 @@ module HTTP
165
191
  # Headers to send with proxy connect request
166
192
  def proxy_connect_headers
167
193
  connect_headers = HTTP::Headers.coerce(
168
- Headers::HOST => headers[Headers::HOST],
169
- Headers::USER_AGENT => headers[Headers::USER_AGENT]
194
+ Headers::HOST => headers[Headers::HOST],
195
+ Headers::USER_AGENT => headers[Headers::USER_AGENT]
170
196
  )
171
197
 
172
198
  connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
@@ -184,15 +210,20 @@ module HTTP
184
210
  using_proxy? ? proxy[:proxy_port] : port
185
211
  end
186
212
 
187
- private
188
-
189
- # Transforms body to an object suitable for streaming.
190
- def request_body(body, opts)
191
- body = Request::Body.new(body) unless body.is_a?(Request::Body)
192
- body = opts[:auto_deflate].deflated_body(body) if opts[:auto_deflate]
193
- body
213
+ # Human-readable representation of base request info.
214
+ #
215
+ # @example
216
+ #
217
+ # req.inspect
218
+ # # => #<HTTP::Request/1.1 GET https://example.com>
219
+ #
220
+ # @return [String]
221
+ def inspect
222
+ "#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>"
194
223
  end
195
224
 
225
+ private
226
+
196
227
  # @!attribute [r] host
197
228
  # @return [String]
198
229
  def_delegator :@uri, :host
@@ -205,20 +236,24 @@ module HTTP
205
236
 
206
237
  # @return [String] Default host (with port if needed) header value.
207
238
  def default_host_header_value
208
- PORTS[@scheme] != port ? "#{host}:#{port}" : host
239
+ value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
240
+
241
+ raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
242
+
243
+ value
209
244
  end
210
245
 
211
- # @return [HTTP::URI] URI with all componentes but query being normalized.
212
- def normalize_uri(uri)
213
- uri = HTTP::URI.parse uri
246
+ def prepare_body(body)
247
+ body.is_a?(Request::Body) ? body : Request::Body.new(body)
248
+ end
214
249
 
215
- HTTP::URI.new(
216
- :scheme => uri.normalized_scheme,
217
- :authority => uri.normalized_authority,
218
- :path => uri.normalized_path,
219
- :query => uri.query,
220
- :fragment => uri.normalized_fragment
221
- )
250
+ def prepare_headers(headers)
251
+ headers = HTTP::Headers.coerce(headers || {})
252
+
253
+ headers[Headers::HOST] ||= default_host_header_value
254
+ headers[Headers::USER_AGENT] ||= USER_AGENT
255
+
256
+ headers
222
257
  end
223
258
  end
224
259
  end
@@ -28,7 +28,8 @@ module HTTP
28
28
  def readpartial(*args)
29
29
  stream!
30
30
  chunk = @stream.readpartial(*args)
31
- chunk.force_encoding(@encoding) if chunk
31
+
32
+ String.new(chunk, :encoding => @encoding) if chunk
32
33
  end
33
34
 
34
35
  # Iterate over the body, allowing it to be enumerable
@@ -46,11 +47,11 @@ module HTTP
46
47
 
47
48
  begin
48
49
  @streaming = false
49
- @contents = String.new("").force_encoding(@encoding)
50
+ @contents = String.new("", :encoding => @encoding)
50
51
 
51
52
  while (chunk = @stream.readpartial)
52
- @contents << chunk.force_encoding(@encoding)
53
- chunk.clear # deallocate string
53
+ @contents << String.new(chunk, :encoding => @encoding)
54
+ chunk = nil # deallocate string
54
55
  end
55
56
  rescue
56
57
  @contents = nil
@@ -64,6 +65,7 @@ module HTTP
64
65
  # Assert that the body is actively being streamed
65
66
  def stream!
66
67
  raise StateError, "body has already been consumed" if @streaming == false
68
+
67
69
  @streaming = true
68
70
  end
69
71
 
@@ -16,7 +16,7 @@ module HTTP
16
16
  if chunk
17
17
  chunk = zstream.inflate(chunk)
18
18
  elsif !zstream.closed?
19
- zstream.finish
19
+ zstream.finish if zstream.total_in.positive?
20
20
  zstream.close
21
21
  end
22
22
  chunk