em-http-request 0.3.0 → 1.0.0.beta.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.

Potentially problematic release.


This version of em-http-request might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/.gitignore +1 -0
  2. data/Changelog.md +10 -0
  3. data/README.md +43 -160
  4. data/Rakefile +2 -73
  5. data/em-http-request.gemspec +7 -7
  6. data/examples/fetch.rb +30 -30
  7. data/examples/fibered-http.rb +38 -38
  8. data/examples/oauth-tweet.rb +49 -49
  9. data/lib/em-http.rb +4 -6
  10. data/lib/em-http/client.rb +101 -522
  11. data/lib/em-http/http_connection.rb +125 -0
  12. data/lib/em-http/http_encoding.rb +19 -12
  13. data/lib/em-http/http_header.rb +2 -17
  14. data/lib/em-http/http_options.rb +37 -19
  15. data/lib/em-http/request.rb +33 -66
  16. data/lib/em-http/version.rb +2 -2
  17. data/spec/client_spec.rb +575 -0
  18. data/spec/dns_spec.rb +41 -0
  19. data/spec/encoding_spec.rb +6 -6
  20. data/spec/external_spec.rb +99 -0
  21. data/spec/fixtures/google.ca +13 -17
  22. data/spec/helper.rb +17 -8
  23. data/spec/http_proxy_spec.rb +53 -0
  24. data/spec/middleware_spec.rb +114 -0
  25. data/spec/multi_spec.rb +11 -38
  26. data/spec/pipelining_spec.rb +38 -0
  27. data/spec/redirect_spec.rb +114 -0
  28. data/spec/socksify_proxy_spec.rb +24 -0
  29. data/spec/ssl_spec.rb +20 -0
  30. data/spec/stallion.rb +7 -63
  31. metadata +59 -39
  32. data/examples/websocket-handler.rb +0 -28
  33. data/examples/websocket-server.rb +0 -8
  34. data/ext/buffer/em_buffer.c +0 -639
  35. data/ext/buffer/extconf.rb +0 -53
  36. data/ext/http11_client/ext_help.h +0 -14
  37. data/ext/http11_client/extconf.rb +0 -6
  38. data/ext/http11_client/http11_client.c +0 -328
  39. data/ext/http11_client/http11_parser.c +0 -418
  40. data/ext/http11_client/http11_parser.h +0 -48
  41. data/ext/http11_client/http11_parser.rl +0 -170
  42. data/lib/em-http/mock.rb +0 -137
  43. data/spec/mock_spec.rb +0 -166
  44. data/spec/request_spec.rb +0 -1003
