em-http-request 0.2.9 → 0.3.0
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/.gitignore +2 -1
- data/.rspec +0 -0
- data/Changelog.md +54 -0
- data/Gemfile +3 -0
- data/README.md +175 -0
- data/Rakefile +11 -37
- data/em-http-request.gemspec +25 -90
- data/examples/fetch.rb +30 -30
- data/examples/fibered-http.rb +38 -38
- data/examples/oauth-tweet.rb +49 -49
- data/examples/socks5.rb +26 -0
- data/examples/websocket-handler.rb +28 -28
- data/examples/websocket-server.rb +8 -8
- data/lib/em-http/client.rb +242 -207
- data/lib/em-http/http_encoding.rb +135 -0
- data/lib/em-http/http_header.rb +71 -0
- data/lib/em-http/http_options.rb +7 -4
- data/lib/em-http/mock.rb +90 -50
- data/lib/em-http/multi.rb +55 -51
- data/lib/em-http/request.rb +2 -4
- data/lib/em-http/version.rb +5 -0
- data/lib/em-http.rb +19 -19
- data/spec/encoding_spec.rb +40 -0
- data/spec/fixtures/google.ca +20 -21
- data/spec/helper.rb +5 -4
- data/spec/mock_spec.rb +85 -36
- data/spec/multi_spec.rb +68 -51
- data/spec/request_spec.rb +422 -108
- data/spec/stallion.rb +65 -3
- metadata +111 -28
- data/LICENSE +0 -58
- data/README.rdoc +0 -138
- data/VERSION +0 -1
- data/lib/em-http/core_ext/hash.rb +0 -53
- data/spec/hash_spec.rb +0 -24
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
module EventMachine
|
|
2
|
+
module HttpEncoding
|
|
3
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
|
4
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
|
5
|
+
|
|
6
|
+
# Escapes a URI.
|
|
7
|
+
def escape(s)
|
|
8
|
+
EscapeUtils.escape_url(s.to_s)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Unescapes a URI escaped string.
|
|
12
|
+
def unescape(s)
|
|
13
|
+
EscapeUtils.unescape_url(s.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if ''.respond_to?(:bytesize)
|
|
17
|
+
def bytesize(string)
|
|
18
|
+
string.bytesize
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
def bytesize(string)
|
|
22
|
+
string.size
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Map all header keys to a downcased string version
|
|
27
|
+
def munge_header_keys(head)
|
|
28
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
|
29
|
+
end
|
|
30
|
+
|
|
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
|
+
def encode_host
|
|
35
|
+
if @uri.port == 80 || @uri.port == 443
|
|
36
|
+
return @uri.host
|
|
37
|
+
else
|
|
38
|
+
@uri.host + ":#{@uri.port}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def encode_request(method, uri, query, proxy)
|
|
43
|
+
query = encode_query(uri, query)
|
|
44
|
+
|
|
45
|
+
# Non CONNECT proxies require that you provide the full request
|
|
46
|
+
# uri in request header, as opposed to a relative path.
|
|
47
|
+
query = uri.join(query) if proxy && proxy[:type] != :socks && !proxy[:use_connect]
|
|
48
|
+
|
|
49
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def encode_query(uri, query)
|
|
53
|
+
encoded_query = if query.kind_of?(Hash)
|
|
54
|
+
query.map { |k, v| encode_param(k, v) }.join('&')
|
|
55
|
+
else
|
|
56
|
+
query.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if !uri.query.to_s.empty?
|
|
60
|
+
encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
|
|
61
|
+
end
|
|
62
|
+
encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# URL encodes query parameters:
|
|
66
|
+
# single k=v, or a URL encoded array, if v is an array of values
|
|
67
|
+
def encode_param(k, v)
|
|
68
|
+
if v.is_a?(Array)
|
|
69
|
+
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
|
70
|
+
else
|
|
71
|
+
escape(k) + "=" + escape(v)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def form_encode_body(obj)
|
|
76
|
+
pairs = []
|
|
77
|
+
recursive = Proc.new do |h, prefix|
|
|
78
|
+
h.each do |k,v|
|
|
79
|
+
key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
|
|
80
|
+
|
|
81
|
+
if v.is_a? Array
|
|
82
|
+
nh = Hash.new
|
|
83
|
+
v.size.times { |t| nh[t] = v[t] }
|
|
84
|
+
recursive.call(nh, key)
|
|
85
|
+
|
|
86
|
+
elsif v.is_a? Hash
|
|
87
|
+
recursive.call(v, key)
|
|
88
|
+
else
|
|
89
|
+
pairs << "#{key}=#{escape(v)}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
recursive.call(obj, '')
|
|
95
|
+
return pairs.join('&')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Encode a field in an HTTP header
|
|
99
|
+
def encode_field(k, v)
|
|
100
|
+
FIELD_ENCODING % [k, v]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Encode basic auth in an HTTP header
|
|
104
|
+
# In: Array ([user, pass]) - for basic auth
|
|
105
|
+
# String - custom auth string (OAuth, etc)
|
|
106
|
+
def encode_auth(k,v)
|
|
107
|
+
if v.is_a? Array
|
|
108
|
+
FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).chomp].join(" ")]
|
|
109
|
+
else
|
|
110
|
+
encode_field(k,v)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def encode_headers(head)
|
|
115
|
+
head.inject('') do |result, (key, value)|
|
|
116
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
|
117
|
+
key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
|
|
118
|
+
result << case key
|
|
119
|
+
when 'Authorization', 'Proxy-Authorization'
|
|
120
|
+
encode_auth(key, value)
|
|
121
|
+
else
|
|
122
|
+
encode_field(key, value)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def encode_cookie(cookie)
|
|
128
|
+
if cookie.is_a? Hash
|
|
129
|
+
cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
|
|
130
|
+
else
|
|
131
|
+
cookie
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
module EventMachine
|
|
2
|
+
# A simple hash is returned for each request made by HttpClient with the
|
|
3
|
+
# headers that were given by the server for that request.
|
|
4
|
+
class HttpResponseHeader < Hash
|
|
5
|
+
# The reason returned in the http response ("OK","File not found",etc.)
|
|
6
|
+
attr_accessor :http_reason
|
|
7
|
+
|
|
8
|
+
# The HTTP version returned.
|
|
9
|
+
attr_accessor :http_version
|
|
10
|
+
|
|
11
|
+
# The status code (as a string!)
|
|
12
|
+
attr_accessor :http_status
|
|
13
|
+
|
|
14
|
+
# E-Tag
|
|
15
|
+
def etag
|
|
16
|
+
self[HttpClient::ETAG]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def last_modified
|
|
20
|
+
self[HttpClient::LAST_MODIFIED]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# HTTP response status as an integer
|
|
24
|
+
def status
|
|
25
|
+
Integer(http_status) rescue 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Length of content as an integer, or nil if chunked/unspecified
|
|
29
|
+
def content_length
|
|
30
|
+
@content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) &&
|
|
31
|
+
(s =~ /^(\d+)$/)) ? $1.to_i : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Cookie header from the server
|
|
35
|
+
def cookie
|
|
36
|
+
self[HttpClient::SET_COOKIE]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Is the transfer encoding chunked?
|
|
40
|
+
def chunked_encoding?
|
|
41
|
+
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def keep_alive?
|
|
45
|
+
/keep-alive/i === self[HttpClient::KEEP_ALIVE]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def compressed?
|
|
49
|
+
/gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def location
|
|
53
|
+
self[HttpClient::LOCATION]
|
|
54
|
+
end
|
|
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
|
data/lib/em-http/http_options.rb
CHANGED
|
@@ -2,7 +2,7 @@ class HttpOptions
|
|
|
2
2
|
attr_reader :uri, :method, :host, :port, :options
|
|
3
3
|
|
|
4
4
|
def initialize(method, uri, options)
|
|
5
|
-
|
|
5
|
+
uri.path = '/' if uri.path.empty?
|
|
6
6
|
|
|
7
7
|
@options = options
|
|
8
8
|
@method = method.to_s.upcase
|
|
@@ -12,12 +12,15 @@ class HttpOptions
|
|
|
12
12
|
@host = proxy[:host]
|
|
13
13
|
@port = proxy[:port]
|
|
14
14
|
else
|
|
15
|
-
|
|
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
|
|
16
18
|
@port = uri.port
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
@options[:timeout] ||= 10
|
|
20
|
-
@options[:redirects] ||= 0
|
|
21
|
+
@options[:timeout] ||= 10 # default connect & inactivity timeouts
|
|
22
|
+
@options[:redirects] ||= 0 # default number of redirects to follow
|
|
23
|
+
@options[:keepalive] ||= false # default to single request per connection
|
|
21
24
|
|
|
22
25
|
# Make sure the ports are set as Addressable::URI doesn't
|
|
23
26
|
# set the port if it isn't there
|
data/lib/em-http/mock.rb
CHANGED
|
@@ -1,91 +1,131 @@
|
|
|
1
1
|
module EventMachine
|
|
2
|
+
OriginalHttpRequest = HttpRequest unless const_defined?(:OriginalHttpRequest)
|
|
3
|
+
|
|
2
4
|
class MockHttpRequest < EventMachine::HttpRequest
|
|
3
|
-
|
|
5
|
+
|
|
4
6
|
include HttpEncoding
|
|
5
|
-
|
|
6
|
-
class FakeHttpClient < EventMachine::HttpClient
|
|
7
7
|
|
|
8
|
+
class RegisteredRequest < Struct.new(:uri, :method, :headers)
|
|
9
|
+
def self.build(uri, method, headers)
|
|
10
|
+
new(uri, method.to_s.upcase, headers || {})
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class FakeHttpClient < EventMachine::HttpClient
|
|
15
|
+
attr_writer :response
|
|
16
|
+
attr_reader :data
|
|
8
17
|
def setup(response, uri)
|
|
9
18
|
@uri = uri
|
|
10
19
|
if response == :fail
|
|
11
20
|
fail(self)
|
|
12
21
|
else
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
if response.respond_to?(:call)
|
|
23
|
+
response.call(self)
|
|
24
|
+
@state = :body
|
|
25
|
+
else
|
|
26
|
+
receive_data(response)
|
|
27
|
+
end
|
|
28
|
+
@state == :body ? succeed(self) : fail(self)
|
|
15
29
|
end
|
|
16
30
|
end
|
|
17
|
-
|
|
31
|
+
|
|
18
32
|
def unbind
|
|
19
33
|
end
|
|
20
|
-
|
|
21
34
|
end
|
|
22
|
-
|
|
23
|
-
@@registry =
|
|
24
|
-
@@registry_count =
|
|
25
|
-
|
|
35
|
+
|
|
36
|
+
@@registry = Hash.new
|
|
37
|
+
@@registry_count = Hash.new{|h,k| h[k] = 0}
|
|
38
|
+
|
|
39
|
+
def self.use
|
|
40
|
+
activate!
|
|
41
|
+
yield
|
|
42
|
+
ensure
|
|
43
|
+
deactivate!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.activate!
|
|
47
|
+
EventMachine.send(:remove_const, :HttpRequest)
|
|
48
|
+
EventMachine.send(:const_set, :HttpRequest, MockHttpRequest)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.deactivate!
|
|
52
|
+
EventMachine.send(:remove_const, :HttpRequest)
|
|
53
|
+
EventMachine.send(:const_set, :HttpRequest, OriginalHttpRequest)
|
|
54
|
+
end
|
|
55
|
+
|
|
26
56
|
def self.reset_counts!
|
|
27
|
-
@@registry_count
|
|
28
|
-
registry[query] = Hash.new{|h,k| h[k] = Hash.new(0)}
|
|
29
|
-
end
|
|
57
|
+
@@registry_count.clear
|
|
30
58
|
end
|
|
31
|
-
|
|
59
|
+
|
|
32
60
|
def self.reset_registry!
|
|
33
|
-
@@registry
|
|
34
|
-
registry[query] = Hash.new{|h,k| h[k] = {}}
|
|
35
|
-
end
|
|
61
|
+
@@registry.clear
|
|
36
62
|
end
|
|
37
|
-
|
|
38
|
-
reset_counts!
|
|
39
|
-
reset_registry!
|
|
40
|
-
|
|
63
|
+
|
|
41
64
|
@@pass_through_requests = true
|
|
42
65
|
|
|
43
66
|
def self.pass_through_requests=(pass_through_requests)
|
|
44
67
|
@@pass_through_requests = pass_through_requests
|
|
45
68
|
end
|
|
46
|
-
|
|
69
|
+
|
|
47
70
|
def self.pass_through_requests
|
|
48
71
|
@@pass_through_requests
|
|
49
72
|
end
|
|
50
|
-
|
|
51
|
-
def self.
|
|
52
|
-
|
|
53
|
-
headers =
|
|
54
|
-
|
|
73
|
+
|
|
74
|
+
def self.parse_register_args(args, &proc)
|
|
75
|
+
args << proc{|client| proc.call(client); ''} if proc
|
|
76
|
+
headers, data = case args.size
|
|
77
|
+
when 3
|
|
78
|
+
args[2].is_a?(Hash) ?
|
|
79
|
+
[args[2][:headers], args[2][:data]] :
|
|
80
|
+
[{}, args[2]]
|
|
81
|
+
when 4
|
|
82
|
+
[args[2], args[3]]
|
|
83
|
+
else
|
|
84
|
+
raise
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
url = args[0]
|
|
88
|
+
method = args[1]
|
|
89
|
+
[headers, url, method, data]
|
|
55
90
|
end
|
|
56
|
-
|
|
57
|
-
def self.
|
|
58
|
-
|
|
91
|
+
|
|
92
|
+
def self.register(*args, &proc)
|
|
93
|
+
headers, url, method, data = parse_register_args(args, &proc)
|
|
94
|
+
@@registry[RegisteredRequest.build(url, method, headers)] = data
|
|
59
95
|
end
|
|
60
|
-
|
|
61
|
-
def self.
|
|
62
|
-
method =
|
|
63
|
-
headers =
|
|
64
|
-
|
|
96
|
+
|
|
97
|
+
def self.register_file(*args)
|
|
98
|
+
headers, url, method, data = parse_register_args(args)
|
|
99
|
+
@@registry[RegisteredRequest.build(url, method, headers)] = File.read(data)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.count(url, method, headers = {})
|
|
103
|
+
@@registry_count[RegisteredRequest.build(url, method, headers)]
|
|
65
104
|
end
|
|
66
|
-
|
|
67
|
-
def self.registered?(
|
|
68
|
-
@@registry
|
|
105
|
+
|
|
106
|
+
def self.registered?(url, method, headers = {})
|
|
107
|
+
@@registry.key?(RegisteredRequest.build(url, method, headers))
|
|
69
108
|
end
|
|
70
|
-
|
|
71
|
-
def self.registered_content(
|
|
72
|
-
@@registry[
|
|
109
|
+
|
|
110
|
+
def self.registered_content(url, method, headers = {})
|
|
111
|
+
@@registry[RegisteredRequest.build(url, method, headers)]
|
|
73
112
|
end
|
|
74
|
-
|
|
75
|
-
def self.increment_access(
|
|
76
|
-
@@registry_count[
|
|
113
|
+
|
|
114
|
+
def self.increment_access(url, method, headers = {})
|
|
115
|
+
@@registry_count[RegisteredRequest.build(url, method, headers)] += 1
|
|
77
116
|
end
|
|
78
|
-
|
|
117
|
+
|
|
79
118
|
alias_method :real_send_request, :send_request
|
|
80
|
-
|
|
119
|
+
|
|
81
120
|
protected
|
|
82
121
|
def send_request(&blk)
|
|
83
|
-
query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri
|
|
84
|
-
headers = @req.options[:head]
|
|
122
|
+
query = "#{@req.uri.scheme}://#{@req.uri.host}:#{@req.uri.port}#{encode_query(@req.uri, @req.options[:query])}"
|
|
123
|
+
headers = @req.options[:head]
|
|
85
124
|
if self.class.registered?(query, @req.method, headers)
|
|
86
125
|
self.class.increment_access(query, @req.method, headers)
|
|
87
126
|
client = FakeHttpClient.new(nil)
|
|
88
|
-
|
|
127
|
+
content = self.class.registered_content(query, @req.method, headers)
|
|
128
|
+
client.setup(content, @req.uri)
|
|
89
129
|
client
|
|
90
130
|
elsif @@pass_through_requests
|
|
91
131
|
real_send_request
|
data/lib/em-http/multi.rb
CHANGED
|
@@ -1,51 +1,55 @@
|
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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(conns=[], &block)
|
|
32
|
+
@requests = []
|
|
33
|
+
@responses = {:succeeded => [], :failed => []}
|
|
34
|
+
|
|
35
|
+
conns.each {|conn| add(conn)}
|
|
36
|
+
callback(&block) if block_given?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add(conn)
|
|
40
|
+
@requests.push(conn)
|
|
41
|
+
|
|
42
|
+
conn.callback { @responses[:succeeded].push(conn); check_progress }
|
|
43
|
+
conn.errback { @responses[:failed].push(conn); check_progress }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
# invoke callback if all requests have completed
|
|
49
|
+
def check_progress
|
|
50
|
+
succeed(self) if (@responses[:succeeded].size +
|
|
51
|
+
@responses[:failed].size) == @requests.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/em-http/request.rb
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
require 'base64'
|
|
2
|
-
require 'addressable/uri'
|
|
3
|
-
|
|
4
1
|
module EventMachine
|
|
5
2
|
|
|
6
3
|
# EventMachine based HTTP request class with support for streaming consumption
|
|
@@ -27,7 +24,7 @@ module EventMachine
|
|
|
27
24
|
attr_reader :options, :method
|
|
28
25
|
|
|
29
26
|
def initialize(host)
|
|
30
|
-
@uri = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host)
|
|
27
|
+
@uri = host.kind_of?(Addressable::URI) ? host : Addressable::URI::parse(host.to_s)
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
# Send an HTTP request and consume the response. Supported options:
|
|
@@ -72,6 +69,7 @@ module EventMachine
|
|
|
72
69
|
rescue EventMachine::ConnectionError => e
|
|
73
70
|
conn = EventMachine::HttpClient.new("")
|
|
74
71
|
conn.on_error(e.message, true)
|
|
72
|
+
conn.uri = @req.uri
|
|
75
73
|
conn
|
|
76
74
|
end
|
|
77
75
|
end
|
data/lib/em-http.rb
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
require
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
require
|
|
13
|
-
require
|
|
14
|
-
|
|
15
|
-
require
|
|
16
|
-
require
|
|
17
|
-
require
|
|
18
|
-
require
|
|
19
|
-
require
|
|
1
|
+
require 'eventmachine'
|
|
2
|
+
require 'escape_utils'
|
|
3
|
+
require 'addressable/uri'
|
|
4
|
+
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'socket'
|
|
7
|
+
|
|
8
|
+
require 'http11_client'
|
|
9
|
+
require 'em_buffer'
|
|
10
|
+
|
|
11
|
+
require 'em-http/core_ext/bytesize'
|
|
12
|
+
require 'em-http/http_header'
|
|
13
|
+
require 'em-http/http_encoding'
|
|
14
|
+
require 'em-http/http_options'
|
|
15
|
+
require 'em-http/client'
|
|
16
|
+
require 'em-http/multi'
|
|
17
|
+
require 'em-http/request'
|
|
18
|
+
require 'em-http/decoders'
|
|
19
|
+
require 'em-http/mock'
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require 'helper'
|
|
2
|
+
|
|
3
|
+
describe EventMachine::HttpEncoding do
|
|
4
|
+
include EventMachine::HttpEncoding
|
|
5
|
+
|
|
6
|
+
it "should transform a basic hash into HTTP POST Params" do
|
|
7
|
+
form_encode_body({:a => "alpha", :b => "beta"}).should == "a=alpha&b=beta"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "should transform a more complex hash into HTTP POST Params" do
|
|
11
|
+
form_encode_body({:a => "a", :b => ["c", "d", "e"]}).should == "a=a&b[0]=c&b[1]=d&b[2]=e"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "should transform a very complex hash into HTTP POST Params" do
|
|
15
|
+
params = form_encode_body({:a => "a", :b => [{:c => "c", :d => "d"}, {:e => "e", :f => "f"}]})
|
|
16
|
+
params.should == "a=a&b[0][c]=c&b[0][d]=d&b[1][e]=e&b[1][f]=f"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "should escape values" do
|
|
20
|
+
params = form_encode_body({:stuff => 'string&string'})
|
|
21
|
+
params.should == "stuff=string%26string"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "should escape keys" do
|
|
25
|
+
params = form_encode_body({'bad&str'=> {'key&key' => [:a, :b]}})
|
|
26
|
+
params.should == 'bad%26str[key%26key][0]=a&bad%26str[key%26key][1]=b'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "should escape keys and values" do
|
|
30
|
+
params = form_encode_body({'bad&str'=> {'key&key' => ['bad+&stuff', '[test]']}})
|
|
31
|
+
params.should == "bad%26str[key%26key][0]=bad%2B%26stuff&bad%26str[key%26key][1]=%5Btest%5D"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "should be fast on long string escapes" do
|
|
35
|
+
s = Time.now
|
|
36
|
+
5000.times { |n| form_encode_body({:a => "{a:'b', d:'f', g:['a','b']}"*50}) }
|
|
37
|
+
(Time.now - s).should satisfy { |t| t < 1.5 }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|