http 0.9.8 → 1.0.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.
data/lib/http/client.rb CHANGED
@@ -32,13 +32,19 @@ module HTTP
32
32
  body = make_request_body(opts, headers)
33
33
  proxy = opts.proxy
34
34
 
35
- req = HTTP::Request.new(verb, uri, headers, proxy, body)
36
- res = perform req, opts
35
+ req = HTTP::Request.new(
36
+ :verb => verb,
37
+ :uri => uri,
38
+ :headers => headers,
39
+ :proxy => proxy,
40
+ :body => body
41
+ )
37
42
 
43
+ res = perform(req, opts)
38
44
  return res unless opts.follow
39
45
 
40
- Redirector.new(opts.follow).perform req, res do |request|
41
- perform request, opts
46
+ Redirector.new(opts.follow).perform(req, res) do |request|
47
+ perform(request, opts)
42
48
  end
43
49
  end
44
50
 
@@ -61,11 +67,12 @@ module HTTP
61
67
  end
62
68
 
63
69
  res = Response.new(
64
- @connection.status_code,
65
- @connection.http_version,
66
- @connection.headers,
67
- Response::Body.new(@connection),
68
- req.uri
70
+ :status => @connection.status_code,
71
+ :version => @connection.http_version,
72
+ :headers => @connection.headers,
73
+ :connection => @connection,
74
+ :encoding => options.encoding,
75
+ :uri => req.uri
69
76
  )
70
77
 
71
78
  @connection.finish_response if req.verb == :head
@@ -73,9 +80,7 @@ module HTTP
73
80
 
74
81
  res
75
82
  rescue
76
- # On any exception we reset the conn. This is a safety measure, to ensure
77
- # we don't have conns in a bad state resulting in mixed requests/responses
78
- close if persistent?
83
+ close
79
84
  raise
80
85
  end
81
86
 
@@ -20,21 +20,24 @@ module HTTP
20
20
 
21
21
  # @param [HTTP::Request] req
22
22
  # @param [HTTP::Options] options
23
+ # @raise [HTTP::ConnectionError] when failed to connect
23
24
  def initialize(req, options)
24
25
  @persistent = options.persistent?
25
- @keep_alive_timeout = options[:keep_alive_timeout].to_f
26
+ @keep_alive_timeout = options.keep_alive_timeout.to_f
26
27
  @pending_request = false
27
28
  @pending_response = false
28
29
  @failed_proxy_connect = false
29
30
 
30
31
  @parser = Response::Parser.new
31
32
 
32
- @socket = options[:timeout_class].new(options[:timeout_options])
33
- @socket.connect(options[:socket_class], req.socket_host, req.socket_port)
33
+ @socket = options.timeout_class.new(options.timeout_options)
34
+ @socket.connect(options.socket_class, req.socket_host, req.socket_port, options.nodelay)
34
35
 
35
36
  send_proxy_connect_request(req)
36
37
  start_tls(req, options)
37
38
  reset_timer
39
+ rescue SocketError, SystemCallError => e
40
+ raise ConnectionError, "failed to connect: #{e}"
38
41
  end
39
42
 
40
43
  # @see (HTTP::Response::Parser#status_code)
@@ -104,7 +107,7 @@ module HTTP
104
107
 
105
108
  set_keep_alive
106
109
  rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
107
- raise IOError, "problem making HTTP request: #{e}"
110
+ raise ConnectionError, "failed to read headers: #{e}"
108
111
  end
109
112
 
110
113
  # Callback for when we've reached the end of a response
@@ -148,14 +151,14 @@ module HTTP
148
151
  def start_tls(req, options)
149
152
  return unless req.uri.https? && !failed_proxy_connect?
150
153
 
151
- ssl_context = options[:ssl_context]
154
+ ssl_context = options.ssl_context
152
155
 
153
156
  unless ssl_context
154
157
  ssl_context = OpenSSL::SSL::SSLContext.new
155
- ssl_context.set_params(options[:ssl] || {})
158
+ ssl_context.set_params(options.ssl || {})
156
159
  end
157
160
 
