rack 1.0.1 → 2.2.21

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 (147) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +829 -0
  3. data/CONTRIBUTING.md +136 -0
  4. data/{COPYING → MIT-LICENSE} +4 -2
  5. data/README.rdoc +355 -0
  6. data/Rakefile +64 -98
  7. data/SPEC.rdoc +292 -0
  8. data/bin/rackup +3 -174
  9. data/contrib/rack.png +0 -0
  10. data/contrib/rack.svg +150 -0
  11. data/contrib/rack_logo.svg +164 -111
  12. data/contrib/rdoc.css +412 -0
  13. data/example/lobster.ru +2 -0
  14. data/example/protectedlobster.rb +4 -2
  15. data/example/protectedlobster.ru +3 -1
  16. data/lib/rack/auth/abstract/handler.rb +7 -5
  17. data/lib/rack/auth/abstract/request.rb +12 -2
  18. data/lib/rack/auth/basic.rb +6 -4
  19. data/lib/rack/auth/digest/md5.rb +20 -13
  20. data/lib/rack/auth/digest/nonce.rb +6 -4
  21. data/lib/rack/auth/digest/params.rb +11 -12
  22. data/lib/rack/auth/digest/request.rb +15 -12
  23. data/lib/rack/body_proxy.rb +45 -0
  24. data/lib/rack/builder.rb +213 -19
  25. data/lib/rack/cascade.rb +50 -18
  26. data/lib/rack/chunked.rb +95 -27
  27. data/lib/rack/common_logger.rb +84 -0
  28. data/lib/rack/conditional_get.rb +83 -0
  29. data/lib/rack/config.rb +22 -0
  30. data/lib/rack/content_length.rb +19 -10
  31. data/lib/rack/content_type.rb +12 -5
  32. data/lib/rack/core_ext/regexp.rb +14 -0
  33. data/lib/rack/deflater.rb +90 -42
  34. data/lib/rack/directory.rb +118 -72
  35. data/lib/rack/etag.rb +77 -0
  36. data/lib/rack/events.rb +153 -0
  37. data/lib/rack/file.rb +4 -85
  38. data/lib/rack/files.rb +218 -0
  39. data/lib/rack/handler/cgi.rb +22 -24
  40. data/lib/rack/handler/fastcgi.rb +34 -24
  41. data/lib/rack/handler/lsws.rb +16 -15
  42. data/lib/rack/handler/scgi.rb +38 -29
  43. data/lib/rack/handler/thin.rb +23 -7
  44. data/lib/rack/handler/webrick.rb +95 -37
  45. data/lib/rack/handler.rb +49 -14
  46. data/lib/rack/head.rb +19 -13
  47. data/lib/rack/lint.rb +466 -217
  48. data/lib/rack/lobster.rb +19 -14
  49. data/lib/rack/lock.rb +24 -8
  50. data/lib/rack/logger.rb +20 -0
  51. data/lib/rack/media_type.rb +53 -0
  52. data/lib/rack/method_override.rb +52 -0
  53. data/lib/rack/mime.rb +652 -171
  54. data/lib/rack/mock.rb +203 -64
  55. data/lib/rack/multipart/generator.rb +97 -0
  56. data/lib/rack/multipart/parser.rb +440 -0
  57. data/lib/rack/multipart/uploaded_file.rb +41 -0
  58. data/lib/rack/multipart.rb +64 -0
  59. data/lib/rack/null_logger.rb +39 -0
  60. data/lib/rack/query_parser.rb +266 -0
  61. data/lib/rack/recursive.rb +18 -11
  62. data/lib/rack/reloader.rb +13 -5
  63. data/lib/rack/request.rb +607 -191
  64. data/lib/rack/response.rb +245 -110
  65. data/lib/rack/rewindable_input.rb +15 -21
  66. data/lib/rack/runtime.rb +34 -0
  67. data/lib/rack/sendfile.rb +192 -0
  68. data/lib/rack/server.rb +466 -0
  69. data/lib/rack/session/abstract/id.rb +449 -68
  70. data/lib/rack/session/cookie.rb +159 -47
  71. data/lib/rack/session/memcache.rb +4 -103
  72. data/lib/rack/session/pool.rb +39 -49
  73. data/lib/rack/show_exceptions.rb +390 -0
  74. data/lib/rack/{showstatus.rb → show_status.rb} +21 -14
  75. data/lib/rack/static.rb +164 -14
  76. data/lib/rack/tempfile_reaper.rb +22 -0
  77. data/lib/rack/urlmap.rb +60 -18
  78. data/lib/rack/utils.rb +508 -277
  79. data/lib/rack/version.rb +29 -0
  80. data/lib/rack.rb +85 -34
  81. data/rack.gemspec +43 -51
  82. metadata +119 -207
  83. data/KNOWN-ISSUES +0 -18
  84. data/RDOX +0 -428
  85. data/README +0 -364
  86. data/SPEC +0 -164
  87. data/lib/rack/adapter/camping.rb +0 -22
  88. data/lib/rack/auth/openid.rb +0 -480
  89. data/lib/rack/commonlogger.rb +0 -61
  90. data/lib/rack/conditionalget.rb +0 -47
  91. data/lib/rack/handler/evented_mongrel.rb +0 -8
  92. data/lib/rack/handler/mongrel.rb +0 -87
  93. data/lib/rack/handler/swiftiplied_mongrel.rb +0 -8
  94. data/lib/rack/methodoverride.rb +0 -27
  95. data/lib/rack/showexceptions.rb +0 -349
  96. data/test/cgi/lighttpd.conf +0 -20
  97. data/test/cgi/test +0 -9
  98. data/test/cgi/test.fcgi +0 -8
  99. data/test/cgi/test.ru +0 -7
  100. data/test/multipart/binary +0 -0
  101. data/test/multipart/empty +0 -10
  102. data/test/multipart/ie +0 -6
  103. data/test/multipart/nested +0 -10
  104. data/test/multipart/none +0 -9
  105. data/test/multipart/semicolon +0 -6
  106. data/test/multipart/text +0 -10
  107. data/test/spec_rack_auth_basic.rb +0 -73
  108. data/test/spec_rack_auth_digest.rb +0 -226
  109. data/test/spec_rack_auth_openid.rb +0 -84
  110. data/test/spec_rack_builder.rb +0 -84
  111. data/test/spec_rack_camping.rb +0 -51
  112. data/test/spec_rack_cascade.rb +0 -50
  113. data/test/spec_rack_cgi.rb +0 -89
  114. data/test/spec_rack_chunked.rb +0 -62
  115. data/test/spec_rack_commonlogger.rb +0 -32
  116. data/test/spec_rack_conditionalget.rb +0 -41
  117. data/test/spec_rack_content_length.rb +0 -43
  118. data/test/spec_rack_content_type.rb +0 -30
  119. data/test/spec_rack_deflater.rb +0 -127
  120. data/test/spec_rack_directory.rb +0 -61
  121. data/test/spec_rack_fastcgi.rb +0 -89
  122. data/test/spec_rack_file.rb +0 -75
  123. data/test/spec_rack_handler.rb +0 -43
  124. data/test/spec_rack_head.rb +0 -30
  125. data/test/spec_rack_lint.rb +0 -521
  126. data/test/spec_rack_lobster.rb +0 -45
  127. data/test/spec_rack_lock.rb +0 -38
  128. data/test/spec_rack_methodoverride.rb +0 -60
  129. data/test/spec_rack_mock.rb +0 -157
  130. data/test/spec_rack_mongrel.rb +0 -189
  131. data/test/spec_rack_recursive.rb +0 -77
  132. data/test/spec_rack_request.rb +0 -504
  133. data/test/spec_rack_response.rb +0 -218
  134. data/test/spec_rack_rewindable_input.rb +0 -118
  135. data/test/spec_rack_session_cookie.rb +0 -82
  136. data/test/spec_rack_session_memcache.rb +0 -240
  137. data/test/spec_rack_session_pool.rb +0 -172
  138. data/test/spec_rack_showexceptions.rb +0 -21
  139. data/test/spec_rack_showstatus.rb +0 -72
  140. data/test/spec_rack_static.rb +0 -37
  141. data/test/spec_rack_thin.rb +0 -91
  142. data/test/spec_rack_urlmap.rb +0 -185
  143. data/test/spec_rack_utils.rb +0 -387
  144. data/test/spec_rack_webrick.rb +0 -130
  145. data/test/testrequest.rb +0 -57
  146. data/test/unregistered_handler/rack/handler/unregistered.rb +0 -7
  147. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +0 -7
