igrigorik-em-http-request 0.1.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.
- data/.autotest +1 -0
- data/LICENSE +58 -0
- data/README.rdoc +51 -0
- data/Rakefile +80 -0
- data/ext/buffer/em_buffer.c +630 -0
- data/ext/buffer/extconf.rb +53 -0
- data/ext/http11_client/ext_help.h +14 -0
- data/ext/http11_client/extconf.rb +6 -0
- data/ext/http11_client/http11_client.c +302 -0
- data/ext/http11_client/http11_parser.c +403 -0
- data/ext/http11_client/http11_parser.h +48 -0
- data/ext/http11_client/http11_parser.rl +173 -0
- data/lib/em-http.rb +17 -0
- data/lib/em-http/client.rb +408 -0
- data/lib/em-http/multi.rb +51 -0
- data/lib/em-http/request.rb +63 -0
- data/test/helper.rb +5 -0
- data/test/stallion.rb +123 -0
- data/test/test_multi.rb +34 -0
- data/test/test_request.rb +192 -0
- metadata +82 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
module EventMachine
|
2
|
+
|
3
|
+
# EventMachine based Multi request client, based on a streaming HTTPRequest class,
|
4
|
+
# which allows you to open multiple parallel connections and return only when all
|
5
|
+
# of them finish. (i.e. ideal for parallelizing workloads)
|
6
|
+
#
|
7
|
+
# == Example
|
8
|
+
#
|
9
|
+
# EventMachine.run {
|
10
|
+
#
|
11
|
+
# multi = EventMachine::MultiRequest.new
|
12
|
+
#
|
13
|
+
# # add multiple requests to the multi-handler
|
14
|
+
# multi.add(EventMachine::HttpRequest.new('http://www.google.com/').get)
|
15
|
+
# multi.add(EventMachine::HttpRequest.new('http://www.yahoo.com/').get)
|
16
|
+
#
|
17
|
+
# multi.callback {
|
18
|
+
# p multi.responses[:succeeded]
|
19
|
+
# p multi.responses[:failed]
|
20
|
+
#
|
21
|
+
# EventMachine.stop
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
|
26
|
+
class MultiRequest
|
27
|
+
include EventMachine::Deferrable
|
28
|
+
|
29
|
+
attr_reader :requests, :responses
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@requests = []
|
33
|
+
@responses = {:succeeded => [], :failed => []}
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(conn)
|
37
|
+
conn.callback { @responses[:succeeded].push(conn); check_progress }
|
38
|
+
conn.errback { @responses[:failed].push(conn); check_progress }
|
39
|
+
|
40
|
+
@requests.push(conn)
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
# invoke callback if all requests have completed
|
46
|
+
def check_progress
|
47
|
+
succeed if (@responses[:succeeded].size + @responses[:failed].size) == @requests.size
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
|
6
|
+
# EventMachine based HTTP request class with support for streaming consumption
|
7
|
+
# of the response. Response is parsed with a Ragel-generated whitelist parser
|
8
|
+
# which supports chunked HTTP encoding.
|
9
|
+
#
|
10
|
+
# == Example
|
11
|
+
#
|
12
|
+
#
|
13
|
+
# EventMachine.run {
|
14
|
+
# http = EventMachine::HttpRequest.new('http://127.0.0.1/').get :query => {'keyname' => 'value'}
|
15
|
+
#
|
16
|
+
# http.callback {
|
17
|
+
# p http.response_header.status
|
18
|
+
# p http.response_header
|
19
|
+
# p http.response
|
20
|
+
#
|
21
|
+
# EventMachine.stop
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
#
|
25
|
+
|
26
|
+
class HttpRequest
|
27
|
+
attr_reader :response, :headers
|
28
|
+
|
29
|
+
def initialize(host, headers = {})
|
30
|
+
@headers = headers
|
31
|
+
@uri = URI::parse(host)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Send an HTTP request and consume the response. Supported options:
|
35
|
+
#
|
36
|
+
# head: {Key: Value}
|
37
|
+
# Specify an HTTP header, e.g. {'Connection': 'close'}
|
38
|
+
#
|
39
|
+
# query: {Key: Value}
|
40
|
+
# Specify query string parameters (auto-escaped)
|
41
|
+
#
|
42
|
+
# body: String
|
43
|
+
# Specify the request body (you must encode it for now)
|
44
|
+
#
|
45
|
+
|
46
|
+
def get options = {}; send_request(:get, options); end
|
47
|
+
def post options = {}; send_request(:post, options); end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def send_request(method, options)
|
52
|
+
raise ArgumentError, "invalid request path" unless /^\// === @uri.path
|
53
|
+
|
54
|
+
method = method.to_s.upcase
|
55
|
+
|
56
|
+
EventMachine.connect(@uri.host, @uri.port, EventMachine::HttpClient) { |c|
|
57
|
+
c.uri = @uri
|
58
|
+
c.method = method
|
59
|
+
c.options = options
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/test/helper.rb
ADDED
data/test/stallion.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# #--
|
2
|
+
# Includes portion originally Copyright (C)2008 Michael Fellinger
|
3
|
+
# license See file LICENSE for details
|
4
|
+
# #--
|
5
|
+
|
6
|
+
require 'rack'
|
7
|
+
|
8
|
+
module Stallion
|
9
|
+
class Mount
|
10
|
+
def initialize(name, *methods, &block)
|
11
|
+
@name, @methods, @block = name, methods, block
|
12
|
+
end
|
13
|
+
|
14
|
+
def ride
|
15
|
+
@block.call
|
16
|
+
end
|
17
|
+
|
18
|
+
def match?(request)
|
19
|
+
method = request['REQUEST_METHOD']
|
20
|
+
right_method = @methods.empty? or @methods.include?(method)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Stable
|
25
|
+
attr_reader :request, :response
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@boxes = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def in(path, *methods, &block)
|
32
|
+
mount = Mount.new(path, *methods, &block)
|
33
|
+
@boxes[[path, methods]] = mount
|
34
|
+
mount
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(request, response)
|
38
|
+
@request, @response = request, response
|
39
|
+
@boxes.each do |(path, methods), mount|
|
40
|
+
if mount.match?(request)
|
41
|
+
mount.ride
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
STABLES = {}
|
48
|
+
|
49
|
+
def self.saddle(name = nil)
|
50
|
+
STABLES[name] = stable = Stable.new
|
51
|
+
yield stable
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.run(options = {})
|
55
|
+
options = {:Host => "127.0.0.1", :Port => 8080}.merge(options)
|
56
|
+
Rack::Handler::Mongrel.run(Rack::Lint.new(self), options)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.call(env)
|
60
|
+
request = Rack::Request.new(env)
|
61
|
+
response = Rack::Response.new
|
62
|
+
|
63
|
+
STABLES.each do |name, stable|
|
64
|
+
stable.call(request, response)
|
65
|
+
end
|
66
|
+
|
67
|
+
response.finish
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Stallion.saddle :spec do |stable|
|
72
|
+
stable.in '/' do
|
73
|
+
|
74
|
+
if stable.request.path_info == '/fail'
|
75
|
+
stable.response.status = 404
|
76
|
+
|
77
|
+
elsif stable.request.query_string == 'q=test'
|
78
|
+
stable.response.write 'test'
|
79
|
+
|
80
|
+
elsif stable.request.path_info == '/echo_query'
|
81
|
+
stable.response.write stable.request.query_string
|
82
|
+
|
83
|
+
elsif stable.request.post?
|
84
|
+
stable.response.write 'test'
|
85
|
+
|
86
|
+
elsif stable.request.path_info == '/gzip'
|
87
|
+
io = StringIO.new
|
88
|
+
gzip = Zlib::GzipWriter.new(io)
|
89
|
+
gzip << "compressed"
|
90
|
+
gzip.close
|
91
|
+
|
92
|
+
stable.response.write io.string
|
93
|
+
stable.response["Content-Encoding"] = "gzip"
|
94
|
+
|
95
|
+
elsif stable.request.path_info == '/deflate'
|
96
|
+
stable.response.write Zlib::Deflate.deflate("compressed")
|
97
|
+
stable.response["Content-Encoding"] = "deflate"
|
98
|
+
|
99
|
+
elsif stable.request.env["HTTP_IF_NONE_MATCH"]
|
100
|
+
stable.response.status = 304
|
101
|
+
|
102
|
+
elsif stable.request.env["HTTP_AUTHORIZATION"]
|
103
|
+
auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
|
104
|
+
|
105
|
+
if auth == stable.request.env["HTTP_AUTHORIZATION"]
|
106
|
+
stable.response.status = 200
|
107
|
+
stable.response.write 'success'
|
108
|
+
else
|
109
|
+
stable.response.status = 401
|
110
|
+
end
|
111
|
+
|
112
|
+
elsif
|
113
|
+
stable.response.write 'Hello, World!'
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
Thread.new do
|
120
|
+
Stallion.run :Host => '127.0.0.1', :Port => 8080
|
121
|
+
end
|
122
|
+
|
123
|
+
sleep(2)
|
data/test/test_multi.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
require 'test/stallion'
|
3
|
+
|
4
|
+
describe EventMachine::MultiRequest do
|
5
|
+
|
6
|
+
def failed
|
7
|
+
EventMachine.stop
|
8
|
+
fail
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should submit multiple requests in parallel and return once all of them are complete" do
|
12
|
+
EventMachine.run {
|
13
|
+
|
14
|
+
# create an instance of multi-request handler, and the requests themselves
|
15
|
+
multi = EventMachine::MultiRequest.new
|
16
|
+
|
17
|
+
# add multiple requests to the multi-handler
|
18
|
+
multi.add(EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get(:query => {:q => 'test'}))
|
19
|
+
multi.add(EventMachine::HttpRequest.new('http://169.169.169.169/').get)
|
20
|
+
|
21
|
+
multi.callback {
|
22
|
+
# verify successfull request
|
23
|
+
multi.responses[:succeeded].size.should == 1
|
24
|
+
multi.responses[:succeeded].first.response.should match(/test/)
|
25
|
+
|
26
|
+
# verify invalid requests
|
27
|
+
multi.responses[:failed].size.should == 1
|
28
|
+
multi.responses[:failed].first.response_header.status.should == 0
|
29
|
+
|
30
|
+
EventMachine.stop
|
31
|
+
}
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'test/helper'
|
2
|
+
require 'test/stallion'
|
3
|
+
|
4
|
+
describe EventMachine::HttpRequest do
|
5
|
+
|
6
|
+
def failed
|
7
|
+
EventMachine.stop
|
8
|
+
fail
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should fail GET on invalid host" do
|
12
|
+
EventMachine.run {
|
13
|
+
http = EventMachine::HttpRequest.new('http://169.169.169.169/').get
|
14
|
+
http.callback { failed }
|
15
|
+
http.errback {
|
16
|
+
http.response_header.status.should == 0
|
17
|
+
EventMachine.stop
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should fail GET on missing path" do
|
23
|
+
EventMachine.run {
|
24
|
+
lambda {
|
25
|
+
EventMachine::HttpRequest.new('http://www.google.com').get
|
26
|
+
}.should raise_error(ArgumentError)
|
27
|
+
|
28
|
+
EventMachine.stop
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should perform successfull GET" do
|
33
|
+
EventMachine.run {
|
34
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get
|
35
|
+
|
36
|
+
http.errback { failed }
|
37
|
+
http.callback {
|
38
|
+
http.response_header.status.should == 200
|
39
|
+
http.response.should match(/Hello/)
|
40
|
+
EventMachine.stop
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should return 404 on invalid path" do
|
46
|
+
EventMachine.run {
|
47
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/fail').get
|
48
|
+
|
49
|
+
http.errback { failed }
|
50
|
+
http.callback {
|
51
|
+
http.response_header.status.should == 404
|
52
|
+
EventMachine.stop
|
53
|
+
}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should build query parameters from Hash" do
|
58
|
+
EventMachine.run {
|
59
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => {:q => 'test'}
|
60
|
+
|
61
|
+
http.errback { failed }
|
62
|
+
http.callback {
|
63
|
+
http.response_header.status.should == 200
|
64
|
+
http.response.should match(/test/)
|
65
|
+
EventMachine.stop
|
66
|
+
}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should pass query parameters string" do
|
71
|
+
EventMachine.run {
|
72
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => "q=test"
|
73
|
+
|
74
|
+
http.errback { failed }
|
75
|
+
http.callback {
|
76
|
+
http.response_header.status.should == 200
|
77
|
+
http.response.should match(/test/)
|
78
|
+
EventMachine.stop
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should encode an array of query parameters" do
|
84
|
+
EventMachine.run {
|
85
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/echo_query').get :query => {:hash => ['value1', 'value2']}
|
86
|
+
|
87
|
+
http.errback { failed }
|
88
|
+
http.callback {
|
89
|
+
http.response_header.status.should == 200
|
90
|
+
http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
|
91
|
+
EventMachine.stop
|
92
|
+
}
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should perform successfull POST" do
|
97
|
+
EventMachine.run {
|
98
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').post :body => "data"
|
99
|
+
|
100
|
+
http.errback { failed }
|
101
|
+
http.callback {
|
102
|
+
http.response_header.status.should == 200
|
103
|
+
http.response.should match(/test/)
|
104
|
+
EventMachine.stop
|
105
|
+
}
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should perform successfull GET with custom header" do
|
110
|
+
EventMachine.run {
|
111
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'if-none-match' => 'evar!'}
|
112
|
+
|
113
|
+
http.errback { failed }
|
114
|
+
http.callback {
|
115
|
+
http.response_header.status.should == 304
|
116
|
+
EventMachine.stop
|
117
|
+
}
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should perform a streaming GET" do
|
122
|
+
EventMachine.run {
|
123
|
+
|
124
|
+
# digg.com uses chunked encoding
|
125
|
+
http = EventMachine::HttpRequest.new('http://www.digg.com/').get
|
126
|
+
|
127
|
+
http.errback { failed }
|
128
|
+
http.callback {
|
129
|
+
http.response_header.status == 200
|
130
|
+
EventMachine.stop
|
131
|
+
}
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should perform basic auth" do
|
136
|
+
EventMachine.run {
|
137
|
+
|
138
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'authorization' => ['user', 'pass']}
|
139
|
+
|
140
|
+
http.errback { failed }
|
141
|
+
http.callback {
|
142
|
+
http.response_header.status == 200
|
143
|
+
EventMachine.stop
|
144
|
+
}
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should work with keep-alive servers" do
|
149
|
+
EventMachine.run {
|
150
|
+
|
151
|
+
http = EventMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get
|
152
|
+
|
153
|
+
http.errback { failed }
|
154
|
+
http.callback {
|
155
|
+
http.response_header.status == 200
|
156
|
+
EventMachine.stop
|
157
|
+
}
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should detect deflate encoding" do
|
162
|
+
EventMachine.run {
|
163
|
+
|
164
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate"}
|
165
|
+
|
166
|
+
http.errback { failed }
|
167
|
+
http.callback {
|
168
|
+
http.response_header.status == 200
|
169
|
+
http.response_header["CONTENT_ENCODING"].should == "deflate"
|
170
|
+
http.response.should == "compressed"
|
171
|
+
|
172
|
+
EventMachine.stop
|
173
|
+
}
|
174
|
+
}
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should detect gzip encoding" do
|
178
|
+
EventMachine.run {
|
179
|
+
|
180
|
+
http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/gzip').get :head => {"accept-encoding" => "gzip, compressed"}
|
181
|
+
|
182
|
+
http.errback { failed }
|
183
|
+
http.callback {
|
184
|
+
http.response_header.status == 200
|
185
|
+
http.response_header["CONTENT_ENCODING"].should == "gzip"
|
186
|
+
http.response.should == "compressed"
|
187
|
+
|
188
|
+
EventMachine.stop
|
189
|
+
}
|
190
|
+
}
|
191
|
+
end
|
192
|
+
end
|