hatetepe 0.5.2 → 0.6.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +4 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +9 -4
  6. data/Gemfile.devtools +55 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +39 -192
  9. data/Rakefile +3 -2
  10. data/bin/hatetepe +35 -2
  11. data/config/devtools.yml +2 -0
  12. data/config/flay.yml +3 -0
  13. data/config/flog.yml +2 -0
  14. data/config/mutant.yml +3 -0
  15. data/config/reek.yml +103 -0
  16. data/config/rubocop.yml +58 -0
  17. data/config/yardstick.yml +2 -0
  18. data/hatetepe.gemspec +23 -27
  19. data/lib/hatetepe/client/keep_alive.rb +59 -0
  20. data/lib/hatetepe/client/timeouts.rb +19 -0
  21. data/lib/hatetepe/client.rb +54 -302
  22. data/lib/hatetepe/connection/eventmachine.rb +61 -0
  23. data/lib/hatetepe/connection/status.rb +28 -0
  24. data/lib/hatetepe/errors.rb +7 -0
  25. data/lib/hatetepe/promise.rb +86 -0
  26. data/lib/hatetepe/request.rb +15 -39
  27. data/lib/hatetepe/response.rb +82 -22
  28. data/lib/hatetepe/serializer/encoding.rb +58 -0
  29. data/lib/hatetepe/serializer.rb +61 -0
  30. data/lib/hatetepe/server/keep_alive.rb +53 -13
  31. data/lib/hatetepe/server/timeouts.rb +17 -0
  32. data/lib/hatetepe/server.rb +37 -85
  33. data/lib/hatetepe/support/handlers.rb +19 -0
  34. data/lib/hatetepe/support/keep_alive.rb +14 -0
  35. data/lib/hatetepe/support/message.rb +40 -0
  36. data/lib/hatetepe/version.rb +3 -1
  37. data/lib/hatetepe.rb +29 -7
  38. data/spec/integration/error_handling_spec.rb +7 -0
  39. data/spec/integration/keep_alive_spec.rb +106 -0
  40. data/spec/integration/smoke_spec.rb +21 -0
  41. data/spec/integration/streaming_spec.rb +61 -0
  42. data/spec/integration/timeouts_spec.rb +82 -0
  43. data/spec/shared/integration/server_client_pair.rb +26 -0
  44. data/spec/spec_helper.rb +41 -10
  45. data/spec/support/handler.rb +55 -0
  46. data/spec/support/helper.rb +74 -0
  47. data/spec/unit/client_spec.rb +115 -156
  48. data/spec/unit/connection/eventmachine_spec.rb +146 -0
  49. data/spec/unit/request_spec.rb +35 -0
  50. data/spec/unit/response_spec.rb +42 -0
  51. data/spec/unit/server_spec.rb +65 -100
  52. data/spec/unit/support/keep_alive_spec.rb +52 -0
  53. data/spec/unit/support/message_spec.rb +41 -0
  54. metadata +68 -103
  55. data/Gemfile.lock +0 -46
  56. data/LICENSE +0 -19
  57. data/Procfile +0 -1
  58. data/config.ru +0 -7
  59. data/examples/parallel_requests.rb +0 -32
  60. data/lib/hatetepe/body.rb +0 -182
  61. data/lib/hatetepe/builder.rb +0 -171
  62. data/lib/hatetepe/cli.rb +0 -61
  63. data/lib/hatetepe/connection.rb +0 -73
  64. data/lib/hatetepe/events.rb +0 -35
  65. data/lib/hatetepe/message.rb +0 -13
  66. data/lib/hatetepe/parser.rb +0 -83
  67. data/lib/hatetepe/server/pipeline.rb +0 -20
  68. data/lib/hatetepe/server/rack_app.rb +0 -39
  69. data/lib/rack/handler/hatetepe.rb +0 -33
  70. data/spec/integration/cli/start_spec.rb +0 -113
  71. data/spec/integration/client/keep_alive_spec.rb +0 -23
  72. data/spec/integration/client/timeout_spec.rb +0 -97
  73. data/spec/integration/server/keep_alive_spec.rb +0 -27
  74. data/spec/integration/server/timeout_spec.rb +0 -51
  75. data/spec/unit/body_spec.rb +0 -205
  76. data/spec/unit/builder_spec.rb +0 -372
  77. data/spec/unit/connection_spec.rb +0 -62
  78. data/spec/unit/events_spec.rb +0 -96
  79. data/spec/unit/parser_spec.rb +0 -209
  80. data/spec/unit/rack_handler_spec.rb +0 -60