@@ -0,0 +1,125 @@
1
+ module EventMachine
2
+
3
+ module HTTPMethods
4
+ def get options = {}, &blk; setup_request(:get, options, &blk); end
5
+ def head options = {}, &blk; setup_request(:head, options, &blk); end
6
+ def delete options = {}, &blk; setup_request(:delete,options, &blk); end
7
+ def put options = {}, &blk; setup_request(:put, options, &blk); end
8
+ def post options = {}, &blk; setup_request(:post, options, &blk); end
9
+ end
10
+
11
+ class FailedConnection
12
+ include HTTPMethods
13
+ include Deferrable
14
+
15
+ attr_accessor :error, :opts
16
+
17
+ def initialize(req)
18
+ @opts = req
19
+ end
20
+
21
+ def setup_request(method, options)
22
+ c = HttpClient.new(self, HttpOptions.new(@opts.uri, options, method), options)
23
+ c.close(@error)
24
+ c
25
+ end
26
+ end
27
+
28
+ class HttpConnection < Connection
29
+ include HTTPMethods
30
+ include Deferrable
31
+ include Socksify
32
+
33
+ attr_accessor :error, :opts
34
+
35
+ def setup_request(method, options = {})
36
+ c = HttpClient.new(self, HttpOptions.new(@opts.uri, options, method), options)
37
+ callback { c.connection_completed }
38
+
39
+ middleware.each do |m|
40
+ c.callback &m.method(:response) if m.respond_to?(:response)
41
+ end
42
+
43
+ @clients.push c
44
+ c
45
+ end
46
+
47
+ def middleware
48
+ [HttpRequest.middleware, @middleware].flatten
49
+ end
50
+
51
+ def post_init
52
+ @clients = []
53
+ @pending = []
54
+
55
+ @middleware = []
56
+
57
+ @p = Http::Parser.new
58
+ @p.on_headers_complete = proc do |h|
59
+ @clients.first.parse_response_header(h, @p.http_version, @p.status_code)
60
+ end
61
+
62
+ @p.on_body = proc do |b|
63
+ @clients.first.on_body_data(b)
64
+ end
65
+
66
+ @p.on_message_complete = proc do
67
+ if not @clients.first.continue?
68
+ c = @clients.shift
69
+ c.state = :finished
70
+ c.on_request_complete
71
+ end
72
+ end
73
+ end
74
+
75
+ def use(klass)
76
+ @middleware << klass
77
+ end
78
+
79
+ def receive_data(data)
80
+ @p << data
81
+ end
82
+
83
+ def connection_completed
84
+ if @opts.proxy && @opts.proxy[:type] == :socks5
85
+ socksify(@opts.uri.host, @opts.uri.port, *@opts.proxy[:authorization]) { start }
86
+ else
87
+ start
88
+ end
89
+ end
90
+
91
+ def start
92
+ ssl = @opts.options[:tls] || @opts.options[:ssl] || {}
93
+ start_tls(ssl) if @opts.uri.scheme == "https" or @opts.uri.port == 443
94
+
95
+ succeed
96
+ end
97
+
98
+ def redirect(client, location)
99
+ client.req.set_uri(location)
100
+ @pending.push client
101
+ rescue Exception => e
102
+ client.on_error(e.message)
103
+ end
104
+
105
+ def unbind
106
+ @clients.map {|c| c.unbind }
107
+
108
+ if r = @pending.shift
109
+ @clients.push r
110
+
111
+ r.reset!
112
+ @p.reset!
113
+
114
+ begin
115
+ set_deferred_status :unknown
116
+ reconnect(r.req.host, r.req.port)
117
+ callback { r.connection_completed }
118
+ rescue EventMachine::ConnectionError => e
119
+ @clients.pop.close(e.message)
120
+ end
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -3,14 +3,24 @@ module EventMachine
3
3
  HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
4
4
  FIELD_ENCODING = "%s: %s\r\n"
5
5
 
6
- # Escapes a URI.
7
6
  def escape(s)
8
- EscapeUtils.escape_url(s.to_s)
7
+ if defined?(EscapeUtils)
8
+ EscapeUtils.escape_url(s.to_s)
9
+ else
10
+ s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
11
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
12
+ }
13
+ end
9
14
  end
10
15
 
11
- # Unescapes a URI escaped string.
12
16
  def unescape(s)
13
- EscapeUtils.unescape_url(s.to_s)
17
+ if defined?(EscapeUtils)
18
+ EscapeUtils.unescape_url(s.to_s)
19
+ else
20
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) {
21
+ [$1.delete('%')].pack('H*')
22
+ }
23
+ end
14
24
  end
15
25
 
16
26
  if ''.respond_to?(:bytesize)
@@ -28,14 +38,11 @@ module EventMachine
28
38
  head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
29
39
  end
30
40
 
31
- # HTTP is kind of retarded that you have to specify a Host header, but if
32
- # you include port 80 then further redirects will tack on the :80 which is
33
- # annoying.
34
41
  def encode_host
35
- if @uri.port == 80 || @uri.port == 443
36
- return @uri.host
42
+ if @req.uri.port == 80 || @req.uri.port == 443
43
+ return @req.uri.host
37
44
  else
38
- @uri.host + ":#{@uri.port}"
45
+ @req.uri.host + ":#{@req.uri.port}"
39
46
  end
40
47
  end
41
48
 
@@ -44,7 +51,7 @@ module EventMachine
44
51
 
45
52
  # Non CONNECT proxies require that you provide the full request
46
53
  # uri in request header, as opposed to a relative path.
47
- query = uri.join(query) if proxy && proxy[:type] != :socks && !proxy[:use_connect]
54
+ query = uri.join(query) if proxy && proxy[:type] != :socks
48
55
 
49
56
  HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
50
57
  end
@@ -105,7 +112,7 @@ module EventMachine
105
112
  # String - custom auth string (OAuth, etc)
