goliath 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of goliath might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +5 -4
- data/goliath.gemspec +10 -7
- data/lib/goliath/api.rb +1 -1
- data/lib/goliath/rack/jsonp.rb +7 -2
- data/lib/goliath/rack/params.rb +3 -2
- data/lib/goliath/rack/validator.rb +1 -1
- data/lib/goliath/request.rb +2 -2
- data/lib/goliath/runner.rb +4 -3
- data/lib/goliath/server.rb +17 -2
- data/lib/goliath/test_helper_sse.rb +76 -0
- data/lib/goliath/validation/error.rb +4 -1
- data/lib/goliath/version.rb +1 -1
- data/spec/integration/early_abort_spec.rb +3 -3
- data/spec/integration/event_stream_spec.rb +50 -0
- data/spec/integration/exception_handling_spec.rb +202 -0
- data/spec/integration/jsonp_spec.rb +61 -10
- data/spec/integration/test_helper_spec.rb +1 -1
- data/spec/integration/valid_spec.rb +21 -1
- data/spec/unit/api_spec.rb +1 -1
- data/spec/unit/env_spec.rb +5 -5
- data/spec/unit/headers_spec.rb +2 -2
- data/spec/unit/rack/formatters/json_spec.rb +2 -2
- data/spec/unit/rack/formatters/xml_spec.rb +2 -2
- data/spec/unit/rack/formatters/yaml_spec.rb +2 -2
- data/spec/unit/rack/params_spec.rb +41 -6
- data/spec/unit/rack/validation/default_params_spec.rb +2 -2
- data/spec/unit/rack/validation/numeric_range_spec.rb +2 -2
- data/spec/unit/rack/validation/param_spec.rb +18 -18
- data/spec/unit/rack/validation/required_param_spec.rb +25 -25
- data/spec/unit/rack/validation/required_value_spec.rb +10 -10
- data/spec/unit/request_spec.rb +3 -3
- data/spec/unit/runner_spec.rb +1 -1
- data/spec/unit/server_spec.rb +4 -4
- metadata +135 -94
- data/examples/around.rb +0 -38
- data/examples/clone.rb +0 -26
- data/examples/router.rb +0 -15
- data/examples/test.rb +0 -31
- data/examples/upload.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2657a958a80151a0eab64c006eb13a4905156cc3
|
4
|
+
data.tar.gz: b140e7bdc85173bb06adeef69b504d7ab39d5197
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bdf5b587bcc2edefd5242b4c447e45787bd9affff51010c47484b9644ba005bc194d68b939c7117f78d375e3ebc16e05abc1292ff3b34626d9329fd140551957
|
7
|
+
data.tar.gz: 1cec64325f506e5838ec7893055c5e15cd52375c0e435b8cab69057d17424aa535f02537eab764a551064a161baeab27245d3a00a451ca0ca8215e720c3d0f1e
|
data/README.md
CHANGED
@@ -64,13 +64,14 @@ Goliath has been used in production environments for 2+ years, across many diffe
|
|
64
64
|
* [Middleware](https://github.com/postrank-labs/goliath/wiki/Middleware)
|
65
65
|
* [Configuration](https://github.com/postrank-labs/goliath/wiki/Configuration)
|
66
66
|
* [Plugins](https://github.com/postrank-labs/goliath/wiki/Plugins)
|
67
|
+
* [Zero Downtime Restart](https://github.com/postrank-labs/goliath/wiki/Zero-downtime-restart)
|
68
|
+
|
67
69
|
|
68
70
|
### Hands-on applications:
|
69
71
|
|
70
|
-
If you are you new to EventMachine, or want a detailed walk-through of building a Goliath powered API? You're in luck,
|
72
|
+
If you are you new to EventMachine, or want a detailed walk-through of building a Goliath powered API? You're in luck, a super-awesome Pluralsight screencast which will teach you all you need to know:
|
71
73
|
|
72
|
-
* [Meet EventMachine
|
73
|
-
* [Meet EventMachine: Part 2](http://peepcode.com/products/eventmachine-ii) - building an API with Goliath
|
74
|
+
* [Meet EventMachine](http://www.pluralsight.com/courses/meet-eventmachine) - introduction to EM, Fibers, building an API with Goliath
|
74
75
|
|
75
76
|
Additionally, you can also watch this presentation from GoGaRuCo 2011, which describes the design and motivation behind Goliath:
|
76
77
|
|
@@ -90,4 +91,4 @@ Other resources:
|
|
90
91
|
|
91
92
|
## License & Acknowledgments
|
92
93
|
|
93
|
-
Goliath is distributed under the MIT license, for full details please see the LICENSE file.
|
94
|
+
Goliath is distributed under the MIT license, for full details please see the LICENSE file.
|
data/goliath.gemspec
CHANGED
@@ -12,13 +12,14 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.summary = 'Async framework for writing API servers'
|
13
13
|
s.description = s.summary
|
14
14
|
|
15
|
-
s.required_ruby_version = '>=1.
|
15
|
+
s.required_ruby_version = '>=2.1.0'
|
16
16
|
|
17
17
|
s.add_dependency 'eventmachine', '>= 1.0.0.beta.4'
|
18
18
|
s.add_dependency 'em-synchrony', '>= 1.0.0'
|
19
|
-
s.add_dependency 'em-websocket',
|
20
|
-
s.add_dependency 'http_parser.rb', '0.6.0'
|
19
|
+
s.add_dependency 'em-websocket', '0.3.8'
|
20
|
+
s.add_dependency 'http_parser.rb', '>= 0.6.0'
|
21
21
|
s.add_dependency 'log4r'
|
22
|
+
s.add_dependency 'einhorn'
|
22
23
|
|
23
24
|
s.add_dependency 'rack', '>=1.2.2'
|
24
25
|
s.add_dependency 'rack-contrib'
|
@@ -27,7 +28,8 @@ Gem::Specification.new do |s|
|
|
27
28
|
s.add_dependency 'multi_json'
|
28
29
|
|
29
30
|
s.add_development_dependency 'rake', '>=0.8.7'
|
30
|
-
s.add_development_dependency 'rspec', '
|
31
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
32
|
+
s.add_development_dependency 'test-unit'
|
31
33
|
s.add_development_dependency 'nokogiri'
|
32
34
|
s.add_development_dependency 'em-http-request', '>=1.0.0'
|
33
35
|
s.add_development_dependency 'em-mongo', '~> 0.4.0'
|
@@ -35,14 +37,15 @@ Gem::Specification.new do |s|
|
|
35
37
|
s.add_development_dependency 'multipart_body'
|
36
38
|
s.add_development_dependency 'amqp', '>=0.7.1'
|
37
39
|
s.add_development_dependency 'em-websocket-client'
|
40
|
+
s.add_development_dependency 'em-eventsource'
|
38
41
|
|
39
42
|
s.add_development_dependency 'tilt', '>=1.2.2'
|
40
43
|
s.add_development_dependency 'haml', '>=3.0.25'
|
41
44
|
s.add_development_dependency 'yard'
|
42
45
|
|
43
|
-
s.add_development_dependency 'guard', '~>
|
44
|
-
s.add_development_dependency 'guard-rspec', '~>
|
45
|
-
s.add_development_dependency 'listen', '~>
|
46
|
+
s.add_development_dependency 'guard', '~> 2.0'
|
47
|
+
s.add_development_dependency 'guard-rspec', '~> 4.0'
|
48
|
+
s.add_development_dependency 'listen', '~> 2.0'
|
46
49
|
|
47
50
|
if RUBY_PLATFORM != 'java'
|
48
51
|
s.add_development_dependency 'yajl-ruby'
|
data/lib/goliath/api.rb
CHANGED
@@ -172,7 +172,7 @@ module Goliath
|
|
172
172
|
|
173
173
|
rescue Goliath::Validation::Error => e
|
174
174
|
env[RACK_EXCEPTION] = e
|
175
|
-
env[ASYNC_CALLBACK].call(validation_error(e.status_code, e.message))
|
175
|
+
env[ASYNC_CALLBACK].call(validation_error(e.status_code, e.message, e.headers))
|
176
176
|
|
177
177
|
rescue Exception => e
|
178
178
|
logthis = "#{e.backtrace[0]}: #{e.message} (#{e.class})\n"
|
data/lib/goliath/rack/jsonp.rb
CHANGED
@@ -18,10 +18,15 @@ module Goliath
|
|
18
18
|
response = body
|
19
19
|
end
|
20
20
|
|
21
|
+
response = "#{env.params['callback']}(#{response})"
|
22
|
+
|
21
23
|
headers[Goliath::Constants::CONTENT_TYPE] = 'application/javascript'
|
22
|
-
|
24
|
+
if headers['Content-Length']
|
25
|
+
headers['Content-Length'] = response.bytesize.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
[status, headers, [response]]
|
23
29
|
end
|
24
30
|
end
|
25
31
|
end
|
26
32
|
end
|
27
|
-
|
data/lib/goliath/rack/params.rb
CHANGED
@@ -60,10 +60,11 @@ module Goliath
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def call(env)
|
63
|
-
Goliath::Rack::Validator.safely(env) do
|
63
|
+
error_response = Goliath::Rack::Validator.safely(env) do
|
64
64
|
env['params'] = retrieve_params(env)
|
65
|
-
|
65
|
+
nil
|
66
66
|
end
|
67
|
+
error_response || @app.call(env)
|
67
68
|
end
|
68
69
|
end
|
69
70
|
end
|
@@ -39,7 +39,7 @@ module Goliath
|
|
39
39
|
begin
|
40
40
|
yield
|
41
41
|
rescue Goliath::Validation::Error => e
|
42
|
-
validation_error(e.status_code, e.message, headers)
|
42
|
+
validation_error(e.status_code, e.message, e.headers.merge(headers))
|
43
43
|
rescue Exception => e
|
44
44
|
env.logger.error(e.message)
|
45
45
|
env.logger.error(e.backtrace.join("\n"))
|
data/lib/goliath/request.rb
CHANGED
@@ -92,7 +92,7 @@ module Goliath
|
|
92
92
|
|
93
93
|
@env[REQUEST_METHOD] = parser.http_method
|
94
94
|
@env[REQUEST_URI] = parser.request_url
|
95
|
-
@env[QUERY_STRING] = uri.query
|
95
|
+
@env[QUERY_STRING] = uri.query || ''.freeze
|
96
96
|
@env[HTTP_VERSION] = parser.http_version.join('.')
|
97
97
|
@env[SCRIPT_NAME] = ""
|
98
98
|
@env[REQUEST_PATH] = uri.path
|
@@ -226,7 +226,7 @@ module Goliath
|
|
226
226
|
# @return [Nil]
|
227
227
|
def server_exception(e)
|
228
228
|
if e.is_a?(Goliath::Validation::Error)
|
229
|
-
status, headers, body = [e.status_code,
|
229
|
+
status, headers, body = [e.status_code, e.headers, ('{"error":"%s"}' % e.message)]
|
230
230
|
else
|
231
231
|
@env[RACK_LOGGER].error("#{e.message}\n#{e.backtrace.join("\n")}")
|
232
232
|
message = Goliath.env?(:production) ? 'An error happened' : e.message
|
data/lib/goliath/runner.rb
CHANGED
@@ -150,6 +150,7 @@ module Goliath
|
|
150
150
|
opts.on('-a', '--address HOST', "Bind to HOST address (default: #{@options[:address]})") { |addr| @options[:address] = addr }
|
151
151
|
opts.on('-p', '--port PORT', "Use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
|
152
152
|
opts.on('-S', '--socket FILE', "Bind to unix domain socket") { |v| @options[:address] = v; @options[:port] = nil }
|
153
|
+
opts.on('-E', '--einhorn', "Use Einhorn socket manager") { |v| @options[:einhorn] = true }
|
153
154
|
|
154
155
|
opts.separator ""
|
155
156
|
opts.separator "Daemon options:"
|
@@ -211,7 +212,8 @@ module Goliath
|
|
211
212
|
|
212
213
|
File.umask(0000)
|
213
214
|
|
214
|
-
|
215
|
+
log_extension = File.extname(@log_file)
|
216
|
+
stdout_log_file = "#{File.dirname(@log_file)}/#{File.basename(@log_file, log_extension)}_stdout#{log_extension}"
|
215
217
|
|
216
218
|
STDIN.reopen("/dev/null")
|
217
219
|
STDOUT.reopen(stdout_log_file, "a")
|
@@ -296,8 +298,7 @@ module Goliath
|
|
296
298
|
# @return [Nil]
|
297
299
|
def run_server
|
298
300
|
log = setup_logger
|
299
|
-
|
300
|
-
log.info("Starting server on #{@address}:#{@port} in #{Goliath.env} mode. Watch out for stones.")
|
301
|
+
log.info("Starting server on http#{ @server_options[:ssl] ? 's' : nil }://#{@address}:#{@port} in #{Goliath.env} mode. Watch out for stones.")
|
301
302
|
|
302
303
|
server = setup_server(log)
|
303
304
|
server.api.setup if server.api.respond_to?(:setup)
|
data/lib/goliath/server.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'em-synchrony'
|
2
|
+
require 'einhorn'
|
2
3
|
require 'goliath/connection'
|
3
4
|
require 'goliath/goliath'
|
4
5
|
|
@@ -83,7 +84,7 @@ module Goliath
|
|
83
84
|
|
84
85
|
EM.set_effective_user(options[:user]) if options[:user]
|
85
86
|
|
86
|
-
config[Goliath::Constants::GOLIATH_SIGNATURE] =
|
87
|
+
config[Goliath::Constants::GOLIATH_SIGNATURE] = start_server(options) do |conn|
|
87
88
|
if options[:ssl]
|
88
89
|
conn.start_tls(
|
89
90
|
:private_key_file => options[:ssl_key],
|
@@ -105,9 +106,23 @@ module Goliath
|
|
105
106
|
end
|
106
107
|
end
|
107
108
|
|
109
|
+
def start_server(options, &blk)
|
110
|
+
if options[:einhorn]
|
111
|
+
fd_num = Einhorn::Worker.socket!
|
112
|
+
socket = Socket.for_fd(fd_num)
|
113
|
+
|
114
|
+
EM.attach_server(socket, Goliath::Connection, &blk)
|
115
|
+
else
|
116
|
+
EM.start_server(address, port, Goliath::Connection, &blk)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
108
120
|
# Stops the server running.
|
109
121
|
def stop
|
110
|
-
EM.
|
122
|
+
EM.add_timer(0) do
|
123
|
+
logger.info('Stopping server...')
|
124
|
+
EM.stop
|
125
|
+
end
|
111
126
|
end
|
112
127
|
|
113
128
|
# Loads a configuration file
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'em-eventsource'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module TestHelper
|
5
|
+
class SSEHelper
|
6
|
+
attr_reader :connection
|
7
|
+
def initialize(url)
|
8
|
+
@message_queue = EM::Queue.new
|
9
|
+
@named_queues = {}
|
10
|
+
@connection = EM::EventSource.new(url)
|
11
|
+
end
|
12
|
+
|
13
|
+
def listen
|
14
|
+
@connection.message do |message|
|
15
|
+
@message_queue.push(message)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def listen_to(name)
|
20
|
+
queue = (@named_queues[name] ||= [])
|
21
|
+
@connection.on(name) do |message|
|
22
|
+
queue.push(message)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def receive
|
27
|
+
pop_queue(@message_queue)
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_on(name)
|
31
|
+
queue = @named_queues.fetch(name) do
|
32
|
+
raise ArgumentError, "You have to call listen_to('#{name}') first"
|
33
|
+
end
|
34
|
+
|
35
|
+
pop_queue(queue)
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_async_http
|
39
|
+
klass = EM::HttpConnection
|
40
|
+
if klass.instance_methods.include?(:aget)
|
41
|
+
begin
|
42
|
+
klass.send(:class_eval) do
|
43
|
+
alias :sget :get
|
44
|
+
alias :get :aget
|
45
|
+
end
|
46
|
+
yield if block_given?
|
47
|
+
ensure
|
48
|
+
klass.send(:class_eval) do
|
49
|
+
alias :get :sget
|
50
|
+
remove_method :sget
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
yield if block_given?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def pop_queue(queue)
|
61
|
+
fiber = Fiber.current
|
62
|
+
queue.pop { |m| fiber.resume(m) }
|
63
|
+
Fiber.yield
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def sse_client_connect(path,&blk)
|
68
|
+
url = "http://localhost:#{@test_server_port}#{path}"
|
69
|
+
client = SSEHelper.new(url)
|
70
|
+
client.with_async_http { client.connection.start }
|
71
|
+
client.listen
|
72
|
+
Fiber.new { blk.call(client) }.resume if blk
|
73
|
+
stop
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -4,6 +4,8 @@ module Goliath
|
|
4
4
|
class Error < StandardError
|
5
5
|
# The status code to return from the error handler
|
6
6
|
attr_accessor :status_code
|
7
|
+
# The headers to return from the error handler
|
8
|
+
attr_accessor :headers
|
7
9
|
|
8
10
|
# Create a new Goliath::Validation::Error.
|
9
11
|
#
|
@@ -13,9 +15,10 @@ module Goliath
|
|
13
15
|
# @param status_code [Integer] The status code to return
|
14
16
|
# @param message [String] The error message to return
|
15
17
|
# @return [Goliath::Validation::Error] The Goliath::Validation::Error
|
16
|
-
def initialize(status_code, message)
|
18
|
+
def initialize(status_code, message, headers = {})
|
17
19
|
super(message)
|
18
20
|
@status_code = status_code
|
21
|
+
@headers = headers
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
data/lib/goliath/version.rb
CHANGED
@@ -25,7 +25,7 @@ describe EarlyAbort do
|
|
25
25
|
|
26
26
|
post_request(request_data, err) do |c|
|
27
27
|
c.response.should == "{\"error\":\"Can't handle requests with X-Crash: true.\"}"
|
28
|
-
File.exist?("/tmp/goliath-test-error.log").should
|
28
|
+
File.exist?("/tmp/goliath-test-error.log").should be false
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
@@ -36,8 +36,8 @@ describe EarlyAbort do
|
|
36
36
|
|
37
37
|
post_request(request_data, err) do |c|
|
38
38
|
c.response.should =~ /Payload size can't exceed 10 bytes/
|
39
|
-
File.exist?("/tmp/goliath-test-error.log").should
|
39
|
+
File.exist?("/tmp/goliath-test-error.log").should be false
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
|
-
end
|
43
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require File.join(File.dirname(__FILE__), '../../', 'lib/goliath/test_helper_sse')
|
3
|
+
require File.join(File.dirname(__FILE__), '../../', 'lib/goliath')
|
4
|
+
require File.join(File.dirname(__FILE__), '../../', 'lib/goliath/api')
|
5
|
+
|
6
|
+
class EventStreamEndpoint < Goliath::API
|
7
|
+
def self.events
|
8
|
+
@events ||= EM::Queue.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def response(env)
|
12
|
+
self.class.events.pop do |event|
|
13
|
+
payload = if event.key?(:name)
|
14
|
+
"event: #{event[:name]}\ndata: #{event[:data]}\n\n"
|
15
|
+
else
|
16
|
+
"data: #{event[:data]}\n\n"
|
17
|
+
end
|
18
|
+
env.stream_send(payload)
|
19
|
+
end
|
20
|
+
streaming_response(200, 'Content-Type' => 'text/event-stream')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'EventStream' do
|
25
|
+
include Goliath::TestHelper
|
26
|
+
|
27
|
+
context 'named events' do
|
28
|
+
it 'should accept stream' do
|
29
|
+
with_api(EventStreamEndpoint, {:verbose => true, :log_stdout => true}) do |server|
|
30
|
+
sse_client_connect('/stream') do |client|
|
31
|
+
client.listen_to('custom_event')
|
32
|
+
EventStreamEndpoint.events.push(name: 'custom_event', data: 'content')
|
33
|
+
client.receive_on('custom_event').should == ['content']
|
34
|
+
client.receive.should == []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'unnamed events' do
|
41
|
+
it 'should accept stream' do
|
42
|
+
with_api(EventStreamEndpoint, {:verbose => true, :log_stdout => true}) do |server|
|
43
|
+
sse_client_connect('/stream') do |client|
|
44
|
+
EventStreamEndpoint.events.push(data: 'content')
|
45
|
+
client.receive.should == ['content']
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
# This integration spec isn't *quite* about exception handling. It's more of a
|
2
|
+
# regression test, since Goliath::Rack::Params used to swallow any exception
|
3
|
+
# generated downstream from it in the middleware chain (even though
|
4
|
+
# Goliath::API#call handles exceptions already).
|
5
|
+
|
6
|
+
require 'spec_helper'
|
7
|
+
|
8
|
+
class ExceptionHandlingMiddleware
|
9
|
+
include Goliath::Rack::AsyncMiddleware
|
10
|
+
include Goliath::Constants
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
# This kind of relies on downstream middleware raising their own exceptions
|
14
|
+
# if something goes wrong. Alternatively, they could rescue the exception
|
15
|
+
# and set env[RACK_EXCEPTION]. See #post_process.
|
16
|
+
super
|
17
|
+
rescue Exception => e
|
18
|
+
handle_exception(e)
|
19
|
+
end
|
20
|
+
|
21
|
+
def post_process(env, status, headers, body)
|
22
|
+
# If an exception was raised by Goliath::API#call, it will store the
|
23
|
+
# exception in env[RACK_EXCEPTION] and automatically generate an error
|
24
|
+
# response. We circumvent the usual error response by returning our own.
|
25
|
+
return handle_exception(env[RACK_EXCEPTION]) if env[RACK_EXCEPTION]
|
26
|
+
[status, headers, body]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def handle_exception(e)
|
32
|
+
# You could imagine this middleware having some sort of side effect, like
|
33
|
+
# sending your team an email alert detailing the exception. For the
|
34
|
+
# purposes of the spec, we'll just return some text.
|
35
|
+
[200, {}, "Exception raised: #{e.message}"]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ExceptionRaisingMiddleware
|
40
|
+
def initialize(app)
|
41
|
+
@app = app
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(env)
|
45
|
+
raise env.params['raise'] if env.params['raise']
|
46
|
+
@app.call(env)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ExceptionHandlingAPI < Goliath::API
|
51
|
+
# The exception handling middleware should come first in the chain so that
|
52
|
+
# any exception in the rest of the chain gets rescued.
|
53
|
+
use ExceptionHandlingMiddleware
|
54
|
+
|
55
|
+
# Require param parsing before the exception raising middleware, because the
|
56
|
+
# latter depends on params being parsed. Herein lies the regression:
|
57
|
+
# Goliath::Rack::Params used to swallow any exceptions raised downstream from
|
58
|
+
# it (here, ExceptionRaisingMiddleware), so any upstream middleware (here,
|
59
|
+
# ExceptionHandlingMiddleware) would be none the wiser. Really, the only
|
60
|
+
# thing Goliath::Rack::Params should be worried about is whether it can parse
|
61
|
+
# the params.
|
62
|
+
use Goliath::Rack::Params
|
63
|
+
|
64
|
+
# Allow us to raise an exception on demand with the param raise=<message>.
|
65
|
+
# You could imagine something less dumb for a real-life application that
|
66
|
+
# incidentally raises an exception, if you'd like.
|
67
|
+
use ExceptionRaisingMiddleware
|
68
|
+
|
69
|
+
def response(env)
|
70
|
+
# Goliath::API#call ensures that any exceptions raised here get rescued and
|
71
|
+
# stored in env[RACK_EXCEPTION].
|
72
|
+
fail env.params['fail'] if env.params['fail']
|
73
|
+
[200, {}, 'No exceptions raised']
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe ExceptionHandlingAPI do
|
78
|
+
let(:err) { Proc.new { fail 'API request failed' } }
|
79
|
+
|
80
|
+
context 'when no exceptions get raised' do
|
81
|
+
let(:query) { '' }
|
82
|
+
|
83
|
+
it 'returns a normal response' do
|
84
|
+
with_api(ExceptionHandlingAPI) do
|
85
|
+
get_request({ query: query }, err) do |c|
|
86
|
+
c.response_header.status.should == 200
|
87
|
+
c.response.should == 'No exceptions raised'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'when a middleware raises an exception' do
|
94
|
+
let(:query) { 'raise=zoinks' }
|
95
|
+
|
96
|
+
it 'handles the exception using ExceptionHandlingMiddleware' do
|
97
|
+
with_api(ExceptionHandlingAPI) do
|
98
|
+
get_request({ query: query }, err) do |c|
|
99
|
+
c.response_header.status.should == 200
|
100
|
+
c.response.should == 'Exception raised: zoinks'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when the API raises an exception' do
|
107
|
+
let(:query) { 'fail=jinkies' }
|
108
|
+
|
109
|
+
it 'handles the exception using ExceptionHandlingMiddleware' do
|
110
|
+
with_api(ExceptionHandlingAPI) do
|
111
|
+
get_request({ query: query }, err) do |c|
|
112
|
+
c.response_header.status.should == 200
|
113
|
+
c.response.should == 'Exception raised: jinkies'
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when param parsing raises an exception' do
|
120
|
+
let(:query) { 'ambiguous[]=&ambiguous[4]=' }
|
121
|
+
|
122
|
+
it 'returns a validation error generated by Goliath::Rack::Params' do
|
123
|
+
with_api(ExceptionHandlingAPI) do
|
124
|
+
get_request({ query: query }, err) do |c|
|
125
|
+
c.response_header.status.should == 400
|
126
|
+
c.response.should == "[:error, \"Invalid parameters: Rack::Utils::ParameterTypeError\"]"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# This API has the same setup as ExceptionHandlingAPI, except for the crucial
|
134
|
+
# difference that it does *not* use ExceptionHandlingMiddleware. This is to
|
135
|
+
# make sure that exceptions are still rescued somewhere in an API call, even
|
136
|
+
# though they aren't getting swallowed by Goliath::Rack::Params anymore.
|
137
|
+
|
138
|
+
class PassiveExceptionHandlingAPI < Goliath::API
|
139
|
+
use Goliath::Rack::Params
|
140
|
+
use ExceptionRaisingMiddleware
|
141
|
+
|
142
|
+
def response(env)
|
143
|
+
fail env.params['fail'] if env.params['fail']
|
144
|
+
[200, {}, 'No exceptions raised']
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe PassiveExceptionHandlingAPI do
|
149
|
+
let(:err) { Proc.new { fail 'API request failed' } }
|
150
|
+
|
151
|
+
context 'when no exceptions get raised' do
|
152
|
+
let(:query) { '' }
|
153
|
+
|
154
|
+
it 'returns a normal response' do
|
155
|
+
with_api(PassiveExceptionHandlingAPI) do
|
156
|
+
get_request({ query: query }, err) do |c|
|
157
|
+
c.response_header.status.should == 200
|
158
|
+
c.response.should == 'No exceptions raised'
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
context 'when a middleware raises an exception' do
|
165
|
+
let(:query) { 'raise=ruh-roh' }
|
166
|
+
|
167
|
+
it 'returns the server error generated by Goliath::Request#process' do
|
168
|
+
with_api(PassiveExceptionHandlingAPI) do
|
169
|
+
get_request({ query: query }, err) do |c|
|
170
|
+
c.response_header.status.should == 500
|
171
|
+
c.response.should == 'ruh-roh'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'when the API raises an exception' do
|
178
|
+
let(:query) { 'fail=puppy-power' }
|
179
|
+
|
180
|
+
it 'returns the validation error generated by Goliath::API#call' do
|
181
|
+
with_api(PassiveExceptionHandlingAPI) do
|
182
|
+
get_request({ query: query }, err) do |c|
|
183
|
+
c.response_header.status.should == 500
|
184
|
+
c.response.should == '[:error, "puppy-power"]'
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'when param parsing raises an exception' do
|
191
|
+
let(:query) { 'ambiguous[]=&ambiguous[4]=' }
|
192
|
+
|
193
|
+
it 'returns a validation error generated by Goliath::Rack::Params' do
|
194
|
+
with_api(PassiveExceptionHandlingAPI) do
|
195
|
+
get_request({ query: query }, err) do |c|
|
196
|
+
c.response_header.status.should == 400
|
197
|
+
c.response.should == '[:error, "Invalid parameters: Rack::Utils::ParameterTypeError"]'
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|