158
- @socket.start_tls(req.uri.host, options[:ssl_socket_class], ssl_context)
161
+ @socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context)
159
162
  end
160
163
 
161
164
  # Open tunnel through proxy
data/lib/http/errors.rb CHANGED
@@ -2,6 +2,9 @@ module HTTP
2
2
  # Generic error
3
3
  class Error < StandardError; end
4
4
 
5
+ # Generic Connection error
6
+ class ConnectionError < Error; end
7
+
5
8
  # Generic Request error
6
9
  class RequestError < Error; end
7
10
 
data/lib/http/headers.rb CHANGED
@@ -51,9 +51,6 @@ module HTTP
51
51
  Array(value).each { |v| @pile << [name, v.to_s] }
52
52
  end
53
53
 
54
- # @deprecated Will be removed in 1.0.0
55
- alias_method :append, :add
56
-
57
54
  # Returns list of header values if any.
58
55
  #
59
56
  # @return [Array<String>]
data/lib/http/options.rb CHANGED
@@ -43,11 +43,13 @@ module HTTP
43
43
  :timeout_class => self.class.default_timeout_class,
44
44
  :timeout_options => {},
45
45
  :socket_class => self.class.default_socket_class,
46
+ :nodelay => false,
46
47
  :ssl_socket_class => self.class.default_ssl_socket_class,
47
48
  :ssl => {},
48
49
  :keep_alive_timeout => 5,
49
50
  :headers => {},
50
- :cookies => {}
51
+ :cookies => {},
52
+ :encoding => nil
51
53
  }
52
54
 
53
55
  opts_w_defaults = defaults.merge(options)
@@ -66,9 +68,13 @@ module HTTP
66
68
  end
67
69
  end
68
70
 
71
+ def_option :encoding do |encoding|
72
+ self.encoding = Encoding.find(encoding)
73
+ end
74
+
69
75
  %w(
70
76
  proxy params form json body follow response
71
- socket_class ssl_socket_class ssl_context ssl
77
+ socket_class nodelay ssl_socket_class ssl_context ssl
72
78
  persistent keep_alive_timeout timeout_class timeout_options
73
79
  ).each do |method_name|
74
80
  def_option method_name
@@ -91,15 +97,6 @@ module HTTP
91
97
  !persistent.nil?
92
98
  end
93
99
 
94
- # @deprecated
95
- def [](option)
96
- send(option)
97
- rescue
98
- warn "[DEPRECATED] `HTTP::Options#[:#{option}]` was deprecated. " \
99
- "Use `HTTP::Options##{option}` instead."
100
- nil
101
- end
102
-
103
100
  def merge(other)
104
101
  h1 = to_hash
105
102
  h2 = other.to_hash
@@ -119,7 +116,7 @@ module HTTP
119
116
  def to_hash
120
117
  hash_pairs = self.class.
121
118
  defined_options.
122
- flat_map { |opt_name| [opt_name, self[opt_name]] }
119
+ flat_map { |opt_name| [opt_name, send(opt_name)] }
123
120
  Hash[*hash_pairs]
124
121
  end
125
122
 
@@ -18,7 +18,7 @@ module HTTP
18
18
  # Types valid to be used as body source
19
19
  VALID_BODY_TYPES = [String, NilClass, Enumerable]
20
20
 
21
- def initialize(socket, body, headers, headline) # rubocop:disable ParameterLists
21
+ def initialize(socket, body, headers, headline)
22
22
  @body = body
23
23
  @socket = socket
24
24
  @headers = headers
@@ -36,8 +36,9 @@ module HTTP
36
36
 
37
37
  # Stream the request to a socket
38
38
  def stream
39
- send_request_header
40
- send_request_body
39
+ add_headers
40
+ add_body_type_headers
41
+ send_request
41
42
  end
42
43
 
43
44
  # Send headers needed to connect through proxy
@@ -64,23 +65,24 @@ module HTTP
64
65
  @request_header.join(CRLF) + (CRLF) * 2
65
66
  end
66
67
 
67
- def send_request_header
68
- add_headers
69
- add_body_type_headers
68
+ def send_request
69
+ headers = join_headers
70
70
 
