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.
- data/.gitignore +1 -0
- data/Changelog.md +10 -0
- data/README.md +43 -160
- data/Rakefile +2 -73
- data/em-http-request.gemspec +7 -7
- data/examples/fetch.rb +30 -30
- data/examples/fibered-http.rb +38 -38
- data/examples/oauth-tweet.rb +49 -49
- data/lib/em-http.rb +4 -6
- data/lib/em-http/client.rb +101 -522
- data/lib/em-http/http_connection.rb +125 -0
- data/lib/em-http/http_encoding.rb +19 -12
- data/lib/em-http/http_header.rb +2 -17
- data/lib/em-http/http_options.rb +37 -19
- data/lib/em-http/request.rb +33 -66
- data/lib/em-http/version.rb +2 -2
- data/spec/client_spec.rb +575 -0
- data/spec/dns_spec.rb +41 -0
- data/spec/encoding_spec.rb +6 -6
- data/spec/external_spec.rb +99 -0
- data/spec/fixtures/google.ca +13 -17
- data/spec/helper.rb +17 -8
- data/spec/http_proxy_spec.rb +53 -0
- data/spec/middleware_spec.rb +114 -0
- data/spec/multi_spec.rb +11 -38
- data/spec/pipelining_spec.rb +38 -0
- data/spec/redirect_spec.rb +114 -0
- data/spec/socksify_proxy_spec.rb +24 -0
- data/spec/ssl_spec.rb +20 -0
- data/spec/stallion.rb +7 -63
- metadata +59 -39
- data/examples/websocket-handler.rb +0 -28
- data/examples/websocket-server.rb +0 -8
- data/ext/buffer/em_buffer.c +0 -639
- data/ext/buffer/extconf.rb +0 -53
- data/ext/http11_client/ext_help.h +0 -14
- data/ext/http11_client/extconf.rb +0 -6
- data/ext/http11_client/http11_client.c +0 -328
- data/ext/http11_client/http11_parser.c +0 -418
- data/ext/http11_client/http11_parser.h +0 -48
- data/ext/http11_client/http11_parser.rl +0 -170
- data/lib/em-http/mock.rb +0 -137
- data/spec/mock_spec.rb +0 -166
- 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
|
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
|
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
|
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(":")).
|
115
|
+
FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")]
|
109
116
|
else
|
110
117
|
encode_field(k,v)
|
111
118
|
end
|
data/lib/em-http/http_header.rb
CHANGED
@@ -41,7 +41,7 @@ module EventMachine
|
|
41
41
|
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
42
42
|
end
|
43
43
|
|
44
|
-
def
|
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
|
data/lib/em-http/http_options.rb
CHANGED
@@ -1,35 +1,53 @@
|
|
1
1
|
class HttpOptions
|
2
2
|
attr_reader :uri, :method, :host, :port, :options
|
3
3
|
|
4
|
-
def initialize(
|
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
|
-
|
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
|
data/lib/em-http/request.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
71
|
-
|
72
|
-
|
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
|
data/lib/em-http/version.rb
CHANGED
data/spec/client_spec.rb
ADDED
@@ -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
|