@@ -0,0 +1,59 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ class Client::KeepAlive
5
+ CONNECTION = 'Connection'.freeze
6
+
7
+ include Support::KeepAlive
8
+
9
+ def initialize(config, client, connection)
10
+ @config, @client = config, client
11
+ @connection = connection
12
+ @close = nil
13
+ @requests = []
14
+ end
15
+
16
+ def perform(request)
17
+ previous_request = current_request
18
+ @requests << request
19
+
20
+ if @close.nil?
21
+ await_connection_header(request)
22
+ elsif !@close
23
+ await_previous_response(previous_request) if previous_request
24
+ end
25
+ end
26
+
27
+ def receive(request, response)
28
+ @requests.delete(request)
29
+ end
30
+
31
+ private
32
+
33
+ def current_request
34
+ @requests.first
35
+ end
36
+
37
+ def await_connection_header(request)
38
+ if current_request == request
39
+ # first response has the relevant Connection header
40
+ assume_connection_header
41
+ else
42
+ # subsequent requests wait for first response
43
+ current_request.served.sync
44
+ end
45
+ end
46
+
47
+ def assume_connection_header
48
+ current_request.served.then do |response|
49
+ @close = close_connection?(response.http_version,
50
+ response.headers[CONNECTION])
51
+ end
52
+ end
53
+
54
+ def await_previous_response(request)
55
+ response = request.served.sync
56
+ response.finished.sync
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ # @see EM.heartbeat_interval
5
+ class Client::Timeouts
6
+ def initialize(config, client, connection)
7
+ @config, @client = config, client
8
+ @connection = connection
9
+
10
+ @config[:timeout] ||= 2.0
11
+ @config[:connect_timeout] ||= 2.0
12
+ end
13
+
14
+ def post_init
15
+ @connection.comm_inactivity_timeout = @config[:timeout]
16
+ @connection.pending_connect_timeout = @config[:connect_timeout]
17
+ end
18
+ end
19
+ end
@@ -1,327 +1,79 @@
1
- require "em-synchrony"
2
- require "eventmachine"
3
-
4
- require "hatetepe/builder"
5
- require "hatetepe/connection"
6
- require "hatetepe/parser"
7
- require "hatetepe/request"
8
- require "hatetepe/version"
1
+ # encoding: utf-8
9
2
 
10
3
  module Hatetepe
11
- HatetepeError = Class.new(StandardError)
12
- RequestError = Class.new(HatetepeError)
13
- ClientError = Class.new(RequestError)
14
- ServerError = Class.new(RequestError)
15
- end
16
-
17
- module Hatetepe::Client
18
- include Hatetepe::Connection
19
-
20
- # @api private
21
- Job = Struct.new(:fiber, :request, :sent, :response)
22
-
23
- # The default configuration.
24
- #
25
- # @api public
26
- CONFIG_DEFAULTS = {
27
- :timeout => 5,
28
- :connect_timeout => 5
29
- }
30
-
31
- # The configuration for this Client instance.
32
- #
33
- # @api public
34
- attr_reader :config
35
-
36
- # The pipe of middleware and request transmission/response reception.
37
- #
38
- # @api private
39
- attr_reader :app
40
-
41
- # Initializes a new Client instance.
42
- #
43
- # @param [Hash] config
44
- # Configuration values that overwrite the defaults.
45
- #
46
- # @api semipublic
47
- def initialize(config)
48
- @config = CONFIG_DEFAULTS.merge(config)
49
- @ssl_handshake_completed = EM::DefaultDeferrable.new
50
- end
51
-
52
- # Initializes the parser, request queue, and middleware pipe.
53
- #
54
- # @see EM::Connection#post_init
55
- #
56
- # @api semipublic
57
- def post_init
58
- @builder, @parser = Hatetepe::Builder.new, Hatetepe::Parser.new
59
- @builder.on_write << method(:send_data)
60
- # @builder.on_write {|data| p "|--> #{data}" }
61
- @parser.on_response << method(:receive_response)
62
-
63
- @queue = []
64
-
65
- @app = proc {|request| send_request(request) }
4
+ class Client
5
+ include Support::Handlers
6
+ include Connection::Status
66
7
 
