httpkit 0.6.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +3 -0
  3. data/.travis.yml +15 -0
  4. data/.yardopts +2 -0
  5. data/Gemfile +19 -0
  6. data/Gemfile.devtools +67 -0
  7. data/Procfile +1 -0
  8. data/README.md +66 -0
  9. data/Rakefile +5 -0
  10. data/UNLICENSE +24 -0
  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 +114 -0
  16. data/config/rubocop.yml +56 -0
  17. data/config/yardstick.yml +2 -0
  18. data/examples/echo_server.rb +36 -0
  19. data/examples/getting_started.rb +34 -0
  20. data/httpkit.gemspec +27 -0
  21. data/lib/httpkit/body.rb +67 -0
  22. data/lib/httpkit/client/keep_alive.rb +10 -0
  23. data/lib/httpkit/client/timeouts.rb +14 -0
  24. data/lib/httpkit/client.rb +94 -0
  25. data/lib/httpkit/connection/eventmachine.rb +72 -0
  26. data/lib/httpkit/connection/status.rb +28 -0
  27. data/lib/httpkit/promise.rb +19 -0
  28. data/lib/httpkit/request.rb +19 -0
  29. data/lib/httpkit/response.rb +110 -0
  30. data/lib/httpkit/serializer/encoding.rb +43 -0
  31. data/lib/httpkit/serializer.rb +75 -0
  32. data/lib/httpkit/server/keep_alive.rb +58 -0
  33. data/lib/httpkit/server/timeouts.rb +13 -0
  34. data/lib/httpkit/server.rb +62 -0
  35. data/lib/httpkit/support/handler_manager.rb +25 -0
  36. data/lib/httpkit/support/message.rb +66 -0
  37. data/lib/httpkit/version.rb +5 -0
  38. data/lib/httpkit.rb +49 -0
  39. data/spec/integration/error_handling_spec.rb +76 -0
  40. data/spec/integration/keep_alive_spec.rb +101 -0
  41. data/spec/integration/smoke_spec.rb +21 -0
  42. data/spec/integration/streaming_spec.rb +57 -0
  43. data/spec/integration/timeouts_spec.rb +82 -0
  44. data/spec/shared/integration/server_client_pair.rb +29 -0
  45. data/spec/spec_helper.rb +45 -0
  46. data/spec/support/handler.rb +48 -0
  47. data/spec/support/helper.rb +70 -0
  48. data/spec/unit/client_spec.rb +230 -0
  49. data/spec/unit/connection/eventmachine_spec.rb +211 -0
  50. data/spec/unit/connection/status_spec.rb +83 -0
  51. data/spec/unit/httpkit_spec.rb +41 -0
  52. data/spec/unit/promise_spec.rb +56 -0
  53. data/spec/unit/request_spec.rb +35 -0
  54. data/spec/unit/response_spec.rb +108 -0
  55. data/spec/unit/server/keep_alive_spec.rb +69 -0
  56. data/spec/unit/server_spec.rb +128 -0
  57. data/spec/unit/support/handler_manager_spec.rb +21 -0
  58. data/spec/unit/support/message_spec.rb +115 -0
  59. metadata +190 -0
