goliath 0.9.1 → 0.9.2

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.

Files changed (94) hide show
  1. data/.gitignore +1 -0
  2. data/HISTORY +50 -0
  3. data/README.md +2 -0
  4. data/examples/activerecord/srv.rb +1 -3
  5. data/examples/async_aroundware_demo.rb +81 -0
  6. data/examples/async_upload.rb +1 -2
  7. data/examples/auth_and_rate_limit.rb +143 -0
  8. data/examples/chunked_streaming.rb +37 -0
  9. data/examples/conf_test.rb +1 -3
  10. data/examples/config/auth_and_rate_limit.rb +30 -0
  11. data/examples/config/template.rb +8 -0
  12. data/examples/content_stream.rb +1 -3
  13. data/examples/echo.rb +8 -6
  14. data/examples/env_use_statements.rb +17 -0
  15. data/examples/gziped.rb +1 -3
  16. data/examples/public/stylesheets/style.css +296 -0
  17. data/examples/rack_routes.rb +65 -3
  18. data/examples/rasterize/rasterize.js +15 -0
  19. data/examples/rasterize/rasterize.rb +36 -0
  20. data/examples/rasterize/rasterize_and_shorten.rb +37 -0
  21. data/examples/stream.rb +2 -2
  22. data/examples/template.rb +48 -0
  23. data/examples/test_rig.rb +125 -0
  24. data/examples/valid.rb +4 -2
  25. data/examples/views/debug.haml +4 -0
  26. data/examples/views/joke.markdown +13 -0
  27. data/examples/views/layout.erb +12 -0
  28. data/examples/views/layout.haml +39 -0
  29. data/examples/views/root.haml +28 -0
  30. data/goliath.gemspec +10 -3
  31. data/lib/goliath.rb +0 -36
  32. data/lib/goliath/api.rb +137 -26
  33. data/lib/goliath/application.rb +71 -21
  34. data/lib/goliath/connection.rb +4 -2
  35. data/lib/goliath/constants.rb +1 -0
  36. data/lib/goliath/env.rb +40 -1
  37. data/lib/goliath/goliath.rb +30 -15
  38. data/lib/goliath/headers.rb +2 -2
  39. data/lib/goliath/plugins/latency.rb +8 -2
  40. data/lib/goliath/rack.rb +18 -0
  41. data/lib/goliath/rack/async_aroundware.rb +56 -0
  42. data/lib/goliath/rack/async_middleware.rb +93 -0
  43. data/lib/goliath/rack/builder.rb +42 -0
  44. data/lib/goliath/rack/default_response_format.rb +3 -15
  45. data/lib/goliath/rack/formatters.rb +11 -0
  46. data/lib/goliath/rack/formatters/html.rb +2 -18
  47. data/lib/goliath/rack/formatters/json.rb +2 -17
  48. data/lib/goliath/rack/formatters/plist.rb +32 -0
  49. data/lib/goliath/rack/formatters/xml.rb +23 -31
  50. data/lib/goliath/rack/formatters/yaml.rb +27 -0
  51. data/lib/goliath/rack/jsonp.rb +1 -13
  52. data/lib/goliath/rack/params.rb +55 -27
  53. data/lib/goliath/rack/render.rb +13 -22
  54. data/lib/goliath/rack/templates.rb +357 -0
  55. data/lib/goliath/rack/tracer.rb +11 -12
  56. data/lib/goliath/rack/validation.rb +12 -0
  57. data/lib/goliath/rack/validation/default_params.rb +0 -2
  58. data/lib/goliath/rack/validation/numeric_range.rb +11 -2
  59. data/lib/goliath/rack/validation/request_method.rb +3 -2
  60. data/lib/goliath/rack/validation/required_param.rb +13 -11
  61. data/lib/goliath/rack/validation/required_value.rb +11 -15
  62. data/lib/goliath/rack/validator.rb +51 -0
  63. data/lib/goliath/request.rb +34 -20
  64. data/lib/goliath/response.rb +3 -2
  65. data/lib/goliath/runner.rb +5 -11
  66. data/lib/goliath/server.rb +2 -1
  67. data/lib/goliath/synchrony/mongo_receiver.rb +64 -0
  68. data/lib/goliath/synchrony/response_receiver.rb +64 -0
  69. data/lib/goliath/test_helper.rb +39 -21
  70. data/lib/goliath/validation.rb +2 -0
  71. data/lib/goliath/{rack/validation_error.rb → validation/error.rb} +0 -16
  72. data/lib/goliath/validation/standard_http_errors.rb +31 -0
  73. data/lib/goliath/version.rb +1 -1
  74. data/spec/integration/http_log_spec.rb +16 -16
  75. data/spec/integration/rack_routes_spec.rb +144 -0
  76. data/spec/integration/reloader_spec.rb +4 -4
  77. data/spec/integration/template_spec.rb +54 -0
  78. data/spec/integration/trace_spec.rb +23 -0
  79. data/spec/integration/valid_spec.rb +21 -0
  80. data/spec/spec_helper.rb +3 -1
  81. data/spec/unit/api_spec.rb +30 -0
  82. data/spec/unit/rack/builder_spec.rb +40 -0
  83. data/spec/unit/rack/formatters/plist_spec.rb +51 -0
  84. data/spec/unit/rack/formatters/yaml_spec.rb +53 -0
  85. data/spec/unit/rack/params_spec.rb +22 -0
  86. data/spec/unit/rack/render_spec.rb +10 -5
  87. data/spec/unit/rack/validation/numeric_range_spec.rb +8 -1
  88. data/spec/unit/rack/validation/request_method_spec.rb +8 -8
  89. data/spec/unit/rack/validation/required_param_spec.rb +19 -15
  90. data/spec/unit/rack/validation/required_value_spec.rb +10 -13
  91. data/spec/unit/server_spec.rb +4 -4
  92. data/spec/unit/validation/standard_http_errors_spec.rb +21 -0
  93. metadata +177 -35
  94. data/spec/unit/rack/validation_error_spec.rb +0 -40