data/lib/rack/lint.rb CHANGED
@@ -1,4 +1,6 @@
1
- require 'rack/utils'
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
2
4
 
3
5
  module Rack
4
6
  # Rack::Lint validates your application and the requests and
@@ -7,14 +9,15 @@ module Rack
7
9
  class Lint
8
10
  def initialize(app)
9
11
  @app = app
12
+ @content_length = nil
10
13
  end
11
14
 
12
15
  # :stopdoc:
13
16
 
14
17
  class LintError < RuntimeError; end
15
18
  module Assertion
16
- def assert(message, &block)
17
- unless block.call
19
+ def assert(message)
20
+ unless yield
18
21
  raise LintError, message
19
22
  end
20
23
  end
@@ -29,40 +32,50 @@ module Rack
29
32
 
30
33
  ## = Rack applications
31
34
 
32
- ## A Rack application is an Ruby object (not a class) that
35
+ ## A Rack application is a Ruby object (not a class) that
33
36
  ## responds to +call+.
34
- def call(env=nil)
37
+ def call(env = nil)
35
38
  dup._call(env)
36
39
  end
37
40
 
38
41
  def _call(env)
39
42
  ## It takes exactly one argument, the *environment*
40
- assert("No env given") { env }
43
+ raise LintError, "No env given" unless env
41
44
  check_env env
42
45
 
43
- env['rack.input'] = InputWrapper.new(env['rack.input'])
44
- env['rack.errors'] = ErrorWrapper.new(env['rack.errors'])
46
+ env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT])
47
+ env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS])
45
48
 
46
49
  ## and returns an Array of exactly three values:
47
- status, headers, @body = @app.call(env)
50
+ ary = @app.call(env)
51
+ raise LintError, "response is not an Array, but #{ary.class}" unless ary.kind_of? Array
52
+ raise LintError, "response array has #{ary.size} elements instead of 3" unless ary.size == 3
53
+
54
+ status, headers, @body = ary
48
55
  ## The *status*,
49
56
  check_status status
50
57
  ## the *headers*,
51
58
  check_headers headers
59
+
60
+ hijack_proc = check_hijack_response headers, env
61
+ if hijack_proc && headers.is_a?(Hash)
62
+ headers[RACK_HIJACK] = hijack_proc
63
+ end
64
+
52
65
  ## and the *body*.
53
66
  check_content_type status, headers
54
- check_content_length status, headers, env
67
+ check_content_length status, headers
68
+ @head_request = env[REQUEST_METHOD] == HEAD
55
69
  [status, headers, self]