data/lib/httpkit.rb ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'eventmachine'
4
+ require 'fiber'
5
+ require 'promise'
6
+ require 'http/parser'
7
+ require 'time'
8
+
9
+ require 'httpkit/version'
10
+
11
+ require 'httpkit/support/handler_manager'
12
+ require 'httpkit/support/message'
13
+
14
+ require 'httpkit/promise'
15
+ require 'httpkit/body'
16
+ require 'httpkit/request'
17
+ require 'httpkit/response'
18
+
19
+ require 'httpkit/connection/eventmachine'
20
+ require 'httpkit/connection/status'
21
+ require 'httpkit/serializer/encoding'
22
+ require 'httpkit/serializer'
23
+
24
+ require 'httpkit/client'
25
+ require 'httpkit/client/keep_alive'
26
+ require 'httpkit/client/timeouts'
27
+ require 'httpkit/server'
28
+ require 'httpkit/server/keep_alive'
29
+ require 'httpkit/server/timeouts'
30
+
31
+ module HTTPkit
32
+ def self.run
33
+ start do
34
+ yield
35
+ stop
36
+ end
37
+ end
38
+
39
+ def self.start
40
+ EM.run do
41
+ Fiber.new { yield }.resume
42
+ end
43
+ end
44
+
45
+ def self.stop
46
+ EM.stop
47
+ EM.next_tick {}
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Client error handling', reactor: true do
6
+ include_context :server_client_pair
7
+
8
+ let(:served) { client.perform(open_request) }
9
+
10
+ subject! { client.close(reason) }
11
+
12
+ describe 'on connection error' do
13
+ let(:reason) { Errno::ENETUNREACH }
14
+
15
+ it 'rejects promises' do
16
+ expect(client).to be_closed
17
+ expect(client).to be_error
18
+ expect(client).to be_network_fault
19
+
20
+ expect(served).to be_rejected
21
+ expect(served.reason).to be(reason)
22
+ end
23
+ end
24
+
25
+ describe 'on timeout' do
26
+ let(:reason) { Errno::ETIMEDOUT }
27
+
28
+ it 'rejects promises' do
29
+ expect(client).to be_closed
30
+ expect(client).to be_error
31
+ expect(client).to be_timeout
32
+
33
+ expect(served).to be_rejected
34
+ expect(served.reason).to be(reason)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'Server error handling', reactor: true do
40
+ include_context :server_client_pair
41
+
42
+ describe 'on application error' do
43
+ let(:server_config) { { handlers: [handler] } }
44
+ let(:handler) { double('handler') }
45
+ let(:reason) { RuntimeError.new }
46
+
47
+ before do
48
+ allow(handler).to receive(:serve) { raise reason }
49
+ end
50
+
51
+ subject! do
52
+ client.perform(open_request)
53
+ tick(2)
54
+ end
55
+
56
+ it 'rejects promises' do
57
+ expect(server).to be_closed
58
+ expect(server).to be_error
59
+ end
60
+ end
61
+
62
+ describe 'on timeout' do
63
+ subject! do
64
+ client.perform(open_request)
65
+ tick(1)
66
+ server.close(Errno::ETIMEDOUT)
67
+ tick(1)
68
+ end
69
+
70
+ it 'rejects promises' do
71
+ expect(server).to be_closed
72
+ expect(server).to be_error
73
+ expect(server).to be_timeout
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe reactor: true do
6
+ include_context :server_client_pair
7
+
8
+ let(:server_config) do
9
+ { handlers: [HTTPkit::Server::KeepAlive.new, SpecHandler.new] }
10
+ end
11
+ let(:client_config) do
12
+ { handlers: [HTTPkit::Client::KeepAlive.new] }
13
+ end
14
+
15
+ let!(:served) do
16
+ requests.map { |request| client.perform(request) }
17
+ end
18
+ let(:responses) { served.map(&:sync) }
19
+
20
+ describe HTTPkit::Server, 'with KeepAlive' do
21
+ let(:requests) do
22
+ [closed_request(:get, '/sleep'), closed_request]
23
+ end
24
+
25
+ # XXX: tends to fail
26
+ it 'sends responses in the correct order' do
27
+ expect(responses[0].body.to_s).to eq('/sleep')
28
+ expect(responses[1].body.to_s).to eq('/')
29
+ end
30
+
31
+ describe 'and HTTP/1.1 request' do
32
+ let(:requests) { [closed_request] }
33
+
34
+ it 'assumes Connection: keep-alive' do
35
+ expect(client).not_to be_closed
36
+ expect(server).not_to be_closed
37
+
38
+ expect(responses[0].headers).to include('Connection' => 'keep-alive')
39
+ end
40
+ end
41
+
42
+ describe 'and HTTP/1.0 request' do
43
+ let(:requests) do
44
+ [closed_request.tap { |r| r.http_version = 1.0 }]
45
+ end
46
+
47
+ it 'assumes Connection: close' do
48
+ responses
49
+ tick(2)
50
+ expect(client).to be_closed
51
+ expect(server).to be_closed
52
+
53
+ expect(responses[0].headers).to include('Connection' => 'close')
54
+ end
55
+ end
56
+
57
+ describe 'and Connection: keep-alive' do
58
+ let(:requests) do
59
+ [closed_request(:get, '/', 'Connection' => 'keep-alive')
60
+ .tap { |r| r.http_version = 1.0 }]
61
+ end
62
+
63
+ it 'keeps the connection open' do
64
+ expect(client).not_to be_closed
65
+ expect(server).not_to be_closed
66
+
67
+ expect(responses[0].headers).to include('Connection' => 'keep-alive')
68
+ end
69
+ end
70
+
71
+ describe 'and Connection: close' do
72
+ let(:requests) do
73
+ [closed_request(:get, '/', 'Connection' => 'close')]
74
+ end
75
+
76
+ it 'closes the connection after sending the response' do
77
+ responses
78
+ tick(2)
79
+ expect(client).to be_closed
80
+ expect(server).to be_closed
81
+
82
+ expect(responses[0].headers).to include('Connection' => 'close')
83
+ end
84
+ end
85
+ end
86
+
87
+ describe HTTPkit::Client, 'with KeepAlive' do
88
+ let(:requests) do
89
+ [open_request(:get, '/?1'), closed_request(:get, '/?2')]
90
+ end
91
+
92
+ before do
93
+ requests[0].close
94
+ end
95
+
96
+ it 'sends one request at a time' do
97
+ expect(responses[0].body.to_s).to eq('/?1')
98
+ expect(responses[1].body.to_s).to eq('/?2')
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Smoke test', reactor: true do
6
+ include_context :server_client_pair
7
+
8
+ let(:response) { client.request(:get, '/') }
9
+ let(:request) { intercepted_requests[0] }
10
+
11
+ it 'exchanges request and response' do
12
+ expect(response.status).to be(200)
13
+ expect(response.headers).to include('Content-Length' => '1')
14
+ expect(response.body.to_s).to eq('/')
15
+
16
+ expect(request.http_method).to be(:get)
17
+ expect(request.uri).to eq('/')
18
+ expect(request.headers).to include('Content-Length' => '0')
19
+ expect(request.body.to_s).to eq('')
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Request body streaming', reactor: true do
6
+ include_context :server_client_pair
7
+
8
+ let(:chunks) { [] }
9
+
10
+ before do
11
+ client.perform(request)
12
+ tick(2)
13
+ Fiber.new do
14
+ intercepted_requests[0].body.each do |chunk|
15
+ chunks << chunk
16
+ end
17
+ end.resume
18
+ end
19
+
20
+ let(:request) { open_request }
21
+
22
+ subject! do
23
+ %w[foo bar baz].each do |chunk|
24
+ request.body.closed.progress(chunk)
25
+ tick
26
+ end
27
+ request.close
28
+ tick
29
+ end
30
+
31
+ it 'progressively sends the body' do
32
+ expect(intercepted_requests[0].headers)
33
+ .to include('Transfer-Encoding' => 'chunked')
34
+
35
+ expect(chunks).to eq(%w[foo bar baz])
36
+ end
37
+ end
38
+
39
+ describe 'Response body streaming', reactor: true do
40
+ include_context :server_client_pair
41
+
42
+ let(:chunks) { [] }
43
+
44
+ let(:response) { client.request(:get, '/streaming') }
45
+
46
+ subject! { response }
47
+
48
+ before do
49
+ response.body.each do |chunk|
50
+ chunks << chunk
51
+ end
52
+ end
53
+
54
+ it 'progressively receives the body' do
55
+ expect(chunks).to eq(%w[foo bar baz])
56
+ end
57
+ end
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe HTTPkit::Client, 'in connecting state', reactor: true do
6
+ # connect to a non-existing host to provoke connection timeout
7
+ let(:address) { '1.2.3.4' }
8
+ let(:client_config) do
9
+ { address: address, port: random_port,
10
+ handlers: [HTTPkit::Client::Timeouts.new] }
11
+ end
12
+ let!(:client) { HTTPkit::Client.start(client_config) }
13
+
14
+ after { client.close }
15
+
16
+ it 'times out after 2 seconds' do
17
+ # only test the config, and don't actually wait for 2 seconds
18
+ expect(client.config).to include(connect_timeout: 2.0)
19
+ end
20
+
21
+ describe 'with :connect_timeout option' do
22
+ let(:client_config) do
23
+ { address: address, port: random_port, connect_timeout: 0.01,
24
+ handlers: [HTTPkit::Client::Timeouts.new] }
25
+ end
26
+
27
+ it 'times out after $n seconds' do
28
+ async_sleep(0.015)
29
+ if client.network_fault?
30
+ pending 'Requires network connection'
31
+ else
32
+ expect(client).to be_timeout
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ describe HTTPkit::Client, 'in idle state', reactor: true do
39
+ include_context :server_client_pair
40
+
41
+ let(:client_config) { { handlers: [HTTPkit::Client::Timeouts.new] } }
42
+
43
+ it 'times out after 2 seconds' do
44
+ # only test the config, and don't actually wait for 2 seconds
45
+ expect(client.config).to include(connect_timeout: 2.0)
46
+ end
47
+
48
+ describe 'with :timeout option' do
49
+ let(:client_config) do
50
+ { timeout: 0.01, handlers: [HTTPkit::Client::Timeouts.new] }
51
+ end
52
+
53
+ it 'times out after $n seconds' do
54
+ async_sleep(0.015)
55
+ expect(client).to be_timeout
56
+ expect(server).to be_closed
57
+ end
58
+ end
59
+ end
60
+
61
+ describe HTTPkit::Server, 'in idle state', reactor: true do
62
+ include_context :server_client_pair
63
+
64
+ let(:server_config) { { handlers: [HTTPkit::Server::Timeouts.new] } }
65
+
66
+ it 'times out after 2 seconds' do
67
+ # only test the config, and don't actually wait for 2 seconds
68
+ expect(server.config).to include(timeout: 2.0)
69
+ end
70
+
71
+ describe 'with :timeout option' do
72
+ let(:server_config) do
73
+ { timeout: 0.01, handlers: [HTTPkit::Server::Timeouts.new] }
74
+ end
75
+
76
+ it 'times out after $n seconds' do
77
+ async_sleep(0.015)
78
+ expect(client).to be_closed
79
+ expect(server).to be_timeout
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ shared_context :server_client_pair do
4
+ let(:server_config) { { handlers: [SpecHandler.new] } }
5
+ let(:client_config) { {} }
6
+ let(:interceptor) { double('interceptor') }
7
+
8
+ let(:server_and_client) do
9
+ server_client_pair(server_config, client_config, interceptor)
10
+ end
11
+
12
+ let(:intercepted_requests) { [] }
13
+ let(:intercepted_promises) { [] }
14
+
15
+ before do
16
+ allow(interceptor).to receive(:serve) { |request, served|
17
+ intercepted_requests << request
18
+ intercepted_promises << served
19
+ }
20
+ end
21
+
22
+ let!(:server) { server_and_client[0] }
23
+ let!(:client) { server_and_client[1] }
24
+
25
+ after do
26
+ server.close
27
+ client.close
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ if ENV['COVERAGE'] == 'true'
4
+ require 'simplecov'
5
+ require 'coveralls'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ Coveralls::SimpleCov::Formatter
10
+ ]
11
+
12
+ SimpleCov.start do
13
+ command_name 'spec:unit'
14
+
15
+ add_filter 'config'
16
+ add_filter 'spec'
17
+ end
18
+ end
19
+
20
+ require 'httpkit'
21
+ require 'weakref'
22
+
23
+ require 'awesome_print'
24
+
25
+ require 'devtools/spec_helper'
26
+
27
+ RSpec.configure do |config|
28
+ config.include(SpecHelper)
29
+
30
+ config.around do |example|
31
+ if example.metadata[:reactor]
32
+ EM.run do
33
+ EM.add_timer(0.5) { raise 'Example timed out' }
34
+
35
+ Fiber.new do
36
+ example.call
37
+ EM.stop
38
+ EM.next_tick {}
39
+ end.resume
40
+ end
41
+ else
42
+ Timeout.timeout(0.5) { example.call }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ class SpecHandler
4
+ def serve(request, served)
5
+ served.fulfill(response_for(request))
6
+ end
7
+
8
+ private
9
+
10
+ def response_for(request)
11
+ case request.uri
12
+ when '/sleep' then sleep_response
13
+ when '/close' then close_response
14
+ when '/streaming' then streaming_response
15
+ else echo_response(request)
16
+ end
17
+ end
18
+
19
+ def echo_response(request)
20
+ HTTPkit::Response.new(200, { 'Content-Type' => 'text/plain' },
21
+ request.uri)
22
+ end
23
+
24
+ def sleep_response
25
+ SpecHelper.async_sleep(0.01)
26
+ HTTPkit::Response.new(200, { 'Content-Type' => 'text/plain' }, '/sleep')
27
+ end
28
+
29
+ def close_response
30
+ HTTPkit::Response.new(200,
31
+ { 'Content-Type' => 'text/plain',
32
+ 'Connection' => 'close' },
33
+ '/close')
34
+ end
35
+
36
+ def streaming_response
37
+ response = HTTPkit::Response.new(200, { 'Content-Type' => 'text/plain' },
38
+ HTTPkit::Body.new)
39
+ SpecHelper.defer do
40
+ %w[foo bar baz].each do |chunk|
41
+ SpecHelper.tick(2)
42
+ response.body.closed.progress(chunk)
43
+ end
44
+ response.close
45
+ end
46
+ response
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ # encoding: utf-8
2
+
3
+ module SpecHelper
4
+ def async_sleep(seconds)
5
+ fiber = Fiber.current
6
+ EM.add_timer(seconds) { fiber.resume }
7
+ Fiber.yield
8
+ end
9
+ module_function :async_sleep
10
+
11
+ def tick(ticks = 1)
12
+ fiber = Fiber.current
13
+ EM.next_tick { fiber.resume }
14
+ Fiber.yield
15
+
16
+ tick(ticks - 1) if ticks > 1
17
+ end
18
+ module_function :tick
19
+
20
+ def defer
21
+ EM.next_tick do
22
+ Fiber.new { yield }.resume
23
+ end
24
+ end
25
+ module_function :defer
26
+
27
+ def open_request(http_method = :get, uri = '/', headers = {})
28
+ HTTPkit::Request.new(http_method, uri, headers, HTTPkit::Body.new)
29
+ end
30
+
31
+ def closed_request(*args)
32
+ open_request(*args).tap(&:close)
33
+ end
34
+
35
+ def open_response(status = 200, headers = {})
36
+ HTTPkit::Response.new(status, headers, HTTPkit::Body.new)
37
+ end
38
+
39
+ def closed_response(*args)
40
+ open_response(*args).tap(&:close)
41
+ end
42
+
43
+ def localhost
44
+ '127.0.0.1'
45
+ end
46
+
47
+ def random_port
48
+ server = TCPServer.new(localhost, 0)
49
+ server.addr[1]
50
+ ensure
51
+ server.shutdown
52
+ end
53
+
54
+ def server_client_pair(server_config, client_config, interceptor)
55
+ server = nil
56
+ inspect_server(server_config, interceptor) { |_, s, _| server = s }
57
+
58
+ config = { address: localhost, port: random_port }
59
+ HTTPkit::Server.start(config.merge(server_config))
60
+ client = HTTPkit::Client.start(config.merge(client_config))
61
+
62
+ tick
63
+ [server, client]
64
+ end
65
+
66
+ def inspect_server(server_config, interceptor, &block)
67
+ allow(interceptor).to receive(:setup, &block)
68
+ server_config[:handlers].unshift(interceptor)
69
+ end
70
+ end