67
- self.comm_inactivity_timeout = config[:timeout]
68
- self.pending_connect_timeout = config[:connect_timeout]
8
+ attr_reader :config
69
9
 
70
- start_tls if config[:ssl]
71
- end
72
-
73
- def ssl_handshake_completed
74
- EM::Synchrony.next_tick { @ssl_handshake_completed.succeed }
75
- end
76
-
77
- # Feeds response data into the parser.
78
- #
79
- # @see EM::Connection#receive_data
80
- #
81
- # @param [String] data
82
- # The received data that's gonna be fed into the parser.
83
- #
84
- # @api semipublic
85
- def receive_data(data)
86
- # p "|<-- #{data}"
87
- @parser << data
88
- end
89
-
90
- # Aborts all outstanding requests.
91
- #
92
- # @see EM::Connection#unbind
93
- #
94
- # @api semipublic
95
- def unbind(reason)
96
- super
97
- @queue.each {|job| job.fiber.resume(:kill) }
98
- end
10
+ def initialize(config)
11
+ @config = config
12
+ @requests = []
99
13
 
100
- # Sends a request and waits for the response without blocking.
101
- #
102
- # Transmission and reception are performed within a separate +Fiber+.
103
- # +#succeed+ and +#fail+ will be called on the +request+ passing the
104
- # response, depending on whether the response indicates success (100-399)
105
- # or failure (400-599).
106
- #
107
- # The request will +#fail+ with a +nil+ response if the connection was
108
- # closed for whatever reason.
109
- #
110
- # TODO find out if there are more cases where the response body
111
- # should automatically be closed.
112
- #
113
- # @api public
114
- def <<(request)
115
- Fiber.new do
116
- EM::Synchrony.sync(@ssl_handshake_completed) if config[:ssl]
14
+ setup_connection
15
+ setup_handlers
16
+ notify_handlers(:post_init)
17
+ end
117
18
 
118
- response = @app.call(request)
19
+ def request(http_method, uri)
20
+ request = Request.new(http_method, uri)
21
+ request.finished.fulfill
22
+ perform(request)
23
+ request.served.sync
24
+ end
119
25
 
120
- if response && (request.verb == "HEAD" || response.status == 204)
121
- response.body.close_write
122
- end
26
+ def perform(request)
27
+ @requests << request
123
28
 
124
- if !response
125
- request.fail
126
- elsif response.failure?
127
- request.fail(response)
128
- else
129
- request.succeed(response)
29
+ fiber = Fiber.new do
30
+ notify_handlers(:perform, request)
31
+ @connection.serialize(request)
130
32
  end
131
- end.resume
132
- end
133
-
134
- # Builds a +Request+, sends it, and blocks while waiting for the response.
135
- #
136
- # @param [Symbol, String] verb
137
- # The HTTP method verb, e.g. +:get+ or +"PUT"+.
138
- # @param [String, URI] uri
139
- # The request URI.
140
- # @param [Hash] headers (optional)
141
- # The request headers.
142
- # @param [#each] body (optional)
143
- # A request body object whose +#each+ method yields objects that respond
144
- # to +#to_s+.
145
- #
146
- # @return [Hatetepe::Response, nil]
147
- #
148
- # @api public
149
- def request(verb, uri, headers = {}, body = [])
150
- uri = URI(uri)
151
- uri.scheme ||= @config[:ssl] ? 'http' : 'https'
152
- uri.host ||= @config[:host]
153
- uri.port ||= @config[:port]
154
-
155
- headers['Host'] ||= "#{uri.host}:#{uri.port}"
156
-
157
- request = Hatetepe::Request.new(verb, URI(uri.to_s), headers, body)
158
- self << request
159
- EM::Synchrony.sync(request)
160
- end
161
-
162
- # Like +#request+, but raises errors for 4xx and 5xx responses.
163
- #
164
- # @param [Symbol, String] verb
165
- # The HTTP method verb, e.g. +:get+ or +"PUT"+.
166
- # @param [String, URI] uri
167
- # The request URI.
168
- # @param [Hash] headers (optional)
169
- # The request headers.
170
- # @param [#each] body (optional)
171
- # A request body object whose +#each+ method yields objects that respond
172
- # to +#to_s+.
173
- #
174
- # @return [Hatetepe]::Response, nil]
175
- #
176
- # @raise [Hatetepe::ClientError]
177
- # If the server responded with a 4xx status code.
178
- # @raise [Hatetepe::ServerError]
179
- # If the server responded with a 5xx status code.
180
- # @raise [Hatetepe::RequestError]
181
- # If the client failed to receive any response at all.
182
- def request!(verb, uri, headers = {}, body = [])
183
- response = request(verb, uri, headers, body)
184
-
185
- if response.nil?
186
- raise Hatetepe::RequestError
187
- elsif response.status >= 500
188
- raise Hatetepe::ServerError
189
- elsif response.status >= 400
190
- raise Hatetepe::ClientError
33
+ fiber.resume
191
34
  end