56
70
  end
57
71
 
58
72
  ## == The Environment
59
73
  def check_env(env)
60
- ## The environment must be an true instance of Hash (no
61
- ## subclassing allowed) that includes CGI-like headers.
62
- ## The application is free to modify the environment.
63
- assert("env #{env.inspect} is not a Hash, but #{env.class}") {
64
- env.instance_of? Hash
65
- }
74
+ ## The environment must be an unfrozen instance of Hash that includes
75
+ ## CGI-like headers. The application is free to modify the
76
+ ## environment.
77
+ raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
78
+ raise LintError, "env should not be frozen, but is" if env.frozen?
66
79
 
67
80
  ##
68
81
  ## The environment is required to include these variables
@@ -89,14 +102,26 @@ module Rack
89
102
  ## empty string, if the request URL targets
90
103
  ## the application root and does not have a
91
104
  ## trailing slash. This value may be
92
- ## percent-encoded when I originating from
105
+ ## percent-encoded when originating from
93
106
  ## a URL.
94
107
 
95
108
  ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
96
109
  ## follows the <tt>?</tt>, if any. May be
97
110
  ## empty, but is always required!
98
111
 
99
- ## <tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>:: When combined with <tt>SCRIPT_NAME</tt> and <tt>PATH_INFO</tt>, these variables can be used to complete the URL. Note, however, that <tt>HTTP_HOST</tt>, if present, should be used in preference to <tt>SERVER_NAME</tt> for reconstructing the request URL. <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt> can never be empty strings, and so are always required.
112
+ ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
113
+ ## <tt>PATH_INFO</tt>, these variables can be
114
+ ## used to complete the URL. Note, however,
115
+ ## that <tt>HTTP_HOST</tt>, if present,
116
+ ## should be used in preference to
117
+ ## <tt>SERVER_NAME</tt> for reconstructing
118
+ ## the request URL.
119
+ ## <tt>SERVER_NAME</tt> can never be an empty
120
+ ## string, and so is always required.
121
+
122
+ ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
123
+ ## server is running on. Should be specified if
124
+ ## the server is running on a non-standard port.
100
125
 
101
126
  ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
102
127
  ## client-supplied HTTP request
@@ -106,46 +131,131 @@ module Rack
106
131
  ## variables should correspond with
107
132
  ## the presence or absence of the
108
133
  ## appropriate HTTP header in the
109
- ## request.
134
+ ## request. See
135
+ ## {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
136
+ ## for specific behavior.
110
137
 
111
138
  ## In addition to this, the Rack environment must include these
112
139
  ## Rack-specific variables:
113
140
 
114
- ## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
115
- ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
141
+ ## <tt>rack.version</tt>:: The Array representing this version of Rack
142
+ ## See Rack::VERSION, that corresponds to
143
+ ## the version of this SPEC.
144
+
145
+ ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
146
+ ## request URL.
147
+
116
148
  ## <tt>rack.input</tt>:: See below, the input stream.
149
+
117
150
  ## <tt>rack.errors</tt>:: See below, the error stream.
118
- ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
119
- ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
120
- ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
121
- ##
151
+
152
+ ## <tt>rack.multithread</tt>:: true if the application object may be
153
+ ## simultaneously invoked by another thread
154
+ ## in the same process, false otherwise.
155
+
156
+ ## <tt>rack.multiprocess</tt>:: true if an equivalent application object
157
+ ## may be simultaneously invoked by another
158
+ ## process, false otherwise.
159
+
160
+ ## <tt>rack.run_once</tt>:: true if the server expects
161
+ ## (but does not guarantee!) that the
162
+ ## application will only be invoked this one
163
+ ## time during the life of its containing
164
+ ## process. Normally, this will only be true
165
+ ## for a server based on CGI
166
+ ## (or something similar).
167
+
168
+ ## <tt>rack.hijack?</tt>:: present and true if the server supports
169
+ ## connection hijacking. See below, hijacking.
170
+
171
+ ## <tt>rack.hijack</tt>:: an object responding to #call that must be
172
+ ## called at least once before using
173
+ ## rack.hijack_io.
174
+ ## It is recommended #call return rack.hijack_io
175
+ ## as well as setting it in env if necessary.
176
+
177
+ ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
178
+ ## has received #call, this will contain
179
+ ## an object resembling an IO. See hijacking.
122
180
 
123
181
  ## Additional environment specifications have approved to
124
182
  ## standardized middleware APIs. None of these are required to
125
183
  ## be implemented by the server.
126
184
 
127
- ## <tt>rack.session</tt>:: A hash like interface for storing request session data.
185
+ ## <tt>rack.session</tt>:: A hash like interface for storing
186
+ ## request session data.
128
187
  ## The store must implement:
129
- if session = env['rack.session']
188
+ if session = env[RACK_SESSION]
130
189
  ## store(key, value) (aliased as []=);
131
- assert("session #{session.inspect} must respond to store and []=") {
132
- session.respond_to?(:store) && session.respond_to?(:[]=)
133
- }
190
+ unless session.respond_to?(:store) && session.respond_to?(:[]=)
191
+ raise LintError, "session #{session.inspect} must respond to store and []="
192
+ end
134
193
 
135
194
  ## fetch(key, default = nil) (aliased as []);
136
- assert("session #{session.inspect} must respond to fetch and []") {
137
- session.respond_to?(:fetch) && session.respond_to?(:[])
138
- }
195
+ unless session.respond_to?(:fetch) && session.respond_to?(:[])
196
+ raise LintError, "session #{session.inspect} must respond to fetch and []"
197
+ end
139
198
 