@@ -1,3 +1,20 @@
1
+ require 'goliath/goliath'
2
+ require 'goliath/runner'
3
+ require 'goliath/rack'
4
+
5
+ # Pre-load the goliath environment so it's available as we try to parse the class.
6
+ # This means we can use Goliath.dev? or Goliath.prod? in the use statements.
7
+ #
8
+ # Note, as implmented, you have to have -e as it's own flag, you can't do -sve dev
9
+ # as it won't pickup the e flag.
10
+ env = ENV['RACK_ENV']
11
+ env ||= begin
12
+ if ((i = ARGV.index('-e')) || (i = ARGV.index('--environment')))
13
+ ARGV[i + 1]
14
+ end
15
+ end
16
+ Goliath.env = env if env
17
+
1
18
  module Goliath
2
19
  # The main execution class for Goliath. This will execute in the at_exit
3
20
  # handler to run the server.
@@ -6,9 +23,10 @@ module Goliath
6
23
  class Application
7
24
  # Most of this stuff is straight out of sinatra.
8
25
 
9
- # Set of caller regex's to be skippe when looking for our API file
26
+ # Set of caller regex's to be skipped when looking for our API file
10
27
  CALLERS_TO_IGNORE = [ # :nodoc:
11
- /\/goliath(\/(application))?\.rb$/, # all goliath code
28
+ /\/goliath(\/application)?\.rb$/, # all goliath code
29
+ /\/goliath(\/(rack|validation|plugins)\/)/, # all goliath code
12
30
  /rubygems\/custom_require\.rb$/, # rubygems require hacks
13
31
  /bundler(\/runtime)?\.rb/, # bundler require hacks
14
32
  /<internal:/ # internal in ruby >= 1.9.2
@@ -40,34 +58,66 @@ module Goliath
40
58
  c
41
59
  end
42
60
 
61
+ # Returns the userland class which inherits the Goliath API
62
+ #
63
+ # @return [String] The app class
64
+ def self.app_class
65
+ @app_class
66
+ end
67
+
68
+ # Sets the userland class that should use the Goliath API
69
+ #
70
+ # @param app_class [String|Symbol|Constant] The new app class
71
+ # @return [String] app_class The new app class
72
+ def self.app_class=(app_class)
73
+ @app_class = app_class.to_s
74
+ end
75
+
76
+ # Retrive the base directory for the API before we've changed directories
77
+ #
78
+ # @note Note sure of a better way to handle this. Goliath will do a chdir
79
+ # when the runner is executed. If you need the +root_path+ before
80
+ # the runner is executing (like, in a use statement) you need this method.
81
+ #
82
+ # @param args [Array] Any arguments to append to the path
83
+ # @return [String] path for the given arguments
84
+ def self.app_path(*args)
85
+ @app_path ||= File.expand_path(File.dirname(app_file))
86
+ File.join(@app_path, *args)
87
+ end
88
+
89
+ # Retrieve the base directory for the API
90
+ #
91
+ # @param args [Array] Any arguments to append to the path
92
+ # @return [String] path for the given arguments
93
+ def self.root_path(*args)
94
+ return app_path(args) if Goliath.test?
95
+
96
+ @root_path ||= File.expand_path("./")
97
+ File.join(@root_path, *args)
98
+ end
99
+
43
100
  # Execute the application
44
101
  #
45
102
  # @return [Nil]
46
103
  def self.run!
47
- file = File.basename(app_file, '.rb')
48
- klass = begin
49
- Kernel.const_get(camel_case(file))
104
+ unless @app_class
105
+ file = File.basename(app_file, '.rb')
106
+ @app_class = camel_case(file)
107
+ end
108
+
109
+ begin
110
+ klass = Kernel
111
+ @app_class.split('::').each do |con|
112
+ klass = klass.const_get(con)
113
+ end
50
114
  rescue NameError
51
- raise NameError, "Class #{camel_case(file)} not found."
115
+ raise NameError, "Class #{@app_class} not found."
52
116
  end
53
117
  api = klass.new
54
118
 
55
119
  runner = Goliath::Runner.new(ARGV, api)
56
- runner.load_app do
57
- klass.middlewares.each do |mw|
58
- use(*(mw[0..1].compact), &mw[2])
59
- end
60
-
61
- # If you use map you can't use run as
62
- # the rack builder will blowup.
63
- if klass.maps.empty?
64
- run api
65
- else
66
- klass.maps.each do |mp|
67
- map(mp.first, &mp.last)
68
- end
69
- end
70
- end
120
+ runner.app = Goliath::Rack::Builder.build(klass, api)
71
121
 
72
122
  runner.load_plugins(klass.plugins)
73
123
  runner.run
@@ -1,5 +1,7 @@
1
1
  require 'http/parser'
2
2
  require 'goliath/env'
3
+ require 'goliath/constants'
4
+ require 'goliath/request'
3
5
 
4
6
  module Goliath
5
7
  # The Goliath::Connection class handles sending and receiving data
@@ -12,7 +14,7 @@ module Goliath
12
14
  attr_accessor :app, :api, :port, :logger, :status, :config, :options
13
15
  attr_reader :parser
14
16
 
15
- AsyncResponse = [-1, {}, []].freeze
17
+ AsyncResponse = [-1, {}, []]
16
18
 
17
19
  def post_init
18
20
  @current = nil
@@ -76,7 +78,7 @@ module Goliath
76
78
  if req = @pending.shift
77
79
  @current = req
78
80
  @current.succeed
79
- else
81
+ elsif @current
80
82
  @current.close
81
83
  @current = nil
82
84
  end
@@ -22,6 +22,7 @@ module Goliath
22
22
  RACK_RUN_ONCE = 'rack.run_once'
23
23
  RACK_VERSION_NUM = [1, 0]
24
24
  RACK_LOGGER = 'rack.logger'
25
+ RACK_EXCEPTION = 'rack.exception'
25
26
 
26
27
  ASYNC_CALLBACK = 'async.callback'
27
28
  ASYNC_HEADERS = 'async.headers'
@@ -47,7 +47,7 @@ module Goliath
47
47
  # @example
48
48
  # [200, {}, {:meta => {:trace => env.trace_stats}}, {}]
49
49
  #
50
- # @return [Array] Array of [name, time] pairs with a Total entry added.d
50
+ # @return [Array] Array of [name, time] pairs with a Total entry added.
51
51
  def trace_stats
52
52
  self[:trace] + [['total', self[:trace].collect { |s| s[1].to_f }.inject(:+).to_s]]
53
53
  end
@@ -67,6 +67,45 @@ module Goliath
67
67
  self[STREAM_CLOSE].call
68
68
  end
69
69
 
70
+ # Sends a chunk in a Chunked transfer encoding stream.
71
+ #
72
+ # Each chunk starts with the number of octets of the data it embeds expressed
73
+ # in hexadecimal followed by optional parameters (chunk extension) and a
74
+ # terminating CRLF (carriage return and line feed) sequence, followed by the
75
+ # chunk data. The chunk is terminated by CRLF. If chunk extensions are
76
+ # provided, the chunk size is terminated by a semicolon followed with the
77
+ # extension name and an optional equal sign and value
78
+ #
79
+ # Note: chunk extensions aren't provided yet
80
+ #
81
+ # This will do nothing if the chunk is empty -- sending a zero-length chunk
82
+ # signals the end of a stream.
83
+ #
84
+ def chunked_stream_send(chunk)
85
+ return if chunk.empty?
86
+ chunk_len_in_hex = chunk.bytesize.to_s(16)
87
+ body = [chunk_len_in_hex, "\r\n", chunk, "\r\n"].join
88
+ stream_send(body)
89
+ end
90
+
91
+ # Sends the terminating chunk in a chunked transfer encoding stream, and
92
+ # closes the stream.
93
+ #
94
+ # The last chunk is a zero-length chunk, with the chunk size coded as 0, but
95
+ # without any chunk data section. The final chunk may be followed by an
96
+ # optional trailer of additional entity header fields that are normally
97
+ # delivered in the HTTP header to allow the delivery of data that can only
98
+ # be computed after all chunk data has been generated. The sender may
99
+ # indicate in a Trailer header field which additional fields it will send
100
+ # in the trailer after the chunks.
101
+ #
102
+ # Note: trailer headers aren't provided yet
103
+ #
104
+ def chunked_stream_close
105
+ stream_send([0, "\r\n", "\r\n"].join)
106
+ stream_close
107
+ end
108
+
70
109
  # Convenience method for accessing the rack.logger item in the environment.
71
110
  #
72
111
  # @return [Logger] The logger object
@@ -1,49 +1,64 @@
1
1
  require 'eventmachine'
2
2
  require 'http/parser'
3
3
  require 'async_rack'
4
+ require 'goliath/constants'
5
+ require 'goliath/version'
4
6
 
5
7
  # The Goliath Framework
6
8
  module Goliath
7
9
  module_function
8
10
 
9
- @env = :development
11
+ ENVIRONMENTS = [:development, :production, :test, :staging]
10
12
 
11
13
  # Retrieves the current goliath environment
12
14
  #
13
15
  # @return [String] the current environment
14
16
  def env
15
- @env
17
+ @env or fail "environment has not been set"
16
18
  end
17
19
 
18
20
  # Sets the current goliath environment
19
21
  #
20
- # @param [String] env the environment string of [dev|prod|test]
21
- def env=(env)
22
- case(env.to_s)
23
- when 'dev' then @env = :development
24
- when 'prod' then @env = :production
25
- when 'test' then @env = :test
22
+ # @param [String|Symbol] env the environment symbol of [dev | development | prod | production | test]
23
+ def env=(e)
24
+ es = case(e.to_sym)
25
+ when :dev then :development
26
+ when :prod then :production
27
+ else e.to_sym
28
+ end
29
+
30
+ if ENVIRONMENTS.include?(es)
31
+ @env = es
32
+ else
33
+ fail "did not recognize environment: #{e}, expected one of: #{ENVIRONMENTS.join(', ')}"
26
34
  end
27
35
  end
28
36
 
29
37
  # Determines if we are in the production environment
30
38
  #
31
- # @return [Boolean] true if current environemnt is production, false otherwise
39
+ # @return [Boolean] true if current environment is production, false otherwise
32
40
  def prod?
33
- @env == :production
41
+ env == :production
34
42
  end
35
43
 
36
44
  # Determines if we are in the development environment
37
45
  #
38
- # @return [Boolean] true if current environemnt is development, false otherwise
46
+ # @return [Boolean] true if current environment is development, false otherwise
39
47
  def dev?
40
- @env == :development
48
+ env == :development
41
49
  end
42
50
 
43
51
  # Determines if we are in the test environment
44
52
  #
45
- # @return [Boolean] true if current environemnt is test, false otherwise
53
+ # @return [Boolean] true if current environment is test, false otherwise
46
54
  def test?
47
- @env == :test
55
+ env == :test
56
+ end
57
+
58
+ # Determines if we are in the staging environment
59
+ #
60
+ # @return [Boolean] true if current environment is staging.
61
+ def staging?
62
+ env == :staging
48
63
  end
49
- end
64
+ end
@@ -1,8 +1,8 @@
1
1
  module Goliath
2
2
  # @private
3
3
  class Headers
4
- HEADER_FORMAT = "%s: %s\r\n".freeze
5
- ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze
4
+ HEADER_FORMAT = "%s: %s\r\n"
5
+ ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate)
6
6
 