71
- write(join_headers)
72
- end
71
+ case @body
72
+ when NilClass
73
+ write(headers)
74
+ when String
75
+ write(headers << @body)
76
+ when Enumerable
77
+ write(headers)
73
78
 
74
- def send_request_body
75
- if @body.is_a?(String)
76
- write(@body)
77
- elsif @body.is_a?(Enumerable)
78
79
  @body.each do |chunk|
79
80
  write(chunk.bytesize.to_s(16) << CRLF)
80
81
  write(chunk << CRLF)
81
82
  end
82
83
 
83
84
  write(CHUNKED_END)
85
+ else fail TypeError, "invalid body type: #{@body.class}"
84
86
  end
85
87
  end
86
88
 
data/lib/http/request.rb CHANGED
@@ -63,20 +63,24 @@ module HTTP
63
63
  attr_reader :uri
64
64
  attr_reader :proxy, :body, :version
65
65
 
66
- # :nodoc:
67
- def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version = "1.1") # rubocop:disable ParameterLists
68
- @verb = verb.to_s.downcase.to_sym
69
- @uri = normalize_uri uri
70
- @scheme = @uri.scheme && @uri.scheme.to_s.downcase.to_sym
66
+ # @option opts [String] :version
67
+ # @option opts [#to_s] :verb HTTP request method
68
+ # @option opts [HTTP::URI, #to_s] :uri
69
+ # @option opts [Hash] :headers
70
+ # @option opts [Hash] :proxy
71
+ # @option opts [String] :body
72
+ def initialize(opts)
73
+ @verb = opts.fetch(:verb).to_s.downcase.to_sym
74
+ @uri = normalize_uri(opts.fetch :uri)
75
+ @scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
71
76
 
