rack 1.6.0.beta → 1.6.0.beta2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/SPEC +3 -3
  3. data/lib/rack.rb +10 -0
  4. data/lib/rack/auth/abstract/handler.rb +4 -4
  5. data/lib/rack/auth/digest/request.rb +1 -1
  6. data/lib/rack/cascade.rb +1 -1
  7. data/lib/rack/chunked.rb +2 -2
  8. data/lib/rack/commonlogger.rb +4 -4
  9. data/lib/rack/conditionalget.rb +3 -3
  10. data/lib/rack/content_length.rb +2 -2
  11. data/lib/rack/content_type.rb +1 -1
  12. data/lib/rack/deflater.rb +4 -5
  13. data/lib/rack/directory.rb +5 -5
  14. data/lib/rack/etag.rb +7 -6
  15. data/lib/rack/file.rb +32 -14
  16. data/lib/rack/handler.rb +1 -1
  17. data/lib/rack/handler/cgi.rb +1 -1
  18. data/lib/rack/handler/fastcgi.rb +1 -1
  19. data/lib/rack/handler/lsws.rb +1 -1
  20. data/lib/rack/handler/mongrel.rb +1 -1
  21. data/lib/rack/handler/scgi.rb +2 -2
  22. data/lib/rack/handler/webrick.rb +6 -4
  23. data/lib/rack/head.rb +1 -1
  24. data/lib/rack/lint.rb +30 -16
  25. data/lib/rack/lobster.rb +2 -2
  26. data/lib/rack/methodoverride.rb +3 -3
  27. data/lib/rack/mock.rb +7 -7
  28. data/lib/rack/multipart/parser.rb +21 -7
  29. data/lib/rack/recursive.rb +6 -5
  30. data/lib/rack/request.rb +6 -6
  31. data/lib/rack/response.rb +8 -6
  32. data/lib/rack/runtime.rb +2 -1
  33. data/lib/rack/sendfile.rb +2 -2
  34. data/lib/rack/server.rb +7 -10
  35. data/lib/rack/showexceptions.rb +2 -2
  36. data/lib/rack/showstatus.rb +3 -3
  37. data/lib/rack/static.rb +1 -1
  38. data/lib/rack/urlmap.rb +2 -2
  39. data/lib/rack/utils.rb +14 -10
  40. data/rack.gemspec +1 -1
  41. data/test/spec_body_proxy.rb +16 -0
  42. data/test/spec_lint.rb +20 -9
  43. data/test/spec_multipart.rb +38 -16
  44. data/test/spec_request.rb +17 -0
  45. data/test/spec_server.rb +22 -13
  46. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b7e9fdbcce0cd16bdeda0ee39e9aa066e2dbcb17
4
- data.tar.gz: 1be24bd36bc380d25cb5ef7bff5bf2abab269412
3
+ metadata.gz: 67268c360a9006f4c63b876a1dff3d8f4dfbdc51
4
+ data.tar.gz: 394f5b04b57657f9fe03bbd84ea1d81bb6a438e5
5
5
  SHA512:
6
- metadata.gz: 1f4730c383cd5346a255fe4a179470653370f345ccf508389bf4149c744908b195ec3676eb564f5bf434e261d698230c7ba38dd7d192ddd022c1df9d9c0f7bf7
7
- data.tar.gz: 42f1f9f7239286710e4b3b58f142cd673fe66e56b9e42ec45c2c7a3406079118f2dd1684c165404d9f77e482129335fa315ef179fefd0711c0f59156c4ca7a31
6
+ metadata.gz: ff862d5cdcb4a71aecd2d94d4774ddf116cc0b94e5b07e5833d14928605b61c845103bcd8cc1638c69d8fb3ce1e4a2a2d39f7a896b3940b1adce960a276fae3f
7
+ data.tar.gz: e96164e1ee85b7d08e16fa2b80d6913f17161847e53eec50f9a8e745833b867c641798ab0857d3a055c6e3cb4ef8ee7094dbd19090a06a6e2c47a45cec636f1d
data/SPEC CHANGED
@@ -112,6 +112,8 @@ be implemented by the server.
112
112
  warn(message, &block)
113
113
  error(message, &block)
114
114
  fatal(message, &block)
115
+ <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
116
+ <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.
115
117
  The server or the application can store their own data in the
116
118
  environment, too. The keys must contain at least one dot,
117
119
  and should be prefixed uniquely. The prefix <tt>rack.</tt>