192
35
 
193
- response
194
- end
195
-
196
- # Gracefully stops the client.
197
- #
198
- # Waits for all requests to finish and then stops the client.
199
- #
200
- # @api public
201
- def stop
202
- wait
203
- stop!
204
- end
205
-
206
- # Immediately stops the client by closing the connection.
207
- #
208
- # This will lead to EventMachine's event loop calling {#unbind}, which fail
209
- # all outstanding requests.
210
- #
211
- # @see #unbind
212
- #
213
- # @api public
214
- def stop!
215
- close_connection
216
- end
217
-
218
- # Blocks until the last request has finished receiving its response.
219
- #
220
- # Returns immediately if there are no outstanding requests.
221
- #
222
- # @api public
223
- def wait
224
- if job = @queue.last
225
- EM::Synchrony.sync(job.request)
226
- EM::Synchrony.sync(job.response.body) if job.response
36
+ def close
37
+ @connection.close
227
38
  end
228
- end
229
39
 
230
- # Starts a new Client.
231
- #
232
- # @param [Hash] config
233
- # The +:host+ and +:port+ the Client should connect to.
234
- #
235
- # @return [Hatetepe::Client]
236
- # The new Client instance.
237
- #
238
- # @api public
239
- def self.start(config)
240
- EM.connect(config[:host], config[:port], self, config)
241
- end
242
-
243
- # @api public
244
- def self.request(verb, uri, headers = {}, body = [])
245
- uri = URI(uri)
246
- client = start(host: uri.host, port: uri.port, ssl: uri.scheme == 'https')
247
- client.request(verb, uri, headers, body)
248
- end
249
-
250
- # Feeds the request into the builder and blocks while waiting for the
251
- # response to arrive.
252
- #
253
- # Supports the request bit of HTTP pipelining by waiting until the previous
254
- # request has been sent.
255
- #
256
- # @param [Hatetepe::Request] request
257
- # The request that's gonna be sent.
258
- #
259
- # @return [Hatetepe::Response, nil]
260
- # The received response or +nil+ if the connection has been closed before
261
- # receiving a response.
262
- #
263
- # @api private
264
- def send_request(request)
265
- previous = @queue.last
266
- current = Job.new(Fiber.current, request, false)
267
- @queue << current
40
+ def receive(response)
41
+ unless (request = correlate(response))
42
+ raise ClientError, 'Unable to correlate with request'
43
+ end
268
44
 
269
- # wait for the previous request to be sent
270
- while previous && !previous.sent
271
- return if Fiber.yield == :kill
45
+ setup_cleanup(request, response)
46
+ notify_handlers(:receive, request, response)
272
47
  end
273
48
 
274
- # send the request
275
- self.comm_inactivity_timeout = 0
276
- @builder.request(request.to_a)
277
- current.sent = true
278
- self.comm_inactivity_timeout = config[:timeout]
279
-
280
- # wait for the response
281
- while !current.response
282
- return if Fiber.yield == :kill
49
+ def teardown(reason)
50
+ while (request = @requests.shift)
51
+ if (response = request.served.value)
52
+ response.finished.reject(reason)
53
+ else
54
+ request.served.reject(reason)
55
+ end
56
+ end
283
57
  end