7
7
  def initialize
8
8
  @sent = {}
@@ -21,13 +21,19 @@ module Goliath
21
21
  @last = Time.now.to_f
22
22
  end
23
23
 
24
+ @@recent_latency = 0
25
+ def self.recent_latency
26
+ @@recent_latency
27
+ end
28
+
24
29
  # Called automatically to start the plugin
25
30
  def run
26
31
  EM.add_periodic_timer(1) do
27
- @logger.info "LATENCY: #{Time.now.to_f - @last}"
32
+ @@recent_latency = (Time.now.to_f - @last)
33
+ @logger.info "LATENCY: #{@@recent_latency}"
28
34
  @last = Time.now.to_f
29
35
  end
30
36
  end
31
37
  end
32
38
  end
33
- end
39
+ end
@@ -0,0 +1,18 @@
1
+ module Goliath
2
+ module Rack
3
+ autoload :AsyncAroundware, 'goliath/rack/async_aroundware'
4
+ autoload :AsyncMiddleware, 'goliath/rack/async_middleware'
5
+ autoload :Builder, 'goliath/rack/builder'
6
+ autoload :DefaultMimeType, 'goliath/rack/default_mime_type'
7
+ autoload :DefaultResponseFormat, 'goliath/rack/default_response_format'
8
+ autoload :Formatters, 'goliath/rack/formatters'
9
+ autoload :Heartbeat, 'goliath/rack/heartbeat'
10
+ autoload :JSONP, 'goliath/rack/jsonp'
11
+ autoload :Params, 'goliath/rack/params'
12
+ autoload :Render, 'goliath/rack/render'
13
+ autoload :Templates, 'goliath/rack/templates'
14
+ autoload :Tracer, 'goliath/rack/tracer'
15
+ autoload :Validator, 'goliath/rack/validator'
16
+ autoload :Validation, 'goliath/rack/validation'
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ module Goliath
2
+ module Rack
3
+ class AsyncAroundware
4
+ # Create a new AsyncAroundware
5
+ #
6
+ # @example
7
+ # class MyResponseReceiver < Goliath::Rack::MultiReceiver
8
+ # # ... define pre_process and post_process ...
9
+ # end
10
+ #
11
+ # class AsyncAroundwareDemoMulti < Goliath::API
12
+ # use Goliath::Rack::AsyncAroundware, MyResponseReceiver
13
+ # # ... stuff ...
14
+ # end
15
+ #
16
+ # @param app [#call] the downstream app
17
+ # @param response_receiver_klass a class that quacks like a
18
+ # Goliath::Rack::ResponseReceiver and an EM::Deferrable
19
+ # @param *args [Array] extra args to pass to the response_receiver
20
+ def initialize app, response_receiver_klass, *args
21
+ @app = app
22
+ @response_receiver_klass = response_receiver_klass
23
+ @response_receiver_args = args
24
+ end
25
+
26
+ #
27
+ def call(env)
28
+ response_receiver = new_response_receiver(env)
29
+
30
+ # put response_receiver in the middle of the async_callback chain:
31
+ # * save the old callback chain;
32
+ # * put the response_receiver in as the new async_callback;
33
+ # * when the response_receiver completes, invoke the old callback chain
34
+ async_callback = env['async.callback']
35
+ env['async.callback'] = response_receiver
36
+ response_receiver.callback{ do_postprocess(env, async_callback, response_receiver) }
37
+ response_receiver.errback{ do_postprocess(env, async_callback, response_receiver) }
38
+
39
+ response_receiver.pre_process
40
+
41
+ response_receiver.call(@app.call(env))
42
+ end
43
+
44
+ def new_response_receiver(env)
45
+ @response_receiver_klass.new(env, *@response_receiver_args)
46
+ end
47
+
48
+ include Goliath::Rack::Validator
49
+ def do_postprocess(env, async_callback, response_receiver)
50
+ Goliath::Rack::Validator.safely(env) do
51
+ async_callback.call(response_receiver.post_process)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,93 @@
1
+ module Goliath
2
+ module Rack
3
+ #
4
+ # Include this to enable middleware that can perform post-processing.
5
+ #
6
+ # For internal reasons, you can't do the following as you would in Rack:
7
+ #
8
+ # def call(env)
9
+ # # ... do pre-processing
10
+ # status, headers, body = @app.call(env)
11
+ # new_body = make_totally_awesome(body) ## !! BROKEN !!
12
+ # [status, headers, new_body]
13
+ # end
14
+ #
15
+ # By including this middleware, you can do that kind of "around" middleware:
16
+ # it lets goliath proceed asynchronously, but still "unwind" the request by
17
+ # walking up the callback chain.
18
+ #
19
+ # @example
20
+ # class AwesomeMiddleware
21
+ # include Goliath::Rack::AsyncMiddleware
22
+ #
23
+ # def call(env)
24
+ # awesomness_quotient = 3
25
+ # # the extra args sent to super are passed along to post_process
26
+ # super(env, awesomness_quotient)
27
+ # end
28
+ #
29
+ # def post_process(env, status, headers, body, awesomness_quotient)
30
+ # new_body = make_totally_awesome(body, awesomness_quotient)
31
+ # [status, headers, new_body]
32
+ # end
33
+ # end
34
+ #
35
+ # @note Some caveats on writing middleware. Unlike other Rack powered app
36
+ # servers, Goliath creates a single instance of the middleware chain at
37
+ # startup, and reuses it for all incoming requests. Since everything is
38
+ # asynchronous, you can have multiple requests using the middleware chain
39
+ # at the same time. If your middleware tries to store any instance or
40
+ # class level variables they'll end up getting stomped all over by the
41
+ # next request. Everything that you need to store needs to be stored in
42
+ # local variables.
43
+ module AsyncMiddleware
44
+ # Called by the framework to create the middleware.
45
+ #
46
+ # @param app [Proc] The application
47
+ # @return [Goliath::Rack::AsyncMiddleware]
48
+ def initialize(app)
49
+ @app = app
50
+ end
51
+
52
+ # Store the previous async.callback into async_cb and redefines it to be
53
+ # our own. When the asynchronous response is done, Goliath can "unwind"
54
+ # the request by walking up the callback chain.
55
+ #
56
+ # However, you will notice that we execute the post_process method in the
57
+ # default return case. If the validations fail later in the middleware
58
+ # chain before your classes response(env) method is executed, the response
59
+ # will come back up through the chain normally and be returned.
60
+ #
61
+ # To do preprocessing, override this method in your subclass and invoke
62
+ # super(env) as the last line. Any extra arguments will be made available
63
+ # to the post_process method.
64
+ #
65
+ # @param env [Goliath::Env] The goliath environment
66
+ # @return [Array] The [status_code, headers, body] tuple
67
+ def call(env, *args)
68
+ async_cb = env['async.callback']
69
+
70
+ env['async.callback'] = Proc.new do |status, headers, body|
71
+ async_cb.call(post_process(env, status, headers, body, *args))
72
+ end
73
+
74
+ status, headers, body = @app.call(env)
75
+
76
+ if status == Goliath::Connection::AsyncResponse.first
77
+ [status, headers, body]
78
+ else
79
+ post_process(env, status, headers, body, *args)
80
+ end
81
+ end
82
+
83
+ # Override this method in your middleware to perform any
84
+ # postprocessing. Note that this can be called in the asynchronous case
85
+ # (walking back up the middleware async.callback chain), or synchronously
86
+ # (in the case of a validation error, or if a downstream middleware
87
+ # supplied a direct response).
88
+ def post_process(env, status, headers, body)
89
+ [status, headers, body]
90
+ end
91
+ end
92
+ end
93
+ end