httpkit 0.6.0.pre.3

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.
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