140
199
  ## delete(key);
141
- assert("session #{session.inspect} must respond to delete") {
142
- session.respond_to?(:delete)
143
- }
200
+ unless session.respond_to?(:delete)
201
+ raise LintError, "session #{session.inspect} must respond to delete"
202
+ end
144
203
 
145
204
  ## clear;
146
- assert("session #{session.inspect} must respond to clear") {
147
- session.respond_to?(:clear)
148
- }
205
+ unless session.respond_to?(:clear)
206
+ raise LintError, "session #{session.inspect} must respond to clear"
207
+ end
208
+
209
+ ## to_hash (returning unfrozen Hash instance);
210
+ unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
211
+ raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
212
+ end
213
+ end
214
+
215
+ ## <tt>rack.logger</tt>:: A common object interface for logging messages.
216
+ ## The object must implement:
217
+ if logger = env[RACK_LOGGER]
218
+ ## info(message, &block)
219
+ unless logger.respond_to?(:info)
220
+ raise LintError, "logger #{logger.inspect} must respond to info"
221
+ end
222
+
223
+ ## debug(message, &block)
224
+ unless logger.respond_to?(:debug)
225
+ raise LintError, "logger #{logger.inspect} must respond to debug"
226
+ end
227
+
228
+ ## warn(message, &block)
229
+ unless logger.respond_to?(:warn)
230
+ raise LintError, "logger #{logger.inspect} must respond to warn"
231
+ end
232
+
233
+ ## error(message, &block)
234
+ unless logger.respond_to?(:error)
235
+ raise LintError, "logger #{logger.inspect} must respond to error"
236
+ end
237
+
238
+ ## fatal(message, &block)
239
+ unless logger.respond_to?(:fatal)
240
+ raise LintError, "logger #{logger.inspect} must respond to fatal"
241
+ end
242
+ end
243
+
244
+ ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
245
+ if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
246
+ unless bufsize.is_a?(Integer) && bufsize > 0
247
+ raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
248
+ end
249
+ end
250
+
251
+ ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
252
+ if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
253
+ raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
254
+ env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
255
+ io = tempfile_factory.call(filename, content_type)
256
+ raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
257
+ io
258
+ end
149
259
  end
150
260
 
151
261
  ## The server or the application can store their own data in the
@@ -155,79 +265,97 @@ module Rack
155
265
  ## accepted specifications and must not be used otherwise.
156
266
  ##
157
267
 
158
- %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
159
- QUERY_STRING
268
+ %w[REQUEST_METHOD SERVER_NAME QUERY_STRING
160
269
  rack.version rack.input rack.errors
161
270
  rack.multithread rack.multiprocess rack.run_once].each { |header|
162
- assert("env missing required key #{header}") { env.include? header }
271
+ raise LintError, "env missing required key #{header}" unless env.include? header
163
272
  }
164
273
 
274
+ ## The <tt>SERVER_PORT</tt> must be an Integer if set.
275
+ server_port = env["SERVER_PORT"]
276
+ unless server_port.nil? || (Integer(server_port) rescue false)
277
+ raise LintError, "env[SERVER_PORT] is not an Integer"
278
+ end
279
+
280
+ ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
281
+ unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
282
+ raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
283
+ end
284
+
285
+ ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
286
+ unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
287
+ raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
288
+ end
289
+
165
290
  ## The environment must not contain the keys
166
291
  ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
167
292
  ## (use the versions without <tt>HTTP_</tt>).
168
293
  %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
169
- assert("env contains #{header}, must use #{header[5,-1]}") {
170
- not env.include? header
171
- }
294
+ if env.include? header
295
+ raise LintError, "env contains #{header}, must use #{header[5, -1]}"
296
+ end
172
297
  }
173
298
 
174
299
  ## The CGI keys (named without a period) must have String values.
300
+ ## If the string values for CGI keys contain non-ASCII characters,
301
+ ## they should use ASCII-8BIT encoding.
175
302
  env.each { |key, value|
176
303
  next if key.include? "." # Skip extensions
177
- assert("env variable #{key} has non-string value #{value.inspect}") {
178
- value.instance_of? String
179
- }
304
+ unless value.kind_of? String
305
+ raise LintError, "env variable #{key} has non-string value #{value.inspect}"
306
+ end
307
+ next if value.encoding == Encoding::ASCII_8BIT
308
+ unless value.b !~ /[\x80-\xff]/n
309
+ raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
310
+ end
180
311
  }
181
312
 
182
- ##
183
313
  ## There are the following restrictions:
184
314
 
185
315
  ## * <tt>rack.version</tt> must be an array of Integers.
186
- assert("rack.version must be an Array, was #{env["rack.version"].class}") {
187
- env["rack.version"].instance_of? Array
188
- }
316
+ unless env[RACK_VERSION].kind_of? Array
317
+ raise LintError, "rack.version must be an Array, was #{env[RACK_VERSION].class}"
318
+ end
189
319
  ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
190
- assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") {
191
- %w[http https].include? env["rack.url_scheme"]
192
- }
320
+ unless %w[http https].include?(env[RACK_URL_SCHEME])
321
+ raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
322
+ end
193
323
 
194
324
  ## * There must be a valid input stream in <tt>rack.input</tt>.
195
- check_input env["rack.input"]
325
+ check_input env[RACK_INPUT]
196
326
  ## * There must be a valid error stream in <tt>rack.errors</tt>.