@@ -238,9 +240,7 @@ server, and must not be sent back to the client.
238
240
  The header keys must be Strings.
239
241
  The header must not contain a +Status+ key,
240
242
  contain keys with <tt>:</tt> or newlines in their name,
241
- contain keys names that end in <tt>-</tt> or <tt>_</tt>,
242
- but only contain keys that consist of
243
- letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
243
+ but only contain keys that match the token rule according to RFC 2616.
244
244
  The values of the header must be Strings,
245
245
  consisting of lines (for multiple header values, e.g. multiple
246
246
  <tt>Set-Cookie</tt> values) separated by "\\n".
@@ -22,6 +22,16 @@ module Rack
22
22
  def self.release
23
23
  "1.5"
24
24
  end
25
+ PATH_INFO = 'PATH_INFO'.freeze
26
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
27
+ SCRIPT_NAME = 'SCRIPT_NAME'.freeze
28
+ QUERY_STRING = 'QUERY_STRING'.freeze
29
+ CACHE_CONTROL = 'Cache-Control'.freeze
30
+ CONTENT_LENGTH = 'Content-Length'.freeze
31
+ CONTENT_TYPE = 'Content-Type'.freeze
32
+
33
+ GET = 'GET'.freeze
34
+ HEAD = 'HEAD'.freeze
25
35
 
26
36
  autoload :Builder, "rack/builder"
27
37
  autoload :BodyProxy, "rack/body_proxy"
@@ -17,8 +17,8 @@ module Rack
17
17
 
18
18
  def unauthorized(www_authenticate = challenge)
19
19
  return [ 401,
20
- { 'Content-Type' => 'text/plain',
21
- 'Content-Length' => '0',
20
+ { CONTENT_TYPE => 'text/plain',
21
+ CONTENT_LENGTH => '0',
22
22
  'WWW-Authenticate' => www_authenticate.to_s },
23
23
  []
24
24
  ]
@@ -26,8 +26,8 @@ module Rack
26
26
 
27
27
  def bad_request
28
28
  return [ 400,
29
- { 'Content-Type' => 'text/plain',
30
- 'Content-Length' => '0' },
29
+ { CONTENT_TYPE => 'text/plain',
30
+ CONTENT_LENGTH => '0' },
31
31
  []
32
32
  ]
33
33
  end
@@ -7,7 +7,7 @@ module Rack
7
7
  module Digest
8
8
  class Request < Auth::AbstractRequest
9
9
  def method
10
- @env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
10
+ @env['rack.methodoverride.original_method'] || @env[REQUEST_METHOD]
11
11
  end
12
12
 
13
13
  def digest?
@@ -4,7 +4,7 @@ module Rack
4
4
  # status codes).
5
5
 
6
6
  class Cascade
7
- NotFound = [404, {"Content-Type" => "text/plain"}, []]
7
+ NotFound = [404, {CONTENT_TYPE => "text/plain"}, []]
8
8
 
9
9
  attr_reader :apps
10
10
 
@@ -56,11 +56,11 @@ module Rack
56
56
 
57
57
  if ! chunkable_version?(env['HTTP_VERSION']) ||
58
58
  STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
59
- headers['Content-Length'] ||
59
+ headers[CONTENT_LENGTH] ||
60
60
  headers['Transfer-Encoding']
61
61
  [status, headers, body]
62
62
  else
63
- headers.delete('Content-Length')
63
+ headers.delete(CONTENT_LENGTH)
64
64
  headers['Transfer-Encoding'] = 'chunked'
65
65
  [status, headers, Body.new(body)]
66
66
  end
@@ -46,9 +46,9 @@ module Rack
46
46
  env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
47
47
  env["REMOTE_USER"] || "-",
48
48
  now.strftime("%d/%b/%Y:%H:%M:%S %z"),
49
- env["REQUEST_METHOD"],
50
- env["PATH_INFO"],
51
- env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
49
+ env[REQUEST_METHOD],
50
+ env[PATH_INFO],
51
+ env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING],
52
52
  env["HTTP_VERSION"],
53
53
  status.to_s[0..3],
54
54
  length,
@@ -65,7 +65,7 @@ module Rack
65
65
  end
66
66
 
67
67
  def extract_content_length(headers)
68
- value = headers['Content-Length'] or return '-'
68
+ value = headers[CONTENT_LENGTH] or return '-'
69
69
  value.to_s == '0' ? '-' : value