284
58
 
285
- # clean up and return response
286
- @queue.delete(current)
287
- current.response
288
- end
59
+ private
289
60
 
290
- # Relates an incoming response to the corresponding request.
291
- #
292
- # Supports the response bit of HTTP pipelining by relating responses to
293
- # requests in the order the requests were sent.
294
- #
295
- # TODO: raise a more meaningful error.
296
- #
297
- # @param [Hatetepe::Response] response
298
- # The incoming response
299
- #
300
- # @raise [RuntimeError]
301
- # There is no request that's waiting for a response.
302
- #
303
- # @api private
304
- def receive_response(response)
305
- query = proc {|j| j.response.nil? }
61
+ def setup_connection
62
+ @connection =
63
+ EM.connect(@config[:address], @config[:port], Connection::EventMachine)
64
+ @connection.parse(method(:receive))
65
+ @connection.closed.then(method(:teardown))
66
+ end
306
67
 
307
- if job = @queue.find(&query)
308
- job.response = response
309
- job.fiber.resume
310
- else
311
- raise "Received response but didn't expect one: #{response.status}"
68
+ def correlate(response)
69
+ @requests
70
+ .detect { |request| request.served.pending? }
71
+ .tap { |request| request.served.fulfill(response) if request }
312
72
  end
313
- end
314
73
 
315
- module VerbMethods
316
- [
317
- :get, :head, :options, :put, :post, :delete, :patch, :connect
318
- ].each do |verb|
319
- define_method(verb) do |uri, headers = {}, body = []|
320
- request(verb, uri, headers, body)
321
- end
74
+ def setup_cleanup(request, response)
75
+ callback = proc { @requests.delete(request) }
76
+ response.finished.then(callback, callback)
322
77
  end
323
78
  end
324
-
325
- include VerbMethods
326
- extend VerbMethods
327
79
  end
@@ -0,0 +1,61 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ module Connection
5
+ class EventMachine < EM::Connection
6
+ include Promise::Attribute
7
+ promise :closed
8
+
9
+ def initialize(callback = nil)
10
+ @parser = HTTP::Parser.new(self)
11
+ callback.call(self) if callback
12
+ end
13
+
14
+ def parse(block)
15
+ @parse = block
16
+ end
17
+
18
+ def serialize(message)
19
+ serializer = Serializer.new(message, method(:send_data))
20
+ serializer.serialize
21
+ end
22
+
23
+ def receive_data(data)
24
+ # ap data
25
+ @parser << data
26
+ # rescue => error
27
+ # $stderr.puts(error.to_s)
28
+ # @message.finished.reject(error) if @message
29
+ # close_connection
30
+ end
31
+
32
+ # def send_data(data)
33
+ # ap data
34
+ # super
35
+ # end
36
+
37
+ def close
38
+ @reason ||= :self
39
+ close_connection_after_writing
40
+ end
41
+
42
+ def unbind(reason)
43
+ closed.fulfill(@reason || reason)
44
+ end
45
+
46
+ def on_headers_complete(_)
47
+ @message = Support::Message.build(@parser)
48
+ @parse.call(@message)
49
+ end
50
+
51
+ def on_body(chunk)
52
+ @message.body << chunk
53
+ @message.finished.progress(chunk)
54
+ end
55
+
56
+ def on_message_complete
57
+ @message.finished.fulfill
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ module Connection
5
+ module Status
6
+ def closed?
7
+ @connection.closed.fulfilled?
8
+ end
9
+
10
+ def closed_by_timeout?
11
+ closed? && @connection.closed.value == Errno::ETIMEDOUT
12
+ end
13
+
14
+ def closed_by_self?
15
+ closed? && @connection.closed.value == :self
16
+ end
17
+
18
+ def closed_by_remote?
19
+ closed? && !closed_by_timeout? && !closed_by_self?
20
+ end
21
+
22
+ # XXX this exists only to support spec/integration/timeout_spec
23
+ def closed_reason
24
+ @connection.closed.value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ Error = Class.new(StandardError)
5
+
6
+ ClientError = Class.new(Error)
7
+ end
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+
3
+ module Hatetepe
4
+ class Promise < ::Promise
5
+ include ::Promise::Progress
6
+
7
+ def fulfill(value = nil)
8
+ super(value)
9
+ end
10
+
11
+ def reject(reason = nil)
12
+ super(reason)
13
+ end
14
+
15
+ def sync
16
+ if fulfilled?
17
+ return value
18
+ elsif rejected?
19
+ return reason
20
+ end
21
+
22
+ sync!
23
+ end
24
+
25
+ def sync!
26
+ fiber = Fiber.current
27
+ resume = proc do |arg|
28
+ EM.next_tick { fiber.resume(arg) }
29
+ # fiber.resume(arg)
30
+ end
31
+
32
+ self.then(resume, resume)
33
+ Fiber.yield
34
+ end
35
+
36
+ private
37
+
38
+ def defer(callback, arg)
39
+ EM.next_tick { safe_dispatch(callback, arg) }
40
+ # safe_dispatch(callback, arg)
41
+ end
42
+
43
+ def safe_dispatch(callback, arg)
44
+ callback.dispatch(arg)
45
+ # rescue => error
46
+ # ...
47
+ end
48
+
49
+ module Attribute
50
+ def self.included(klass)
51
+ klass.extend(ClassMethods)
52
+ end
53
+
54
+ def initialize
55
+ self.class.send(:promises).map { |name| send(name) }
56
+ end
57
+
58
+ private
59
+
60
+ def get_or_set_promise(ivar)
61
+ if instance_variable_defined?(ivar)
62
+ instance_variable_get(ivar)
63
+ else
64
+ instance_variable_set(ivar, Promise.new)
65
+ end
66
+ end
67
+
68
+ module ClassMethods
69
+ private
70
+
71
+ def promises
72
+ @promises ||= []
73
+ end
74
+
75
+ def promise(name)
76
+ promises << name
77
+ define_promise(name, "@#{name}")
78
+ end
79
+
80
+ def define_promise(name, ivar)
81
+ define_method(name) { get_or_set_promise(ivar) }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -1,45 +1,21 @@
1
- require "hatetepe/message"
1
+ # encoding: utf-8
2
2
 
