em-http-request 0.3.0 → 1.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.

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