197
- check_error env["rack.errors"]
327
+ check_error env[RACK_ERRORS]
328
+ ## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
329
+ check_hijack env
198
330
 
199
331
  ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
200
- assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") {
201
- env["REQUEST_METHOD"] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
202
- }
332
+ unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
333
+ raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
334
+ end
203
335
 
204
336
  ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
205
- assert("SCRIPT_NAME must start with /") {
206
- !env.include?("SCRIPT_NAME") ||
207
- env["SCRIPT_NAME"] == "" ||
208
- env["SCRIPT_NAME"] =~ /\A\//
209
- }
337
+ if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
338
+ raise LintError, "SCRIPT_NAME must start with /"
339
+ end
210
340
  ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
211
- assert("PATH_INFO must start with /") {
212
- !env.include?("PATH_INFO") ||
213
- env["PATH_INFO"] == "" ||
214
- env["PATH_INFO"] =~ /\A\//
215
- }
341
+ if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\//
342
+ raise LintError, "PATH_INFO must start with /"
343
+ end
216
344
  ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
217
- assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
218
- !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
219
- }
345
+ if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
346
+ raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
347
+ end
220
348
 
221
349
  ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
222
350
  ## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
223
351
  ## <tt>SCRIPT_NAME</tt> is empty.
224
- assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
225
- env["SCRIPT_NAME"] || env["PATH_INFO"]
226
- }
352
+ unless env[SCRIPT_NAME] || env[PATH_INFO]
353
+ raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
354
+ end
227
355
  ## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
228
- assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
229
- env["SCRIPT_NAME"] != "/"
230
- }
356
+ unless env[SCRIPT_NAME] != "/"
357
+ raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
358
+ end
231
359
  end
232
360
 
233
361
  ## === The Input Stream
@@ -237,18 +365,18 @@ module Rack
237
365
  def check_input(input)
238
366
  ## When applicable, its external encoding must be "ASCII-8BIT" and it
239
367
  ## must be opened in binary mode, for Ruby 1.9 compatibility.
240
- assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
241
- input.external_encoding.name == "ASCII-8BIT"
242
- } if input.respond_to?(:external_encoding)
243
- assert("rack.input #{input} is not opened in binary mode") {
244
- input.binmode?
245
- } if input.respond_to?(:binmode?)
246
-
368
+ if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
369
+ raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
370
+ end
371
+ if input.respond_to?(:binmode?) && !input.binmode?
372
+ raise LintError, "rack.input #{input} is not opened in binary mode"
373
+ end
374
+
247
375
  ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
248
376
  [:gets, :each, :read, :rewind].each { |method|
249
- assert("rack.input #{input} does not respond to ##{method}") {
250
- input.respond_to? method
251
- }
377
+ unless input.respond_to? method
378
+ raise LintError, "rack.input #{input} does not respond to ##{method}"
379
+ end
252
380
  }
253
381
  end
254
382
 
@@ -259,93 +387,95 @@ module Rack
259
387
  @input = input
260
388
  end
261
389
 
262
- def size
263
- @input.size
264
- end
265
-
266
390
  ## * +gets+ must be called without arguments and return a string,
267
391
  ## or +nil+ on EOF.
268
392
  def gets(*args)
269
- assert("rack.input#gets called with arguments") { args.size == 0 }
393
+ raise LintError, "rack.input#gets called with arguments" unless args.size == 0
270
394
  v = @input.gets
271
- assert("rack.input#gets didn't return a String") {
272
- v.nil? or v.instance_of? String
273
- }
395
+ unless v.nil? or v.kind_of? String
396
+ raise LintError, "rack.input#gets didn't return a String"
397
+ end
274
398
  v
275
399
  end
276
400
 
277
- ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
278
- ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
279
- ## be a String and may not be nil. If +length+ is given and not nil, then this method
280
- ## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
281
- ## then this method reads all data until EOF.
282
- ## When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
283
- ## if +length+ is not given or is nil.
284
- ## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
285
- ## newly created String object.
401
+ ## * +read+ behaves like IO#read.
402
+ ## Its signature is <tt>read([length, [buffer]])</tt>.
403
+ ##
404
+ ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
405
+ ## and +buffer+ must be a String and may not be nil.
406
+ ##
407
+ ## If +length+ is given and not nil, then this method reads at most
408
+ ## +length+ bytes from the input stream.
409
+ ##
410
+ ## If +length+ is not given or nil, then this method reads
411
+ ## all data until EOF.
412
+ ##
413
+ ## When EOF is reached, this method returns nil if +length+ is given
414
+ ## and not nil, or "" if +length+ is not given or is nil.
415
+ ##
416
+ ## If +buffer+ is given, then the read data will be placed
417
+ ## into +buffer+ instead of a newly created String object.
286
418
  def read(*args)
287
- assert("rack.input#read called with too many arguments") {
288
- args.size <= 2
289
- }
419
+ unless args.size <= 2
420
+ raise LintError, "rack.input#read called with too many arguments"
421
+ end
290
422
  if args.size >= 1
291
- assert("rack.input#read called with non-integer and non-nil length") {
292
- args.first.kind_of?(Integer) || args.first.nil?
293
- }
294
- assert("rack.input#read called with a negative length") {
295
- args.first.nil? || args.first >= 0
296
- }
423
+ unless args.first.kind_of?(Integer) || args.first.nil?
424
+ raise LintError, "rack.input#read called with non-integer and non-nil length"
425
+ end
426
+ unless args.first.nil? || args.first >= 0
427
+ raise LintError, "rack.input#read called with a negative length"
428
+ end
297
429
  end