106
113
  def encode_auth(k,v)
107
114
  if v.is_a? Array
108
- FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).chomp].join(" ")]
115
+ FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")]
109
116
  else
110
117
  encode_field(k,v)
111
118
  end
@@ -41,7 +41,7 @@ module EventMachine
41
41
  /chunked/i === self[HttpClient::TRANSFER_ENCODING]
42
42
  end
43
43
 
44
- def keep_alive?
44
+ def keepalive?
45
45
  /keep-alive/i === self[HttpClient::KEEP_ALIVE]
46
46
  end
47
47
 
@@ -53,19 +53,4 @@ module EventMachine
53
53
  self[HttpClient::LOCATION]
54
54
  end
55
55
  end
56
-
57
- class HttpChunkHeader < Hash
58
- # When parsing chunked encodings this is set
59
- attr_accessor :http_chunk_size
60
-
61
- def initialize
62
- super
63
- @http_chunk_size = '0'
64
- end
65
-
66
- # Size of the chunk as an integer
67
- def chunk_size
68
- @http_chunk_size.to_i(base=16)
69
- end
70
- end
71
- end
56
+ end
@@ -1,35 +1,53 @@
1
1
  class HttpOptions
2
2
  attr_reader :uri, :method, :host, :port, :options
3
3
 
4
- def initialize(method, uri, options)
5
- uri.path = '/' if uri.path.empty?
6
-
4
+ def initialize(uri, options, method = :none)
7
5
  @options = options
8
6
  @method = method.to_s.upcase
9
- @uri = uri
10
7
 
11
- if proxy = options[:proxy]
12
- @host = proxy[:host]
13
- @port = proxy[:port]
14
- else
15
- # optional host for cases where you may have
16
- # pre-resolved the host, or you need an override
17
- @host = options.delete(:host) || uri.host
18
- @port = uri.port
19
- end
8
+ set_uri(uri)
20
9
 
21
- @options[:timeout] ||= 10 # default connect & inactivity timeouts
22
- @options[:redirects] ||= 0 # default number of redirects to follow
23
10
  @options[:keepalive] ||= false # default to single request per connection
11
+ @options[:redirects] ||= 0 # default number of redirects to follow
12
+ @options[:followed] ||= 0 # keep track of number of followed requests
13
+
14
+ @options[:connect_timeout] ||= 5 # default connection setup timeout
15
+ @options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout
16
+ end
17
+
18
+ def proxy
19
+ @options[:proxy]
20
+ end
21
+
22
+ def follow_redirect?
23
+ @options[:followed] < @options[:redirects]
24
+ end
25
+
26
+ def set_uri(uri)
27
+ uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
28
+
29
+ uri.path = '/' if uri.path.empty?
30
+ if path = @options.delete(:path)
31
+ uri.path = path
32
+ end
33
+
34
+ @uri = uri
24
35
 
25
36
  # Make sure the ports are set as Addressable::URI doesn't
26
37
  # set the port if it isn't there
27
- if uri.scheme == "https"
38
+ if @uri.scheme == "https"
28
39
  @uri.port ||= 443
29
- @port ||= 443
30
40
  else
31
41
  @uri.port ||= 80
32
- @port ||= 80
33
42
  end
43
+
44
+ if proxy = @options[:proxy]
45
+ @host = proxy[:host]
46
+ @port = proxy[:port]
47
+ else
48
+ @host = @uri.host
49
+ @port = @uri.port
50
+ end
51
+
34
52
  end
35
- end
53
+ end
@@ -1,77 +1,44 @@
1
1
  module EventMachine
2
-
3
- # EventMachine based HTTP request class with support for streaming consumption
4
- # of the response. Response is parsed with a Ragel-generated whitelist parser
5
- # which supports chunked HTTP encoding.
6
- #
7
- # == Example
8
- #
9
- # EventMachine.run {
10
- # http = EventMachine::HttpRequest.new('http://127.0.0.1/').get :query => {'keyname' => 'value'}
11
- #
12
- # http.callback {
13
- # p http.response_header.status
14
- # p http.response_header
15
- # p http.response
16
- #
17
- # EventMachine.stop
18
- # }
19
- # }
20
- #
21
-
22
2
  class HttpRequest