3
3
  module Hatetepe
4
- class Request < Message
5
- include EM::Deferrable
6
-
7
- attr_reader :verb
8
- attr_accessor :uri, :response
9
-
10
- def initialize(verb, uri, headers = {}, body = nil, http_version = "1.1")
11
- self.verb = verb
12
- @uri = uri
13
- super headers, body, http_version
14
- end
15
-
16
- def verb=(verb)
17
- @verb = verb.to_s.upcase
18
- end
19
-
20
- def to_a
21
- [verb, uri, headers, body, http_version]
22
- end
23
-
24
- def to_h
25
- {
26
- "rack.version" => [1, 0],
27
- "hatetepe.request" => self,
28
- "rack.input" => body,
29
- "REQUEST_METHOD" => verb.dup,
30
- "REQUEST_URI" => uri.dup
31
- }.tap do |hsh|
32
- headers.each do |key, value|
33
- key = key.upcase.gsub /[^A-Z]/, "_"
34
- key = "HTTP_#{key}" unless key =~ /^CONTENT_(TYPE|LENGTH)$/
35
- hsh[key] = value.dup
36
- end
4
+ class Request
5
+ include Promise::Attribute
6
+ promise :finished
7
+ promise :served
8
+
9
+ attr_reader :http_method, :uri, :headers, :body
10
+ attr_accessor :http_version
37
11
 
38
- hsh["REQUEST_PATH"], qm, hsh["QUERY_STRING"] = uri.partition("?")
39
- hsh["PATH_INFO"], hsh["SCRIPT_NAME"] = hsh["REQUEST_PATH"].dup, ""
40
-
41
- hsh["HTTP_VERSION"] = "HTTP/#{http_version}"
42
- end
12
+ # TODO: URI.join is really slow
13
+ def initialize(http_method, uri, headers = {}, body = '')
14
+ # @http_method, @uri = http_method, URI('http://' + uri)
15
+ @http_method, @uri = http_method, URI.join('http:///', uri).request_uri
16
+ @headers, @body = headers, body
17
+ @http_version = 1.1
18
+ super()
43
19
  end
44
20
  end
45
21
  end