298
430
  if args.size >= 2
299
- assert("rack.input#read called with non-String buffer") {
300
- args[1].kind_of?(String)
301
- }
431
+ unless args[1].kind_of?(String)
432
+ raise LintError, "rack.input#read called with non-String buffer"
433
+ end
302
434
  end
303
-
435
+
304
436
  v = @input.read(*args)
305
-
306
- assert("rack.input#read didn't return nil or a String") {
307
- v.nil? or v.instance_of? String
308
- }
437
+
438
+ unless v.nil? or v.kind_of? String
439
+ raise LintError, "rack.input#read didn't return nil or a String"
440
+ end
309
441
  if args[0].nil?
310
- assert("rack.input#read(nil) returned nil on EOF") {
311
- !v.nil?
312
- }
442
+ unless !v.nil?
443
+ raise LintError, "rack.input#read(nil) returned nil on EOF"
444
+ end
313
445
  end
314
-
446
+
315
447
  v
316
448
  end
317
449
 
318
450
  ## * +each+ must be called without arguments and only yield Strings.
319
451
  def each(*args)
320
- assert("rack.input#each called with arguments") { args.size == 0 }
452
+ raise LintError, "rack.input#each called with arguments" unless args.size == 0
321
453
  @input.each { |line|
322
- assert("rack.input#each didn't yield a String") {
323
- line.instance_of? String
324
- }
454
+ unless line.kind_of? String
455
+ raise LintError, "rack.input#each didn't yield a String"
456
+ end
325
457
  yield line
326
458
  }
327
459
  end
328
-
460
+
329
461
  ## * +rewind+ must be called without arguments. It rewinds the input
330
462
  ## stream back to the beginning. It must not raise Errno::ESPIPE:
331
463
  ## that is, it may not be a pipe or a socket. Therefore, handler
332
464
  ## developers must buffer the input data into some rewindable object
333
465
  ## if the underlying input stream is not rewindable.
334
466
  def rewind(*args)
335
- assert("rack.input#rewind called with arguments") { args.size == 0 }
336
- assert("rack.input#rewind raised Errno::ESPIPE") {
337
- begin
338
- @input.rewind
339
- true
340
- rescue Errno::ESPIPE
341
- false
342
- end
343
- }
467
+ raise LintError, "rack.input#rewind called with arguments" unless args.size == 0
468
+ begin
469
+ @input.rewind
470
+ true
471
+ rescue Errno::ESPIPE
472
+ raise LintError, "rack.input#rewind raised Errno::ESPIPE"
473
+ end
344
474
  end
345
475
 
346
476
  ## * +close+ must never be called on the input stream.
347
477
  def close(*args)
348
- assert("rack.input#close must not be called") { false }
478
+ raise LintError, "rack.input#close must not be called"
349
479
  end
350
480
  end
351
481
 
@@ -353,9 +483,9 @@ module Rack
353
483
  def check_error(error)
354
484
  ## The error stream must respond to +puts+, +write+ and +flush+.
355
485
  [:puts, :write, :flush].each { |method|
356
- assert("rack.error #{error} does not respond to ##{method}") {
357
- error.respond_to? method
358
- }
486
+ unless error.respond_to? method
487
+ raise LintError, "rack.error #{error} does not respond to ##{method}"
488
+ end
359
489
  }
360
490
  end
361
491
 
@@ -373,7 +503,7 @@ module Rack
373
503
 
374
504
  ## * +write+ must be called with a single argument that is a String.
375
505
  def write(str)
376
- assert("rack.errors#write not called with a String") { str.instance_of? String }
506
+ raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
377
507
  @error.write str
378
508
  end
379
509
 
@@ -385,50 +515,177 @@ module Rack
385
515
 
386
516
  ## * +close+ must never be called on the error stream.
387
517
  def close(*args)
388
- assert("rack.errors#close must not be called") { false }
518
+ raise LintError, "rack.errors#close must not be called"
389
519
  end
390
520
  end
391
521
 