70
70
  end
71
71
  end
@@ -20,14 +20,14 @@ module Rack
20
20
  end
21
21
 
22
22
  def call(env)
23
- case env['REQUEST_METHOD']
23
+ case env[REQUEST_METHOD]
24
24
  when "GET", "HEAD"
25
25
  status, headers, body = @app.call(env)
26
26
  headers = Utils::HeaderHash.new(headers)
27
27
  if status == 200 && fresh?(env, headers)
28
28
  status = 304
29
- headers.delete('Content-Type')
30
- headers.delete('Content-Length')
29
+ headers.delete(CONTENT_TYPE)
30
+ headers.delete(CONTENT_LENGTH)
31
31
  original_body = body
32
32
  body = Rack::BodyProxy.new([]) do
33
33
  original_body.close if original_body.respond_to?(:close)
@@ -16,7 +16,7 @@ module Rack
16
16
  headers = HeaderHash.new(headers)
17
17
 
18
18
  if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
19
- !headers['Content-Length'] &&
19
+ !headers[CONTENT_LENGTH] &&
20
20
  !headers['Transfer-Encoding'] &&
21
21
  body.respond_to?(:to_ary)
22
22
 
@@ -28,7 +28,7 @@ module Rack
28
28
  obody.close if obody.respond_to?(:close)
29
29
  end
30
30
 
31
- headers['Content-Length'] = length.to_s
31
+ headers[CONTENT_LENGTH] = length.to_s
32
32
  end
33
33
 
34
34
  [status, headers, body]
@@ -20,7 +20,7 @@ module Rack
20
20
  headers = Utils::HeaderHash.new(headers)
21
21
 
22
22
  unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
23
- headers['Content-Type'] ||= @content_type
23
+ headers[CONTENT_TYPE] ||= @content_type
24
24
  end
25
25
 
26
26
  [status, headers, body]
@@ -1,5 +1,4 @@
1
1
  require "zlib"
2
- require "stringio"
3
2
  require "time" # for Time.httpdate
4
3
  require 'rack/utils'
5
4
 
@@ -54,20 +53,20 @@ module Rack
54
53
  case encoding
55
54
  when "gzip"
56
55
  headers['Content-Encoding'] = "gzip"
57
- headers.delete('Content-Length')
56
+ headers.delete(CONTENT_LENGTH)
58
57
  mtime = headers.key?("Last-Modified") ?
59
58
  Time.httpdate(headers["Last-Modified"]) : Time.now
60
59
  [status, headers, GzipStream.new(body, mtime)]
61
60
  when "deflate"
62
61
  headers['Content-Encoding'] = "deflate"
63
- headers.delete('Content-Length')
62
+ headers.delete(CONTENT_LENGTH)
64
63
  [status, headers, DeflateStream.new(body)]
65
64
  when "identity"
66
65
  [status, headers, body]
67
66
  when nil
68
67
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
69
68
  bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