3
+ @middleware = []
23
4
 
24
- attr_reader :options, :method
25
-
26
- def initialize(host)
27
- @uri = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host.to_s)
28
- end
29
-
30
- # Send an HTTP request and consume the response. Supported options:
31
- #
32
- # head: {Key: Value}
33
- # Specify an HTTP header, e.g. {'Connection': 'close'}
34
- #
35
- # query: {Key: Value}
36
- # Specify query string parameters (auto-escaped)
37
- #
38
- # body: String
39
- # Specify the request body (you must encode it for now)
40
- #
41
- # on_response: Proc
42
- # Called for each response body chunk (you may assume HTTP 200
43
- # OK then)
44
- #
5
+ def self.new(uri, options={})
6
+ begin
7
+ req = HttpOptions.new(uri, options)
45
8
 
46
- def get options = {}, &blk; setup_request(:get, options, &blk); end
47
- def head options = {}, &blk; setup_request(:head, options, &blk); end
48
- def delete options = {}, &blk; setup_request(:delete,options, &blk); end
49
- def put options = {}, &blk; setup_request(:put, options, &blk); end
50
- def post options = {}, &blk; setup_request(:post, options, &blk); end
9
+ EventMachine.connect(req.host, req.port, HttpConnection) do |c|
10
+ c.opts = req
51
11
 
52
- protected
12
+ c.pending_connect_timeout = req.options[:connect_timeout]
13
+ c.comm_inactivity_timeout = req.options[:inactivity_timeout]
14
+ end
53
15
 
54
- def setup_request(method, options, &blk)
55
- @req = HttpOptions.new(method, @uri, options)
56
- send_request(&blk)
57
- end
58
-
59
- def send_request(&blk)
60
- begin
61
- EventMachine.connect(@req.host, @req.port, EventMachine::HttpClient) { |c|
62
- c.uri = @req.uri
63
- c.method = @req.method
64
- c.options = @req.options
65
- c.comm_inactivity_timeout = @req.options[:timeout]
66
- c.pending_connect_timeout = @req.options[:timeout]
67
- blk.call(c) unless blk.nil?
68
- }
69
16
  rescue EventMachine::ConnectionError => e
70
- conn = EventMachine::HttpClient.new("")
71
- conn.on_error(e.message, true)
72
- conn.uri = @req.uri
17
+ #
18
+ # Currently, this can only fire on initial connection setup
19
+ # since #connect is a synchronous method. Hence, rescue the
20
+ # exception, and return a failed deferred which will immediately
21
+ # fail any client request.
22
+ #
23
+ # Once there is async-DNS, then we'll iterate over the outstanding
24
+ # client requests and fail them in order.
25
+ #
26
+ # Net outcome: failed connection will invoke the same ConnectionError
27
+ # message on the connection deferred, and on the client deferred.
28
+ #
29
+ conn = EventMachine::FailedConnection.new(req)
30
+ conn.error = e.message
31
+ conn.fail
73
32
  conn
74
33
  end
75
34
  end
35
+
36
+ def self.use(klass)
37
+ @middleware << klass
38
+ end
39
+
40
+ def self.middleware
41
+ @middleware
42
+ end
76
43
  end
77
- end
44
+ end
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  class HttpRequest
3
- VERSION = "0.3.0"
3
+ VERSION = "1.0.0.beta.1"
4
4
  end