72
77
  fail(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
73
78
  fail(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
74
79
 
75
- @proxy = proxy
76
- @body = body
77
- @version = version
78
-
79
- @headers = HTTP::Headers.coerce(headers || {})
80
+ @proxy = opts[:proxy] || {}
81
+ @body = opts[:body]
82
+ @version = opts[:version] || "1.1"
83
+ @headers = HTTP::Headers.coerce(opts[:headers] || {})
80
84
 
81
85
  @headers[Headers::HOST] ||= default_host_header_value
82
86
  @headers[Headers::USER_AGENT] ||= USER_AGENT
@@ -84,7 +88,15 @@ module HTTP
84
88
 
85
89
  # Returns new Request with updated uri
86
90
  def redirect(uri, verb = @verb)
87
- req = self.class.new(verb, @uri.join(uri), headers, proxy, body, version)
91
+ req = self.class.new(
92
+ :verb => verb,
93
+ :uri => @uri.join(uri),
94
+ :headers => headers,
95
+ :proxy => proxy,
96
+ :body => body,
97
+ :version => version
98
+ )
99
+
88
100
  req[Headers::HOST] = req.uri.host
89
101
  req
90
102
  end
@@ -126,9 +138,6 @@ module HTTP
126
138
  "#{verb.to_s.upcase} #{request_uri.omit :fragment} HTTP/#{version}"
127
139
  end
128
140
 
129
- # @deprecated Will be removed in 1.0.0
130
- alias_method :request_header, :headline
131
-
132
141
  # Compute HTTP request header SSL proxy connection
133
142
  def proxy_connect_header
134
143
  "CONNECT #{host}:#{port} HTTP/#{version}"
@@ -9,10 +9,11 @@ module HTTP
9
9
  include Enumerable
10
10
  def_delegator :to_s, :empty?
11
11
 
12
- def initialize(client)
13
- @client = client
14
- @streaming = nil
15
- @contents = nil
12
+ def initialize(client, encoding = Encoding::BINARY)
13
+ @client = client
14
+ @streaming = nil
15
+ @contents = nil
16
+ @encoding = encoding
16
17
  end
17
18
 
18
19
  # (see HTTP::Client#readpartial)
@@ -36,9 +37,9 @@ module HTTP
36
37
 
37
38
  begin
38
39
  @streaming = false
39
- @contents = "".force_encoding(Encoding::UTF_8)
40
+ @contents = "".force_encoding(@encoding)
40
41
  while (chunk = @client.readpartial)
41
- @contents << chunk.force_encoding(Encoding::ASCII_8BIT)
42
+ @contents << chunk.force_encoding(@encoding)
42
43
  end
43
44
  rescue
44
45
  @contents = nil
@@ -42,7 +42,8 @@ module HTTP
42
42
  end
43
43
 
44
44
  def chunk
45
- chunk, @chunk = @chunk, nil
45
+ chunk = @chunk
46
+ @chunk = nil
46
47
  chunk
47
48
  end
48
49
 
@@ -91,9 +91,6 @@ module HTTP
91
91
  SYMBOLS[code]
92
92
  end
93
93
 
94
- # @deprecated Will be removed in 1.0.0
95
- alias_method :symbolize, :to_sym
96
-
97
94
  # Printable version of HTTP Status, surrounded by quote marks,
98
95
  # with special characters escaped.
99
96
  #
@@ -111,6 +108,7 @@ module HTTP
111
108
  end
112
109
 
113
110
  def __setobj__(obj)
111
+ fail TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i
114
112
  @code = obj.to_i
115
113
  end
116
114
 
data/lib/http/response.rb CHANGED
@@ -14,13 +14,6 @@ module HTTP
14
14
 
15
15
  include HTTP::Headers::Mixin
16
16
 
17
- # @deprecated Will be removed in 1.0.0
18
- # Use Status::REASONS
19
- STATUS_CODES = Status::REASONS
20
-
21
- # @deprecated Will be removed in 1.0.0
22
- SYMBOL_TO_STATUS_CODE = Hash[STATUS_CODES.map { |k, v| [v.downcase.gsub(/\s|-/, "_").to_sym, k] }].freeze
23
-
24
17
  # @return [Status]
25
18
  attr_reader :status
26
19
 
@@ -30,12 +23,29 @@ module HTTP
30
23
  # @return [URI, nil]
31
24
  attr_reader :uri
32
25
 
33
- def initialize(status, version, headers, body, uri = nil) # rubocop:disable ParameterLists
34
- @version = version
35
- @body = body
36
- @uri = uri && HTTP::URI.parse(uri)
37
- @status = HTTP::Response::Status.new status
38
- @headers = HTTP::Headers.coerce(headers || {})
26
+ # Inits a new instance
27
+ #
28
+ # @option opts [Integer] :status Status code
29
+ # @option opts [String] :version HTTP version
30
+ # @option opts [Hash] :headers
31
+ # @option opts [HTTP::Connection] :connection
32
+ # @option opts [String] :encoding Encoding to use when reading body
33
+ # @option opts [String] :body
34
+ # @option opts [String] :uri
35
+ def initialize(opts)
36
+ @version = opts.fetch(:version)
37
+ @uri = HTTP::URI.parse(opts.fetch :uri) if opts.include? :uri
38
+ @status = HTTP::Response::Status.new(opts.fetch :status)
39
+ @headers = HTTP::Headers.coerce(opts[:headers] || {})
40
+
41
+ if opts.include?(:connection)
42
+ connection = opts.fetch(:connection)
43
+ encoding = opts[:encoding] || charset || Encoding::BINARY
44
+
45
+ @body = Response::Body.new(connection, encoding)
46
+ else
47
+ @body = opts.fetch(:body)
48
+ end
39
49
  end
40
50
 
41
51
  # @!method reason
@@ -46,9 +56,6 @@ module HTTP
46
56
  # @return (see HTTP::Response::Status#code)
47
57
  def_delegator :status, :code
48
58
 
49
- # @deprecated Will be removed in 1.0.0
50
- alias_method :status_code, :code
51
-
52
59
  # @!method to_s
53
60
  # (see HTTP::Response::Body#to_s)
54
61
  def_delegator :body, :to_s
@@ -80,19 +87,15 @@ module HTTP
80
87
  @content_type ||= ContentType.parse headers[Headers::CONTENT_TYPE]
81
88
  end
82
89
 
83
- # MIME type of response (if any)
84
- #
85
- # @return [String, nil]
86
- def mime_type
87
- @mime_type ||= content_type.mime_type
88
- end
90
+ # @!method mime_type
91
+ # MIME type of response (if any)
92
+ # @return [String, nil]
93
+ def_delegator :content_type, :mime_type
89
94
 
90
- # Charset of response (if any)
91
- #
92
- # @return [String, nil]
93
- def charset
94
- @charset ||= content_type.charset
95
- end
95
+ # @!method charset
96
+ # Charset of response (if any)
97
+ # @return [String, nil]
98
+ def_delegator :content_type, :charset
96
99
 
97
100
  def cookies
98
101
  @cookies ||= headers.each_with_object CookieJar.new do |(k, v), jar|
@@ -18,10 +18,11 @@ module HTTP
18
18
  @total_timeout = time_left
19
19
  end
20
20
 
21
- def connect(socket_class, host, port)
21
+ def connect(socket_class, host, port, nodelay = false)
22
22
  reset_timer
23
23
  ::Timeout.timeout(time_left, TimeoutError) do
24
24
  @socket = socket_class.open(host, port)
25
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
25
26
  end
26
27
 
27
28
  log_time
@@ -14,8 +14,9 @@ module HTTP
14
14
  end
15
15
 
16
16
  # Connects to a socket
17
- def connect(socket_class, host, port)
17
+ def connect(socket_class, host, port, nodelay = false)
18
18
  @socket = socket_class.open(host, port)
19
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
19
20
  end
20
21
 
21
22
  # Starts a SSL connection on a socket
@@ -19,9 +19,10 @@ module HTTP
19
19
  @connect_timeout = options.fetch(:connect_timeout, CONNECT_TIMEOUT)
20
20
  end
21
21
 
22
- def connect(socket_class, host, port)
22
+ def connect(socket_class, host, port, nodelay = false)
23
23
  ::Timeout.timeout(connect_timeout, TimeoutError) do
24
24
  @socket = socket_class.open(host, port)
25
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
25
26
  end
26
27
  end
27
28
 
data/lib/http/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module HTTP
2
- VERSION = "0.9.8".freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
data/lib/http.rb CHANGED
@@ -23,5 +23,3 @@ module HTTP
23
23
  alias_method :[], :headers
24
24
  end
25
25
  end
26
-
27
- Http = HTTP unless defined?(Http)
data/logo.png CHANGED
Binary file
@@ -26,11 +26,20 @@ RSpec.describe HTTP::Client do
26
26
  end
27
27
 
28
28
  def redirect_response(location, status = 302)
29
- HTTP::Response.new(status, "1.1", {"Location" => location}, "")
29
+ HTTP::Response.new(
30
+ :status => status,
31
+ :version => "1.1",
32
+ :headers => {"Location" => location},
33
+ :body => ""
34
+ )
30
35
  end
31
36
 
32
37
  def simple_response(body, status = 200)
33
- HTTP::Response.new(status, "1.1", {}, body)
38
+ HTTP::Response.new(
39
+ :status => status,
40
+ :version => "1.1",
41
+ :body => body
42
+ )
34
43
  end
35
44
 
36
45
  describe "following redirects" do
@@ -100,48 +109,48 @@ RSpec.describe HTTP::Client do
100
109
  before { allow(client).to receive :perform }
101
110
 
102
111
  it "accepts params within the provided URL" do
103
- expect(HTTP::Request).to receive(:new) do |_, uri|
104
- expect(CGI.parse uri.query).to eq("foo" => %w(bar))
112
+ expect(HTTP::Request).to receive(:new) do |opts|
113
+ expect(CGI.parse opts[:uri].query).to eq("foo" => %w(bar))
105
114
  end
106
115
 
107
116
  client.get("http://example.com/?foo=bar")
108
117
  end
109
118
 
110
119
  it "combines GET params from the URI with the passed in params" do
111
- expect(HTTP::Request).to receive(:new) do |_, uri|
112
- expect(CGI.parse uri.query).to eq("foo" => %w(bar), "baz" => %w(quux))
120
+ expect(HTTP::Request).to receive(:new) do |opts|
121
+ expect(CGI.parse opts[:uri].query).to eq("foo" => %w(bar), "baz" => %w(quux))
113
122
  end
114
123
 
115
124
  client.get("http://example.com/?foo=bar", :params => {:baz => "quux"})
116
125
  end
117
126
 
118
127
  it "merges duplicate values" do
119
- expect(HTTP::Request).to receive(:new) do |_, uri|
120
- expect(uri.query).to match(/^(a=1&a=2|a=2&a=1)$/)
128
+ expect(HTTP::Request).to receive(:new) do |opts|
129
+ expect(opts[:uri].query).to match(/^(a=1&a=2|a=2&a=1)$/)
121
130
  end
122
131
 
123
132
  client.get("http://example.com/?a=1", :params => {:a => 2})
124
133
  end
125
134
 
126
135
  it "does not modifies query part if no params were given" do
127
- expect(HTTP::Request).to receive(:new) do |_, uri|
128
- expect(uri.query).to eq "deadbeef"
136
+ expect(HTTP::Request).to receive(:new) do |opts|
137
+ expect(opts[:uri].query).to eq "deadbeef"
129
138
  end
130
139
 
131
140
  client.get("http://example.com/?deadbeef")
132
141
  end
133
142
 
134
143
  it "does not corrupts index-less arrays" do
135
- expect(HTTP::Request).to receive(:new) do |_, uri|
136
- expect(CGI.parse uri.query).to eq "a[]" => %w(b c), "d" => %w(e)
144
+ expect(HTTP::Request).to receive(:new) do |opts|
145
+ expect(CGI.parse opts[:uri].query).to eq "a[]" => %w(b c), "d" => %w(e)
137
146
  end
138
147
 
139
148
  client.get("http://example.com/?a[]=b&a[]=c", :params => {:d => "e"})
140
149
  end
141
150
 
142
151
  it "properly encodes colons" do
143
- expect(HTTP::Request).to receive(:new) do |_, uri|
144
- expect(uri.query).to eq "t=1970-01-01T00%3A00%3A00Z"
152
+ expect(HTTP::Request).to receive(:new) do |opts|
153
+ expect(opts[:uri].query).to eq "t=1970-01-01T00%3A00%3A00Z"
145
154
  end
146
155
 
147
156
  client.get("http://example.com/", :params => {:t => "1970-01-01T00:00:00Z"})
@@ -153,8 +162,8 @@ RSpec.describe HTTP::Client do
153
162
  client = HTTP::Client.new
154
163
  allow(client).to receive(:perform)
155
164
 
156
- expect(HTTP::Request).to receive(:new) do |*args|
157
- expect(args.last).to eq('{"foo":"bar"}')
165
+ expect(HTTP::Request).to receive(:new) do |opts|
166
+ expect(opts[:body]).to eq '{"foo":"bar"}'
158
167
  end
159
168
 
160
169
  client.get("http://example.com/", :json => {:foo => :bar})
@@ -53,8 +53,10 @@ RSpec.describe HTTP::Options, "merge" do
53
53
  :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080},
54
54
  :follow => nil,
55
55
  :socket_class => described_class.default_socket_class,
56
+ :nodelay => false,
56
57
  :ssl_socket_class => described_class.default_ssl_socket_class,
57
58
  :ssl_context => nil,
58
- :cookies => {})
59
+ :cookies => {},
60
+ :encoding => nil)
59
61
  end
60
62
  end
@@ -1,9 +1,8 @@
1
1
  RSpec.describe HTTP::Options do
2
2
  subject { described_class.new(:response => :body) }
3
3
 
4
- it "behaves like a Hash for reading" do
5
- expect(subject[:response]).to eq(:body)
6
- expect(subject[:nosuchone]).to be nil
4
+ it "has reader methods for attributes" do
5
+ expect(subject.response).to eq(:body)
7
6
  end
8
7
 
9
8
  it "coerces to a Hash" do