astro-em-http-request 0.1.3.20090419

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.
@@ -0,0 +1,119 @@
1
+ require 'zlib'
2
+
3
+ ##
4
+ # Provides a unified callback interface to decompression libraries.
5
+ module EventMachine::HttpDecoders
6
+
7
+ class DecoderError < StandardError
8
+ end
9
+
10
+ class << self
11
+ def accepted_encodings
12
+ DECODERS.inject([]) { |r,d| r + d.encoding_names }
13
+ end
14
+
15
+ def decoder_for_encoding(encoding)
16
+ DECODERS.each { |d|
17
+ return d if d.encoding_names.include? encoding
18
+ }
19
+ nil
20
+ end
21
+ end
22
+
23
+ class Base
24
+ def self.encoding_names
25
+ name = to_s.split('::').last.downcase
26
+ [name]
27
+ end
28
+
29
+ ##
30
+ # chunk_callback:: [Block] To handle a decompressed chunk
31
+ def initialize(&chunk_callback)
32
+ @chunk_callback = chunk_callback
33
+ end
34
+
35
+ def <<(compressed)
36
+ return unless compressed && compressed.size > 0
37
+
38
+ decompressed = decompress(compressed)
39
+ receive_decompressed decompressed
40
+ end
41
+
42
+ def finalize!
43
+ decompressed = finalize
44
+ receive_decompressed decompressed
45
+ end
46
+
47
+ private
48
+
49
+ def receive_decompressed(decompressed)
50
+ if decompressed && decompressed.size > 0
51
+ @chunk_callback.call(decompressed)
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ ##
58
+ # Must return a part of decompressed
59
+ def decompress(compressed)
60
+ nil
61
+ end
62
+
63
+ ##
64
+ # May return last part
65
+ def finalize
66
+ nil
67
+ end
68
+ end
69
+
70
+ class Deflate < Base
71
+ def decompress(compressed)
72
+ begin
73
+ @zstream ||= Zlib::Inflate.new(nil)
74
+ @zstream.inflate(compressed)
75
+ rescue Zlib::Error
76
+ raise DecoderError
77
+ end
78
+ end
79
+
80
+ def finalize
81
+ return nil unless @zstream
82
+
83
+ begin
84
+ r = @zstream.inflate(nil)
85
+ @zstream.close
86
+ r
87
+ rescue Zlib::Error
88
+ raise DecoderError
89
+ end
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Oneshot decompressor, due to lack of a streaming Gzip reader
95
+ # implementation. We may steal code from Zliby to improve this.
96
+ #
97
+ # For now, do not put `gzip' or `compressed' in your accept-encoding
98
+ # header if you expect much data through the :on_response interface.
99
+ class GZip < Base
100
+ def self.encoding_names
101
+ %w(gzip compressed)
102
+ end
103
+
104
+ def decompress(compressed)
105
+ @buf ||= ''
106
+ @buf += compressed
107
+ nil
108
+ end
109
+
110
+ def finalize
111
+ Zlib::GzipReader.new(StringIO.new(@buf)).read
112
+ end
113
+ end
114
+
115
+ DECODERS = [Deflate, GZip]
116
+
117
+ end
118
+
119
+
@@ -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,77 @@
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
+
28
+ def initialize(host, headers = {})
29
+ @headers = headers
30
+ @uri = URI::parse(host) unless host.kind_of? URI
31
+ end
32
+
33
+ # Send an HTTP request and consume the response. Supported options:
34
+ #
35
+ # head: {Key: Value}
36
+ # Specify an HTTP header, e.g. {'Connection': 'close'}
37
+ #
38
+ # query: {Key: Value}
39
+ # Specify query string parameters (auto-escaped)
40
+ #
41
+ # body: String
42
+ # Specify the request body (you must encode it for now)
43
+ #
44
+ # on_response: Proc
45
+ # Called for each response body chunk (you may assume HTTP 200
46
+ # OK then)
47
+ #
48
+ # host: String
49
+ # Manually specify TCP connect host address, independent of
50
+ # Host: header
51
+
52
+ def get options = {}; send_request(:get, options); end
53
+ def post options = {}; send_request(:post, options); end
54
+
55
+ protected
56
+
57
+ def send_request(method, options)
58
+ raise ArgumentError, "invalid request path" unless /^\// === @uri.path
59
+
60
+ method = method.to_s.upcase
61
+ begin
62
+ host = options[:host] || @uri.host
63
+ EventMachine.connect(host, @uri.port, EventMachine::HttpClient) { |c|
64
+ c.uri = @uri
65
+ c.method = method
66
+ c.options = options
67
+ c.comm_inactivity_timeout = options[:timeout] || 5
68
+ }
69
+ rescue RuntimeError => e
70
+ raise e unless e.message == "no connection"
71
+ conn = EventMachine::HttpClient.new("")
72
+ conn.on_error("no connection")
73
+ conn
74
+ end
75
+ end
76
+ end
77
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'pp'
4
+
5
+ require 'lib/em-http'
data/test/stallion.rb ADDED
@@ -0,0 +1,127 @@
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 == '/timeout'
87
+ sleep(10)
88
+ stable.response.write 'timeout'
89
+
90
+ elsif stable.request.path_info == '/gzip'
91
+ io = StringIO.new
92
+ gzip = Zlib::GzipWriter.new(io)
93
+ gzip << "compressed"
94
+ gzip.close
95
+
96
+ stable.response.write io.string
97
+ stable.response["Content-Encoding"] = "gzip"
98
+
99
+ elsif stable.request.path_info == '/deflate'
100
+ stable.response.write Zlib::Deflate.deflate("compressed")
101
+ stable.response["Content-Encoding"] = "deflate"
102
+
103
+ elsif stable.request.env["HTTP_IF_NONE_MATCH"]
104
+ stable.response.status = 304
105
+
106
+ elsif stable.request.env["HTTP_AUTHORIZATION"]
107
+ auth = "Basic %s" % Base64.encode64(['user', 'pass'].join(':')).chomp
108
+
109
+ if auth == stable.request.env["HTTP_AUTHORIZATION"]
110
+ stable.response.status = 200
111
+ stable.response.write 'success'
112
+ else
113
+ stable.response.status = 401
114
+ end
115
+
116
+ elsif
117
+ stable.response.write 'Hello, World!'
118
+ end
119
+
120
+ end
121
+ end
122
+
123
+ Thread.new do
124
+ Stallion.run :Host => '127.0.0.1', :Port => 8080
125
+ end
126
+
127
+ sleep(2)
@@ -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,264 @@
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 DNS timeout" do
12
+ EventMachine.run {
13
+ http = EventMachine::HttpRequest.new('http://127.1.1.1/').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 invalid host" do
23
+ EventMachine.run {
24
+ http = EventMachine::HttpRequest.new('http://google1.com/').get
25
+ http.callback { failed }
26
+ http.errback {
27
+ http.response_header.status.should == 0
28
+ http.errors.should match(/no connection/)
29
+ EventMachine.stop
30
+ }
31
+ }
32
+ end
33
+
34
+ it "should fail GET on missing path" do
35
+ EventMachine.run {
36
+ lambda {
37
+ EventMachine::HttpRequest.new('http://www.google.com').get
38
+ }.should raise_error(ArgumentError)
39
+
40
+ EventMachine.stop
41
+ }
42
+ end
43
+
44
+ it "should perform successfull GET" do
45
+ EventMachine.run {
46
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get
47
+
48
+ http.errback { failed }
49
+ http.callback {
50
+ http.response_header.status.should == 200
51
+ http.response.should match(/Hello/)
52
+ EventMachine.stop
53
+ }
54
+ }
55
+ end
56
+
57
+ it "should return 404 on invalid path" do
58
+ EventMachine.run {
59
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/fail').get
60
+
61
+ http.errback { failed }
62
+ http.callback {
63
+ http.response_header.status.should == 404
64
+ EventMachine.stop
65
+ }
66
+ }
67
+ end
68
+
69
+ it "should build query parameters from Hash" do
70
+ EventMachine.run {
71
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => {:q => 'test'}
72
+
73
+ http.errback { failed }
74
+ http.callback {
75
+ http.response_header.status.should == 200
76
+ http.response.should match(/test/)
77
+ EventMachine.stop
78
+ }
79
+ }
80
+ end
81
+
82
+ it "should pass query parameters string" do
83
+ EventMachine.run {
84
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :query => "q=test"
85
+
86
+ http.errback { failed }
87
+ http.callback {
88
+ http.response_header.status.should == 200
89
+ http.response.should match(/test/)
90
+ EventMachine.stop
91
+ }
92
+ }
93
+ end
94
+
95
+ it "should encode an array of query parameters" do
96
+ EventMachine.run {
97
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/echo_query').get :query => {:hash => ['value1', 'value2']}
98
+
99
+ http.errback { failed }
100
+ http.callback {
101
+ http.response_header.status.should == 200
102
+ http.response.should match(/hash\[\]=value1&hash\[\]=value2/)
103
+ EventMachine.stop
104
+ }
105
+ }
106
+ end
107
+
108
+ it "should perform successfull POST" do
109
+ EventMachine.run {
110
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').post :body => "data"
111
+
112
+ http.errback { failed }
113
+ http.callback {
114
+ http.response_header.status.should == 200
115
+ http.response.should match(/test/)
116
+ EventMachine.stop
117
+ }
118
+ }
119
+ end
120
+
121
+ it "should perform successfull GET with custom header" do
122
+ EventMachine.run {
123
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'if-none-match' => 'evar!'}
124
+
125
+ http.errback { failed }
126
+ http.callback {
127
+ http.response_header.status.should == 304
128
+ EventMachine.stop
129
+ }
130
+ }
131
+ end
132
+
133
+ it "should perform a streaming GET" do
134
+ EventMachine.run {
135
+
136
+ # digg.com uses chunked encoding
137
+ http = EventMachine::HttpRequest.new('http://digg.com/').get
138
+
139
+ http.errback { failed }
140
+ http.callback {
141
+ http.response_header.status.should == 200
142
+ EventMachine.stop
143
+ }
144
+ }
145
+ end
146
+
147
+ it "should perform basic auth" do
148
+ EventMachine.run {
149
+
150
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :head => {'authorization' => ['user', 'pass']}
151
+
152
+ http.errback { failed }
153
+ http.callback {
154
+ http.response_header.status.should == 200
155
+ EventMachine.stop
156
+ }
157
+ }
158
+ end
159
+
160
+ it "should work with keep-alive servers" do
161
+ EventMachine.run {
162
+
163
+ http = EventMachine::HttpRequest.new('http://mexicodiario.com/touch.public.json.php').get
164
+
165
+ http.errback { failed }
166
+ http.callback {
167
+ http.response_header.status.should == 200
168
+ EventMachine.stop
169
+ }
170
+ }
171
+ end
172
+
173
+ it "should detect deflate encoding" do
174
+ EventMachine.run {
175
+
176
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate"}
177
+
178
+ http.errback { failed }
179
+ http.callback {
180
+ http.response_header.status.should == 200
181
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
182
+ http.response.should == "compressed"
183
+
184
+ EventMachine.stop
185
+ }
186
+ }
187
+ end
188
+
189
+ it "should detect gzip encoding" do
190
+ EventMachine.run {
191
+
192
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/gzip').get :head => {"accept-encoding" => "gzip, compressed"}
193
+
194
+ http.errback { failed }
195
+ http.callback {
196
+ http.response_header.status.should == 200
197
+ http.response_header["CONTENT_ENCODING"].should == "gzip"
198
+ http.response.should == "compressed"
199
+
200
+ EventMachine.stop
201
+ }
202
+ }
203
+ end
204
+
205
+ it "should timeout after 10 seconds" do
206
+ EventMachine.run {
207
+ t = Time.now.to_i
208
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/timeout').get :timeout => 2
209
+
210
+ http.errback {
211
+ (Time.now.to_i - t).should == 2
212
+ EventMachine.stop
213
+ }
214
+ http.callback { failed }
215
+ }
216
+ end
217
+
218
+ it "should optionally pass the response body progressively" do
219
+ EventMachine.run {
220
+ body = ''
221
+ on_body = lambda { |chunk| body += chunk }
222
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/').get :on_response => on_body
223
+
224
+ http.errback { failed }
225
+ http.callback {
226
+ http.response_header.status.should == 200
227
+ http.response.should == ''
228
+ body.should match(/Hello/)
229
+ EventMachine.stop
230
+ }
231
+ }
232
+ end
233
+
234
+ it "should optionally pass the deflate-encoded response body progressively" do
235
+ EventMachine.run {
236
+ body = ''
237
+ on_body = lambda { |chunk| body += chunk }
238
+ http = EventMachine::HttpRequest.new('http://127.0.0.1:8080/deflate').get :head => {"accept-encoding" => "deflate, compressed"},
239
+ :on_response => on_body
240
+
241
+ http.errback { failed }
242
+ http.callback {
243
+ http.response_header.status.should == 200
244
+ http.response_header["CONTENT_ENCODING"].should == "deflate"
245
+ http.response.should == ''
246
+ body.should == "compressed"
247
+ EventMachine.stop
248
+ }
249
+ }
250
+ end
251
+
252
+ it "should respect manually-passed host address" do
253
+ EventMachine.run {
254
+ http = EventMachine::HttpRequest.new('http://127.1.1.1:8080/').get :host => '127.0.0.1'
255
+
256
+ http.errback { failed }
257
+ http.callback {
258
+ http.response_header.status.should == 200
259
+ http.response.should match(/Hello/)
260
+ EventMachine.stop
261
+ }
262
+ }
263
+ end
264
+ end