5
- end
5
+ end
@@ -0,0 +1,575 @@
1
+ require 'helper'
2
+
3
+ describe EventMachine::HttpRequest do
4
+
5
+ def failed(http=nil)
6
+ EventMachine.stop
7
+ http ? fail(http.error) : fail
8
+ end
9
+
10
+ it "should perform successful GET" do
11
+ EventMachine.run {
12
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
13
+
14
+ http.errback { failed(http) }
15
+ http.callback {
16
+ http.response_header.status.should == 200
17
+ http.response.should match(/Hello/)
18
+ EventMachine.stop
19
+ }
20
+ }
21
+ end
22
+
23
+ it "should perform successful GET with a URI passed as argument" do
24
+ EventMachine.run {
25
+ uri = URI.parse('http://127.0.0.1:8090/')
26
+ http = EventMachine::HttpRequest.new(uri).get
27
+
28
+ http.errback { failed(http) }
29
+ http.callback {
30
+ http.response_header.status.should == 200
31
+ http.response.should match(/Hello/)
32
+ EventMachine.stop
33
+ }
34
+ }
35
+ end
36
+
37
+ it "should succeed GET on missing path" do
38
+ EventMachine.run {
39
+ lambda {
40
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090').get
41
+ http.callback {
42
+ http.response.should match(/Hello/)
43
+ EventMachine.stop
44
+ }
45
+ }.should_not raise_error(ArgumentError)
46
+
47
+ }
48
+ end
49
+
50
+ it "should raise error on invalid URL" do
51
+ EventMachine.run {
52
+ lambda {
53
+ EventMachine::HttpRequest.new('random?text').get
54
+ }.should raise_error
55
+
56
+ EM.stop
57
+ }
58
+ end
59
+
60
+ it "should perform successful HEAD with a URI passed as argument" do
61
+ EventMachine.run {
62
+ uri = URI.parse('http://127.0.0.1:8090/')
63
+ http = EventMachine::HttpRequest.new(uri).head
64
+
65
+ http.errback { failed(http) }
66
+ http.callback {
67
+ http.response_header.status.should == 200
68
+ http.response.should == ""
69
+ EventMachine.stop
70
+ }
71
+ }
72
+ end
73
+
74
+ it "should perform successful DELETE with a URI passed as argument" do
75
+ EventMachine.run {
76
+ uri = URI.parse('http://127.0.0.1:8090/')
77
+ http = EventMachine::HttpRequest.new(uri).delete
78
+
79
+ http.errback { failed(http) }
80
+ http.callback {
81
+ http.response_header.status.should == 200
82
+ http.response.should == ""
83
+ EventMachine.stop
84
+ }
85
+ }
86
+ end
87
+
88
+ it "should return 404 on invalid path" do
89
+ EventMachine.run {
90
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/fail').get
91
+
92
+ http.errback { failed(http) }
93
+ http.callback {
94
+ http.response_header.status.should == 404
95
+ EventMachine.stop
96
+ }
97
+ }
98
+ end
99
+
100
+ it "should build query parameters from Hash" do
101
+ EventMachine.run {
102
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => {:q => 'test'}
103
+
104
+ http.errback { failed(http) }
105
+ http.callback {
106
+ http.response_header.status.should == 200
107
+ http.response.should match(/test/)
108
+ EventMachine.stop
109
+ }
110
+ }
111
+ end
112
+
113
+ it "should pass query parameters string" do
114
+ EventMachine.run {
115
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :query => "q=test"
116
+
117
+ http.errback { failed(http) }
118
+ http.callback {
119
+ http.response_header.status.should == 200
120
+ http.response.should match(/test/)
121
+ EventMachine.stop
122
+ }
123
+ }
124
+ end
125
+
126
+ it "should encode an array of query parameters" do
127
+ EventMachine.run {
128
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get :query => {:hash =>['value1','value2']}
129
+
130
+ http.errback { failed(http) }
131
+ http.callback {
132
+ http.response_header.status.should == 200
133
+ http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
134
+ EventMachine.stop
135
+ }
136
+ }
137
+ end
138
+
139
+ it "should perform successful PUT" do
140
+ EventMachine.run {
141
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').put :body => "data"
142
+
143
+ http.errback { failed(http) }
144
+ http.callback {
145
+ http.response_header.status.should == 200
146
+ http.response.should match(/data/)
147
+ EventMachine.stop
148
+ }
149
+ }
150
+ end
151
+
152
+ it "should perform successful POST" do
153
+ EventMachine.run {
154
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => "data"
155
+
156
+ http.errback { failed(http) }
157
+ http.callback {
158
+ http.response_header.status.should == 200
159
+ http.response.should match(/data/)
160
+ EventMachine.stop
161
+ }
162
+ }
163
+ end
164
+
165
+ it "should escape body on POST" do
166
+ EventMachine.run {
167
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {:stuff => 'string&string'}
168
+
169
+ http.errback { failed(http) }
170
+ http.callback {
171
+ http.response_header.status.should == 200
172
+ http.response.should == "stuff=string%26string"
173
+ EventMachine.stop
174
+ }
175
+ }
176
+ end
177
+
178
+ it "should perform successful POST with Ruby Hash/Array as params" do
179
+ EventMachine.run {
180
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :body => {"key1" => 1, "key2" => [2,3]}
181
+
182
+ http.errback { failed(http) }
183
+ http.callback {
184
+ http.response_header.status.should == 200
185
+
186
+ http.response.should match(/key1=1&key2\[0\]=2&key2\[1\]=3/)
187
+ EventMachine.stop
188
+ }
189
+ }
190
+ end
191
+
192
+ it "should perform successful POST with Ruby Hash/Array as params and with the correct content length" do
193
+ EventMachine.run {
194
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_length').post :body => {"key1" => "data1"}
195
+
196
+ http.errback { failed(http) }
197
+ http.callback {
198
+ http.response_header.status.should == 200
199
+
200
+ http.response.to_i.should == 10
201
+ EventMachine.stop
202
+ }
203
+ }
204
+ end
205
+
206
+ it "should perform successful GET with custom header" do
207
+ EventMachine.run {
208
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :head => {'if-none-match' => 'evar!'}
209
+
210
+ http.errback { p http; failed(http) }
211
+ http.callback {
212
+ http.response_header.status.should == 304
213
+ EventMachine.stop
214
+ }
215
+ }
216
+ end
217
+
218
+ it "should perform basic auth" do
219
+ EventMachine.run {
220
+
221
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get :head => {'authorization' => ['user', 'pass']}
222
+
223
+ http.errback { failed(http) }
224
+ http.callback {
225
+ http.response_header.status.should == 200
226
+ EventMachine.stop
227
+ }
228
+ }
229
+ end
230
+
231
+ it "should remove all newlines from long basic auth header" do
232
+ EventMachine.run {
233
+ auth = {'authorization' => ['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz']}
234
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => auth
235
+ http.errback { failed(http) }
236
+ http.callback {
237
+ http.response_header.status.should == 200
238
+ http.response.should == "Basic YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhOnp6enp6enp6enp6enp6enp6enp6enp6enp6enp6eg=="
239
+ EventMachine.stop
240
+ }
241
+ }
242
+ end
243
+
244
+ it "should send proper OAuth auth header" do
245
+ EventMachine.run {
246
+ oauth_header = 'OAuth oauth_nonce="oqwgSYFUD87MHmJJDv7bQqOF2EPnVus7Wkqj5duNByU", b=c, d=e'
247
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/auth').get :head => {
248
+ 'authorization' => oauth_header
249
+ }
250
+
251
+ http.errback { failed(http) }
252
+ http.callback {
253
+ http.response_header.status.should == 200
254
+ http.response.should == oauth_header
255
+ EventMachine.stop
256
+ }
257
+ }
258
+ end
259
+
260
+ it "should return ETag and Last-Modified headers" do
261
+ EventMachine.run {
262
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_query').get
263
+
264
+ http.errback { failed(http) }
265
+ http.callback {
266
+ http.response_header.status.should == 200
267
+ http.response_header.etag.should match('abcdefg')
268
+ http.response_header.last_modified.should match('Fri, 13 Aug 2010 17:31:21 GMT')
269
+ EventMachine.stop
270
+ }
271
+ }
272
+ end
273
+
274
+ it "should detect deflate encoding" do
275
+ EventMachine.run {
276
+
277
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => {"accept-encoding" => "deflate"}
278
+
279
+ http.errback { failed(http) }
280
+ http.callback {
281
+ http.response_header.status.should == 200
282
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
283
+ http.response.should == "compressed"
284
+
285
+ EventMachine.stop
286
+ }
287
+ }
288
+ end
289
+
290
+ it "should detect gzip encoding" do
291
+ EventMachine.run {
292
+
293
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/gzip').get :head => {
294
+ "accept-encoding" => "gzip, compressed"
295
+ }
296
+
297
+ http.errback { failed(http) }
298
+ http.callback {
299
+ http.response_header.status.should == 200
300
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
301
+ http.response.should == "compressed"
302
+
303
+ EventMachine.stop
304
+ }
305
+ }
306
+ end
307
+
308
+ it "should timeout after 0.1 seconds of inactivity" do
309
+ EventMachine.run {
310
+ t = Time.now.to_i
311
+ EventMachine.heartbeat_interval = 0.1
312
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/timeout', :inactivity_timeout => 0.1).get
313
+
314
+ http.errback {
315
+ (Time.now.to_i - t).should <= 1
316
+ EventMachine.stop
317
+ }
318
+ http.callback { failed(http) }
319
+ }
320
+ end
321
+
322
+ it "should complete a Location: with a relative path" do
323
+ EventMachine.run {
324
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/relative-location').get
325
+
326
+ http.errback { failed(http) }
327
+ http.callback {
328
+ http.response_header['LOCATION'].should == 'http://127.0.0.1:8090/forwarded'
329
+ EventMachine.stop
330
+ }
331
+ }
332
+ end
333
+
334
+ context "body content-type encoding" do
335
+ it "should not set content type on string in body" do
336
+ EventMachine.run {
337
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => "data"
338
+
339
+ http.errback { failed(http) }
340
+ http.callback {
341
+ http.response_header.status.should == 200
342
+ http.response.should be_empty
343
+ EventMachine.stop
344
+ }
345
+ }
346
+ end
347
+
348
+ it "should set content-type automatically when passed a ruby hash/array for body" do
349
+ EventMachine.run {
350
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post :body => {:a => :b}
351
+
352
+ http.errback { failed(http) }
353
+ http.callback {
354
+ http.response_header.status.should == 200
355
+ http.response.should match("application/x-www-form-urlencoded")
356
+ EventMachine.stop
357
+ }
358
+ }
359
+ end
360
+
361
+ it "should not override content-type when passing in ruby hash/array for body" do
362
+ EventMachine.run {
363
+ ct = 'text; charset=utf-8'
364
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
365
+ :body => {:a => :b}, :head => {'content-type' => ct}})
366
+
367
+ http.errback { failed(http) }
368
+ http.callback {
369
+ http.response_header.status.should == 200
370
+ http.content_charset.should == Encoding.find('utf-8')
371
+ http.response_header["CONTENT_TYPE"].should == ct
372
+ EventMachine.stop
373
+ }
374
+ }
375
+ end
376
+
377
+ it "should default to external encoding on invalid encoding" do
378
+ EventMachine.run {
379
+ ct = 'text/html; charset=utf-8lias'
380
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
381
+ :body => {:a => :b}, :head => {'content-type' => ct}})
382
+
383
+ http.errback { failed(http) }
384
+ http.callback {
385
+ http.response_header.status.should == 200
386
+ http.content_charset.should == Encoding.find('utf-8')
387
+ http.response_header["CONTENT_TYPE"].should == ct
388
+ EventMachine.stop
389
+ }
390
+ }
391
+ end
392
+
393
+ it "should processed escaped content-type" do
394
+ EventMachine.run {
395
+ ct = "text/html; charset=\"ISO-8859-4\""
396
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_content_type').post({
397
+ :body => {:a => :b}, :head => {'content-type' => ct}})
398
+
399
+ http.errback { failed(http) }
400
+ http.callback {
401
+ http.response_header.status.should == 200
402
+ http.content_charset.should == Encoding.find('ISO-8859-4')
403
+ http.response_header["CONTENT_TYPE"].should == ct
404
+ EventMachine.stop
405
+ }
406
+ }
407
+ end
408
+ end
409
+
410
+ context "optional header callback" do
411
+ it "should optionally pass the response headers" do
412
+ EventMachine.run {
413
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
414
+
415
+ http.errback { failed(http) }
416
+ http.headers { |hash|
417
+ hash.should be_an_kind_of Hash
418
+ hash.should include 'CONNECTION'
419
+ hash.should include 'CONTENT_LENGTH'
420
+ }
421
+
422
+ http.callback {
423
+ http.response_header.status.should == 200
424
+ http.response.should match(/Hello/)
425
+ EventMachine.stop
426
+ }
427
+ }
428
+ end
429
+
430
+ it "should allow to terminate current connection from header callback" do
431
+ EventMachine.run {
432
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
433
+
434
+ http.callback { failed(http) }
435
+ http.headers { |hash|
436
+ hash.should be_an_kind_of Hash
437
+ hash.should include 'CONNECTION'
438
+ hash.should include 'CONTENT_LENGTH'
439
+
440
+ http.close('header callback terminated connection')
441
+ }
442
+
443
+ http.errback { |e|
444
+ http.response_header.status.should == 200
445
+ http.error.should == 'header callback terminated connection'
446
+ http.response.should == ''
447
+ EventMachine.stop
448
+ }
449
+ }
450
+ end
451
+ end
452
+
453
+ it "should optionally pass the response body progressively" do
454
+ EventMachine.run {
455
+ body = ''
456
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').get
457
+
458
+ http.errback { failed(http) }
459
+ http.stream { |chunk| body += chunk }
460
+
461
+ http.callback {
462
+ http.response_header.status.should == 200
463
+ http.response.should == ''
464
+ body.should match(/Hello/)
465
+ EventMachine.stop
466
+ }
467
+ }
468
+ end
469
+
470
+ it "should optionally pass the deflate-encoded response body progressively" do
471
+ EventMachine.run {
472
+ body = ''
473
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/deflate').get :head => {
474
+ "accept-encoding" => "deflate, compressed"
475
+ }
476
+
477
+ http.errback { failed(http) }
478
+ http.stream { |chunk| body += chunk }
479
+
480
+ http.callback {
481
+ http.response_header.status.should == 200
482
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
483
+ http.response.should == ''
484
+ body.should == "compressed"
485
+ EventMachine.stop
486
+ }
487
+ }
488
+ end
489
+
490
+ it "should accept & return cookie header to user" do
491
+ EventMachine.run {
492
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/set_cookie').get
493
+
494
+ http.errback { failed(http) }
495
+ http.callback {
496
+ http.response_header.status.should == 200
497
+ http.response_header.cookie.should == "id=1; expires=Tue, 09-Aug-2011 17:53:39 GMT; path=/;"
498
+ EventMachine.stop
499
+ }
500
+ }
501
+ end
502
+
503
+ it "should pass cookie header to server from string" do
504
+ EventMachine.run {
505
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => 'id=2;'}
506
+
507
+ http.errback { failed(http) }
508
+ http.callback {
509
+ http.response.should == "id=2;"
510
+ EventMachine.stop
511
+ }
512
+ }
513
+ end
514
+
515
+ it "should pass cookie header to server from Hash" do
516
+ EventMachine.run {
517
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/echo_cookie').get :head => {'cookie' => {'id' => 2}}
518
+
519
+ http.errback { failed(http) }
520
+ http.callback {
521
+ http.response.should == "id=2;"
522
+ EventMachine.stop
523
+ }
524
+ }
525
+ end
526
+
527
+ context "when talking to a stub HTTP/1.0 server" do
528
+ it "should get the body without Content-Length" do
529
+
530
+ EventMachine.run {
531
+ @s = StubServer.new("HTTP/1.0 200 OK\r\nConnection: close\r\n\r\nFoo")
532
+
533
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
534
+ http.errback { failed(http) }
535
+ http.callback {
536
+ http.response.should match(/Foo/)
537
+ http.response_header['CONTENT_LENGTH'].should_not == 0
538
+
539
+ @s.stop
540
+ EventMachine.stop
541
+ }
542
+ }
543
+ end
544
+
545
+ it "should work with \\n instead of \\r\\n" do
546
+ EventMachine.run {
547
+ @s = StubServer.new("HTTP/1.0 200 OK\nContent-Type: text/plain\nContent-Length: 3\nConnection: close\n\nFoo")
548
+
549
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8081/').get
550
+ http.errback { failed(http) }
551
+ http.callback {
552
+ http.response_header.status.should == 200
553
+ http.response_header['CONTENT_TYPE'].should == 'text/plain'
554
+ http.response.should match(/Foo/)
555
+
556
+ @s.stop
557
+ EventMachine.stop
558
+ }
559
+ }
560
+ end
561
+ end
562
+
563
+ it "should stream a file off disk" do
564
+ EventMachine.run {
565
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8090/').post :file => 'spec/fixtures/google.ca'
566
+
567
+ http.errback { failed(http) }
568
+ http.callback {
569
+ http.response.should match('google')
570
+ EventMachine.stop
571
+ }
572
+ }
573
+ end
574
+
575
+ end