70
- [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, bp]
69
+ [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
71
70
  end
72
71
  end
73
72
 
@@ -138,7 +137,7 @@ module Rack
138
137
  # Skip compressing empty entity body responses and responses with
139
138
  # no-transform set.
140
139
  if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
141
- headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
140
+ headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
142
141
  (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
143
142
  return false
144
143
  end
@@ -55,8 +55,8 @@ table { width:100%%; }
55
55
 
56
56
  def _call(env)
57
57
  @env = env
58
- @script_name = env['SCRIPT_NAME']
59
- @path_info = Utils.unescape(env['PATH_INFO'])
58
+ @script_name = env[SCRIPT_NAME]
59
+ @path_info = Utils.unescape(env[PATH_INFO])
60
60
 
61
61
  if forbidden = check_forbidden
62
62
  forbidden
@@ -72,7 +72,7 @@ table { width:100%%; }
72
72
  body = "Forbidden\n"
73
73
  size = Rack::Utils.bytesize(body)
74
74
  return [403, {"Content-Type" => "text/plain",
75
- "Content-Length" => size.to_s,
75
+ CONTENT_LENGTH => size.to_s,
76
76
  "X-Cascade" => "pass"}, [body]]
77
77
  end
78
78
 
@@ -101,7 +101,7 @@ table { width:100%%; }
101
101
  @files << [ url, basename, size, type, mtime ]
102
102
  end
103
103
 
104
- return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
104
+ return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
105
105
  end
106
106
 
107
107
  def stat(node, max = 10)
@@ -130,7 +130,7 @@ table { width:100%%; }
130
130
  body = "Entity not found: #{@path_info}\n"
131
131
  size = Rack::Utils.bytesize(body)
132
132
  return [404, {"Content-Type" => "text/plain",
133
- "Content-Length" => size.to_s,
133
+ CONTENT_LENGTH => size.to_s,
134
134
  "X-Cascade" => "pass"}, [body]]
135
135
  end
136
136
 
@@ -11,6 +11,7 @@ module Rack
11
11
  # used when Etag is absent and a directive when it is present. The first
12
12
  # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
13
13
  class ETag
14
+ ETAG_STRING = 'ETag'.freeze
14
15
  DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
15
16
 
16
17
  def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@@ -28,14 +29,14 @@ module Rack
28
29
  body = Rack::BodyProxy.new(new_body) do
29
30
  original_body.close if original_body.respond_to?(:close)
30
31
  end
31
- headers['ETag'] = %(W/"#{digest}") if digest
32
+ headers[ETAG_STRING] = %(W/"#{digest}") if digest
32
33
  end
33
34
 
34
- unless headers['Cache-Control']
35
+ unless headers[CACHE_CONTROL]
35
36
  if digest
36
- headers['Cache-Control'] = @cache_control if @cache_control
37
+ headers[CACHE_CONTROL] = @cache_control if @cache_control
37
38
  else
38
- headers['Cache-Control'] = @no_cache_control if @no_cache_control
39
+ headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
39
40
  end
40
41
  end
41
42
 
@@ -53,8 +54,8 @@ module Rack
53
54
  end
54
55
 
55
56
  def skip_caching?(headers)
56
- (headers['Cache-Control'] && headers['Cache-Control'].include?('no-cache')) ||
57
- headers.key?('ETag') || headers.key?('Last-Modified')
57
+ (headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
58
+ headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
58
59
  end
59
60
 
60
61
  def digest_body(body)
@@ -34,11 +34,11 @@ module Rack
34
34
  F = ::File
35
35
 
36
36
  def _call(env)
37
- unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
37
+ unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
38
38
  return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
39
39
  end
40
40
 
41
- path_info = Utils.unescape(env["PATH_INFO"])
41
+ path_info = Utils.unescape(env[PATH_INFO])
42
42
  clean_path_info = Utils.clean_path_info(path_info)
43
43
 
44
44
  @path = F.join(@root, clean_path_info)
@@ -58,25 +58,20 @@ module Rack
58
58
 
59
59
  def serving(env)
60
60
  if env["REQUEST_METHOD"] == "OPTIONS"
61
- return [200, {'Allow' => ALLOW_HEADER, 'Content-Length' => '0'}, []]
61
+ return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
62
62
  end
63
63
  last_modified = F.mtime(@path).httpdate
64
64
  return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified
65
65
 
66
66
  headers = { "Last-Modified" => last_modified }
67
- mime = Mime.mime_type(F.extname(@path), @default_mime)
68
- headers["Content-Type"] = mime if mime
67
+ headers[CONTENT_TYPE] = mime_type if mime_type
69
68
 
70
69
  # Set custom headers
71
70
  @headers.each { |field, content| headers[field] = content } if @headers
72
71
 
73
- response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
72
+ response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
74
73
 
75
- # NOTE:
76
- # We check via File::size? whether this file provides size info
77
- # via stat (e.g. /proc files often don't), otherwise we have to
78
- # figure it out by reading the whole file into memory.
79
- size = F.size?(@path) || Utils.bytesize(F.read(@path))
74
+ size = filesize
80
75
 
81
76
  ranges = Rack::Utils.byte_ranges(env, size)
82
77
  if ranges.nil? || ranges.length > 1
@@ -97,7 +92,9 @@ module Rack
97
92
  size = @range.end - @range.begin + 1
98
93
  end
99
94
 
100
- response[1]["Content-Length"] = size.to_s
95
+ response[2] = [response_body] unless response_body.nil?
96
+
97
+ response[1][CONTENT_LENGTH] = size.to_s
101
98
  response
102
99
  end
103
100
 
@@ -122,13 +119,34 @@ module Rack
122
119
  [
123
120
  status,
124
121
  {
125
- "Content-Type" => "text/plain",
126
- "Content-Length" => body.size.to_s,
122
+ CONTENT_TYPE => "text/plain",
123
+ CONTENT_LENGTH => body.size.to_s,
127
124
  "X-Cascade" => "pass"
128
125
  }.merge!(headers),
129
126
  [body]
130
127
  ]
131
128
  end
132
129
 
130
+ # The MIME type for the contents of the file located at @path
131
+ def mime_type
132
+ Mime.mime_type(F.extname(@path), @default_mime)
133
+ end
134
+
135
+ def filesize
136
+ # If response_body is present, use its size.
137
+ return Rack::Utils.bytesize(response_body) if response_body
138
+
139
+ # We check via File::size? whether this file provides size info
140
+ # via stat (e.g. /proc files often don't), otherwise we have to
141
+ # figure it out by reading the whole file into memory.
142
+ F.size?(@path) || Utils.bytesize(F.read(@path))
143
+ end
144
+
145
+ # By default, the response body for file requests is nil.
146
+ # In this case, the response body will be generated later
147
+ # from the file at @path
148
+ def response_body
149
+ nil
150
+ end
133
151
  end
134
152
  end
@@ -51,7 +51,7 @@ module Rack
51
51
  options.delete :Port
52
52
 
53
53
  Rack::Handler::FastCGI
54
- elsif ENV.include?("REQUEST_METHOD")
54
+ elsif ENV.include?(REQUEST_METHOD)
55
55
  Rack::Handler::CGI
56
56
  elsif ENV.include?("RACK_HANDLER")
57
57
  self.get(ENV["RACK_HANDLER"])
@@ -26,7 +26,7 @@ module Rack
26
26
  "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
27
27
  })
28
28
 
29
- env["QUERY_STRING"] ||= ""
29
+ env[QUERY_STRING] ||= ""
30
30
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
31
31
  env["REQUEST_PATH"] ||= "/"
32
32
 
@@ -59,7 +59,7 @@ module Rack
59
59
  "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
60
60
  })