522
+ class HijackWrapper
523
+ include Assertion
524
+ extend Forwardable
525
+
526
+ REQUIRED_METHODS = [
527
+ :read, :write, :read_nonblock, :write_nonblock, :flush, :close,
528
+ :close_read, :close_write, :closed?
529
+ ]
530
+
531
+ def_delegators :@io, *REQUIRED_METHODS
532
+
533
+ def initialize(io)
534
+ @io = io
535
+ REQUIRED_METHODS.each do |meth|
536
+ raise LintError, "rack.hijack_io must respond to #{meth}" unless io.respond_to? meth
537
+ end
538
+ end
539
+ end
540
+
541
+ ## === Hijacking
542
+ #
543
+ # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
544
+ # should not be removed. The whitespace creates paragraphs in the RDoc
545
+ # output.
546
+ #
547
+ ## ==== Request (before status)
548
+ def check_hijack(env)
549
+ if env[RACK_IS_HIJACK]
550
+ ## If rack.hijack? is true then rack.hijack must respond to #call.
551
+ original_hijack = env[RACK_HIJACK]
552
+ raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
553
+ env[RACK_HIJACK] = proc do
554
+ ## rack.hijack must return the io that will also be assigned (or is
555
+ ## already present, in rack.hijack_io.
556
+ io = original_hijack.call
557
+ HijackWrapper.new(io)
558
+ ##
559
+ ## rack.hijack_io must respond to:
560
+ ## <tt>read, write, read_nonblock, write_nonblock, flush, close,
561
+ ## close_read, close_write, closed?</tt>
562
+ ##
563
+ ## The semantics of these IO methods must be a best effort match to
564
+ ## those of a normal ruby IO or Socket object, using standard
565
+ ## arguments and raising standard exceptions. Servers are encouraged
566
+ ## to simply pass on real IO objects, although it is recognized that
567
+ ## this approach is not directly compatible with SPDY and HTTP 2.0.
568
+ ##
569
+ ## IO provided in rack.hijack_io should preference the
570
+ ## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
571
+ ##
572
+ ## There is a deliberate lack of full specification around
573
+ ## rack.hijack_io, as semantics will change from server to server.
574
+ ## Users are encouraged to utilize this API with a knowledge of their
575
+ ## server choice, and servers may extend the functionality of
576
+ ## hijack_io to provide additional features to users. The purpose of
577
+ ## rack.hijack is for Rack to "get out of the way", as such, Rack only
578
+ ## provides the minimum of specification and support.
579
+ env[RACK_HIJACK_IO] = HijackWrapper.new(env[RACK_HIJACK_IO])
580
+ io
581
+ end
582
+ else
583
+ ##
584
+ ## If rack.hijack? is false, then rack.hijack should not be set.
585
+ raise LintError, "rack.hijack? is false, but rack.hijack is present" unless env[RACK_HIJACK].nil?
586
+ ##
587
+ ## If rack.hijack? is false, then rack.hijack_io should not be set.
588
+ raise LintError, "rack.hijack? is false, but rack.hijack_io is present" unless env[RACK_HIJACK_IO].nil?
589
+ end
590
+ end
591
+
592
+ ## ==== Response (after headers)
593
+ ## It is also possible to hijack a response after the status and headers
594
+ ## have been sent.
595
+ def check_hijack_response(headers, env)
596
+
597
+ # this check uses headers like a hash, but the spec only requires
598
+ # headers respond to #each
599
+ headers = Rack::Utils::HeaderHash[headers]
600
+
601
+ ## In order to do this, an application may set the special header
602
+ ## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
603
+ ## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
604
+ ## protocol.
605
+ ##
606
+ ## After the headers have been sent, and this hijack callback has been
607
+ ## called, the application is now responsible for the remaining lifecycle
608
+ ## of the IO. The application is also responsible for maintaining HTTP
609
+ ## semantics. Of specific note, in almost all cases in the current SPEC,
610
+ ## applications will have wanted to specify the header Connection:close in
611
+ ## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
612
+ ## returning hijacked sockets to the web server. For that purpose, use the
613
+ ## body streaming API instead (progressively yielding strings via each).
614
+ ##
615
+ ## Servers must ignore the <tt>body</tt> part of the response tuple when
616
+ ## the <tt>rack.hijack</tt> response API is in use.
617
+
618
+ if env[RACK_IS_HIJACK] && headers[RACK_HIJACK]
619
+ unless headers[RACK_HIJACK].respond_to? :call
620
+ raise LintError, 'rack.hijack header must respond to #call'
621
+ end
622
+ original_hijack = headers[RACK_HIJACK]
623
+ proc do |io|
624
+ original_hijack.call HijackWrapper.new(io)
625
+ end
626
+ else
627
+ ##
628
+ ## The special response header <tt>rack.hijack</tt> must only be set
629
+ ## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
630
+ unless headers[RACK_HIJACK].nil?
631
+ raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
632
+ end
633
+
634
+ nil
635
+ end
636
+ end
637
+ ## ==== Conventions
638
+ ## * Middleware should not use hijack unless it is handling the whole
639
+ ## response.
640
+ ## * Middleware may wrap the IO object for the response pattern.
641
+ ## * Middleware should not wrap the IO object for the request pattern. The
642
+ ## request pattern is intended to provide the hijacker with "raw tcp".
643
+
392
644
  ## == The Response
393
645
 
394
646
  ## === The Status
395
647
  def check_status(status)
396
648
  ## This is an HTTP status. When parsed as integer (+to_i+), it must be
397
649
  ## greater than or equal to 100.
398
- assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
650
+ unless status.to_i >= 100
651
+ raise LintError, "Status must be >=100 seen as integer"
652
+ end
399
653
  end
400
654
 
401
655
  ## === The Headers
402
656
  def check_headers(header)
403
657
  ## The header must respond to +each+, and yield values of key and value.
404
- assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
405
- header.respond_to? :each
406
- }
658
+ unless header.respond_to? :each
659
+ raise LintError, "headers object should respond to #each, but doesn't (got #{header.class} as headers)"
660
+ end
661
+
407
662
  header.each { |key, value|
408
663
  ## The header keys must be Strings.
409
- assert("header key must be a string, was #{key.class}") {
410
- key.instance_of? String
411
- }
412
- ## The header must not contain a +Status+ key,
413
- assert("header must not contain Status") { key.downcase != "status" }
414
- ## contain keys with <tt>:</tt> or newlines in their name,
415
- assert("header names must not contain : or \\n") { key !~ /[:\n]/ }
416
- ## contain keys names that end in <tt>-</tt> or <tt>_</tt>,
417
- assert("header names must not end in - or _") { key !~ /[-_]\z/ }
418
- ## but only contain keys that consist of
419
- ## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
420
- assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
664
+ unless key.kind_of? String
665
+ raise LintError, "header key must be a string, was #{key.class}"
666
+ end
667
+
668
+ ## Special headers starting "rack." are for communicating with the
669
+ ## server, and must not be sent back to the client.
670
+ next if key =~ /^rack\..+$/
671
+
672
+ ## The header must not contain a +Status+ key.
673
+ raise LintError, "header must not contain Status" if key.downcase == "status"
674
+ ## The header must conform to RFC7230 token specification, i.e. cannot
675
+ ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
676
+ raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
421
677
 
422
678
  ## The values of the header must be Strings,
423
- assert("a header value must be a String, but the value of " +
424
- "'#{key}' is a #{value.class}") { value.kind_of? String }
679
+ unless value.kind_of? String
680
+ raise LintError, "a header value must be a String, but the value of '#{key}' is a #{value.class}"
681
+ end
425
682
  ## consisting of lines (for multiple header values, e.g. multiple
426
- ## <tt>Set-Cookie</tt> values) seperated by "\n".
683
+ ## <tt>Set-Cookie</tt> values) separated by "\\n".
427
684
  value.split("\n").each { |item|
428
685
  ## The lines must not contain characters below 037.
429
- assert("invalid header value #{key}: #{item.inspect}") {
430
- item !~ /[\000-\037]/
431
- }
686
+ if item =~ /[\000-\037]/
687
+ raise LintError, "invalid header value #{key}: #{item.inspect}"
688
+ end
432
689
  }
433
690
  }