61
61
 
62
- env["QUERY_STRING"] ||= ""
62
+ env[QUERY_STRING] ||= ""
63
63
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
64
64
  env["REQUEST_PATH"] ||= "/"
65
65
  env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
@@ -27,7 +27,7 @@ module Rack
27
27
  "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
28
28
  )
29
29
 
30
- env["QUERY_STRING"] ||= ""
30
+ env[QUERY_STRING] ||= ""
31
31
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
32
32
  env["REQUEST_PATH"] ||= "/"
33
33
  status, headers, body = app.call(env)
@@ -78,7 +78,7 @@ module Rack
78
78
 
79
79
  "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
80
80
  })
81
- env["QUERY_STRING"] ||= ""
81
+ env[QUERY_STRING] ||= ""
82
82
 
83
83
  status, headers, body = @app.call(env)
84
84
 
@@ -35,9 +35,9 @@ module Rack
35
35
  env = Hash[request]
36
36
  env.delete "HTTP_CONTENT_TYPE"
37
37
  env.delete "HTTP_CONTENT_LENGTH"
38
- env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2)
38
+ env["REQUEST_PATH"], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2)
39
39
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
40
- env["PATH_INFO"] = env["REQUEST_PATH"]
40
+ env[PATH_INFO] = env["REQUEST_PATH"]
41
41
  env["QUERY_STRING"] ||= ""
42
42
  env["SCRIPT_NAME"] = ""
43
43
 
@@ -79,12 +79,12 @@ module Rack
79
79
  })
80
80
 
81
81
  env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
82
- env["QUERY_STRING"] ||= ""
83
- unless env["PATH_INFO"] == ""
82
+ env[QUERY_STRING] ||= ""
83
+ unless env[PATH_INFO] == ""
84
84
  path, n = req.request_uri.path, env["SCRIPT_NAME"].length
85
- env["PATH_INFO"] = path[n, path.length-n]
85
+ env[PATH_INFO] = path[n, path.length-n]
86
86
  end
87
- env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env["PATH_INFO"]].join
87
+ env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[PATH_INFO]].join
88
88
 
89
89
  status, headers, body = @app.call(env)
90
90
  begin
@@ -107,6 +107,8 @@ module Rack
107
107
  res.body = rd
108
108
  res.chunked = true
109
109
  io_lambda.call wr
110
+ elsif body.respond_to?(:to_path)
111
+ res.body = ::File.open(body.to_path, 'rb')
110
112
  else
111
113
  body.each { |part|
112
114
  res.body << part