434
691
  end
@@ -436,79 +693,71 @@ module Rack
436
693
  ## === The Content-Type
437
694
  def check_content_type(status, headers)
438
695
  headers.each { |key, value|
439
- ## There must be a <tt>Content-Type</tt>, except when the
440
- ## +Status+ is 1xx, 204 or 304, in which case there must be none
441
- ## given.
696
+ ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
697
+ ## 204 or 304.
442
698
  if key.downcase == "content-type"
443
- assert("Content-Type header found in #{status} response, not allowed") {
444
- not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
445
- }
699
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
700
+ raise LintError, "Content-Type header found in #{status} response, not allowed"
701
+ end
446
702
  return
447
703
  end
448
704
  }
449
- assert("No Content-Type header found") {
450
- Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
451
- }
452
705
  end
453
706
 
454
707
  ## === The Content-Length
455
- def check_content_length(status, headers, env)
708
+ def check_content_length(status, headers)
456
709
  headers.each { |key, value|
457
710
  if key.downcase == 'content-length'
458
711
  ## There must not be a <tt>Content-Length</tt> header when the
459
712
  ## +Status+ is 1xx, 204 or 304.
460
- assert("Content-Length header found in #{status} response, not allowed") {
461
- not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
462
- }
463
-
464
- bytes = 0
465
- string_body = true
466
-
467
- if @body.respond_to?(:to_ary)
468
- @body.each { |part|
469
- unless part.kind_of?(String)
470
- string_body = false
471
- break
472
- end
473
-
474
- bytes += Rack::Utils.bytesize(part)
475
- }
476
-
477
- if env["REQUEST_METHOD"] == "HEAD"
478
- assert("Response body was given for HEAD request, but should be empty") {
479
- bytes == 0
480
- }
481
- else
482
- if string_body
483
- assert("Content-Length header was #{value}, but should be #{bytes}") {
484
- value == bytes.to_s
485
- }
486
- end
487
- end
713
+ if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
714
+ raise LintError, "Content-Length header found in #{status} response, not allowed"
488
715
  end
489
-
490
- return
716
+ @content_length = value
491
717
  end
492
718
  }
493
719
  end
494
720
 
721
+ def verify_content_length(bytes)
722
+ if @head_request
723
+ unless bytes == 0
724
+ raise LintError, "Response body was given for HEAD request, but should be empty"
725
+ end
726
+ elsif @content_length
727
+ unless @content_length == bytes.to_s
728
+ raise LintError, "Content-Length header was #{@content_length}, but should be #{bytes}"
729
+ end
730
+ end
731
+ end
732
+
495
733
  ## === The Body
496
734
  def each
497
735
  @closed = false
736
+ bytes = 0
737
+
498
738
  ## The Body must respond to +each+
739
+ unless @body.respond_to?(:each)
740
+ raise LintError, "Response body must respond to each"
741
+ end
742
+
499
743
  @body.each { |part|
500
744
  ## and must only yield String values.
501
- assert("Body yielded non-string value #{part.inspect}") {
502
- part.instance_of? String
503
- }
745
+ unless part.kind_of? String
746
+ raise LintError, "Body yielded non-string value #{part.inspect}"
747
+ end
748
+ bytes += part.bytesize
504
749
  yield part
505
750
  }
751
+ verify_content_length(bytes)
752
+
506
753
  ##
507
754
  ## The Body itself should not be an instance of String, as this will
508
755
  ## break in Ruby 1.9.
509
756
  ##
510
- ## If the Body responds to +close+, it will be called after iteration.
511
- # XXX howto: assert("Body has not been closed") { @closed }
757
+ ## If the Body responds to +close+, it will be called after iteration. If
758
+ ## the body is replaced by a middleware after action, the original body
759
+ ## must be closed first, if it responds to close.
760
+ # XXX howto: raise LintError, "Body has not been closed" unless @closed
512
761
 
513
762
 
514
763
  ##
@@ -519,9 +768,9 @@ module Rack
519
768
  ## transport the response.
520
769
 
521
770
  if @body.respond_to?(:to_path)
522
- assert("The file identified by body.to_path does not exist") {
523
- ::File.exist? @body.to_path
524
- }
771
+ unless ::File.exist? @body.to_path
772
+ raise LintError, "The file identified by body.to_path does not exist"
773
+ end
525
774
  end
526
775
 
527
776
  ##