rack 0.3.0 → 0.4.0
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.
- data/AUTHORS +1 -0
- data/RDOX +61 -3
- data/README +94 -9
- data/Rakefile +36 -32
- data/SPEC +1 -7
- data/bin/rackup +31 -13
- data/lib/rack.rb +8 -19
- data/lib/rack/auth/digest/params.rb +2 -2
- data/lib/rack/auth/openid.rb +406 -80
- data/lib/rack/builder.rb +1 -1
- data/lib/rack/cascade.rb +10 -0
- data/lib/rack/commonlogger.rb +6 -1
- data/lib/rack/deflater.rb +63 -0
- data/lib/rack/directory.rb +158 -0
- data/lib/rack/file.rb +11 -5
- data/lib/rack/handler.rb +44 -0
- data/lib/rack/handler/evented_mongrel.rb +8 -0
- data/lib/rack/handler/fastcgi.rb +1 -0
- data/lib/rack/handler/mongrel.rb +21 -1
- data/lib/rack/lint.rb +20 -13
- data/lib/rack/mock.rb +1 -0
- data/lib/rack/request.rb +69 -2
- data/lib/rack/session/abstract/id.rb +140 -0
- data/lib/rack/session/memcache.rb +97 -0
- data/lib/rack/session/pool.rb +50 -59
- data/lib/rack/showstatus.rb +3 -1
- data/lib/rack/urlmap.rb +12 -12
- data/lib/rack/utils.rb +88 -9
- data/test/cgi/lighttpd.conf +1 -1
- data/test/cgi/test.fcgi +1 -2
- data/test/cgi/test.ru +2 -2
- data/test/spec_rack_auth_openid.rb +137 -0
- data/test/spec_rack_camping.rb +37 -33
- data/test/spec_rack_cascade.rb +15 -0
- data/test/spec_rack_cgi.rb +4 -3
- data/test/spec_rack_deflater.rb +70 -0
- data/test/spec_rack_directory.rb +56 -0
- data/test/spec_rack_fastcgi.rb +4 -3
- data/test/spec_rack_file.rb +11 -1
- data/test/spec_rack_handler.rb +24 -0
- data/test/spec_rack_lint.rb +19 -33
- data/test/spec_rack_mongrel.rb +71 -0
- data/test/spec_rack_request.rb +91 -1
- data/test/spec_rack_session_memcache.rb +132 -0
- data/test/spec_rack_session_pool.rb +48 -1
- data/test/spec_rack_showstatus.rb +5 -4
- data/test/spec_rack_urlmap.rb +60 -25
- data/test/spec_rack_utils.rb +118 -1
- data/test/testrequest.rb +3 -1
- metadata +67 -44
data/lib/rack/builder.rb
CHANGED
data/lib/rack/cascade.rb
CHANGED
data/lib/rack/commonlogger.rb
CHANGED
@@ -21,6 +21,10 @@ module Rack
|
|
21
21
|
[@status, @header, self]
|
22
22
|
end
|
23
23
|
|
24
|
+
def close
|
25
|
+
@body.close if @body.respond_to? :close
|
26
|
+
end
|
27
|
+
|
24
28
|
# By default, log to rack.errors.
|
25
29
|
def <<(str)
|
26
30
|
@env["rack.errors"].write(str)
|
@@ -40,7 +44,8 @@ module Rack
|
|
40
44
|
# lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
|
41
45
|
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
|
42
46
|
@logger << %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} %
|
43
|
-
[
|
47
|
+
[
|
48
|
+
@env['HTTP_X_FORWARDED_FOR'] || @env["REMOTE_ADDR"] || "-",
|
44
49
|
@env["REMOTE_USER"] || "-",
|
45
50
|
@now.strftime("%d/%b/%Y %H:%M:%S"),
|
46
51
|
@env["REQUEST_METHOD"],
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
|
6
|
+
class Deflater
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
status, headers, body = @app.call(env)
|
13
|
+
|
14
|
+
request = Request.new(env)
|
15
|
+
|
16
|
+
encoding = Utils.select_best_encoding(%w(gzip deflate identity), request.accept_encoding)
|
17
|
+
|
18
|
+
case encoding
|
19
|
+
when "gzip"
|
20
|
+
mtime = headers["Last-Modified"] || Time.now
|
21
|
+
[status, headers.merge("Content-Encoding" => "gzip"), self.class.gzip(body, mtime)]
|
22
|
+
when "deflate"
|
23
|
+
[status, headers.merge("Content-Encoding" => "deflate"), self.class.deflate(body)]
|
24
|
+
when "identity"
|
25
|
+
[status, headers, body]
|
26
|
+
when nil
|
27
|
+
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
|
28
|
+
[406, {"Content-Type" => "text/plain"}, message]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.gzip(body, mtime)
|
33
|
+
io = StringIO.new
|
34
|
+
gzip = Zlib::GzipWriter.new(io)
|
35
|
+
gzip.mtime = mtime
|
36
|
+
|
37
|
+
# TODO: Add streaming
|
38
|
+
body.each { |part| gzip << part }
|
39
|
+
|
40
|
+
gzip.close
|
41
|
+
return io.string
|
42
|
+
end
|
43
|
+
|
44
|
+
DEFLATE_ARGS = [
|
45
|
+
Zlib::DEFAULT_COMPRESSION,
|
46
|
+
# drop the zlib header which causes both Safari and IE to choke
|
47
|
+
-Zlib::MAX_WBITS,
|
48
|
+
Zlib::DEF_MEM_LEVEL,
|
49
|
+
Zlib::DEFAULT_STRATEGY
|
50
|
+
]
|
51
|
+
|
52
|
+
# Loosely based on Mongrel's Deflate handler
|
53
|
+
def self.deflate(body)
|
54
|
+
deflater = Zlib::Deflate.new(*DEFLATE_ARGS)
|
55
|
+
|
56
|
+
# TODO: Add streaming
|
57
|
+
body.each { |part| deflater << part }
|
58
|
+
|
59
|
+
return deflater.finish
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Rack::Directory serves entries below the +root+ given, according to the
|
5
|
+
# path info of the Rack request. If a directory is found, the file's contents
|
6
|
+
# will be presented in an html based index. If a file is found, the env will
|
7
|
+
# be passed to the specified +app+.
|
8
|
+
#
|
9
|
+
# If +app+ is not specified, a Rack::File of the same +root+ will be used.
|
10
|
+
|
11
|
+
class Directory
|
12
|
+
DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
|
13
|
+
DIR_PAGE = <<-PAGE
|
14
|
+
<html><head>
|
15
|
+
<title>%s</title>
|
16
|
+
<style type='text/css'>
|
17
|
+
table { width:100%%; }
|
18
|
+
.name { text-align:left; }
|
19
|
+
.size, .mtime { text-align:right; }
|
20
|
+
</style>
|
21
|
+
</head><body>
|
22
|
+
<h1>%s</h1>
|
23
|
+
<hr />
|
24
|
+
<table>
|
25
|
+
<tr>
|
26
|
+
<th class='name'>Name</th>
|
27
|
+
<th class='size'>Size</th>
|
28
|
+
<th class='type'>Type</th>
|
29
|
+
<th class='mtime'>Last Modified</th>
|
30
|
+
</tr>
|
31
|
+
%s
|
32
|
+
</table>
|
33
|
+
<hr />
|
34
|
+
</body></html>
|
35
|
+
PAGE
|
36
|
+
|
37
|
+
attr_reader :files
|
38
|
+
attr_accessor :root, :path
|
39
|
+
|
40
|
+
def initialize(root, app=nil)
|
41
|
+
@root = root
|
42
|
+
@app = app
|
43
|
+
unless defined? @app
|
44
|
+
@app = Rack::File.new(@root)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def call(env)
|
49
|
+
dup._call(env)
|
50
|
+
end
|
51
|
+
|
52
|
+
F = ::File
|
53
|
+
|
54
|
+
def _call(env)
|
55
|
+
if env["PATH_INFO"].include? ".."
|
56
|
+
body = "Forbidden\n"
|
57
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
58
|
+
return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
|
59
|
+
end
|
60
|
+
|
61
|
+
@path = F.join(@root, Utils.unescape(env['PATH_INFO']))
|
62
|
+
|
63
|
+
if F.exist?(@path) and F.readable?(@path)
|
64
|
+
if F.file?(@path)
|
65
|
+
return @app.call(env)
|
66
|
+
elsif F.directory?(@path)
|
67
|
+
@files = [['../','Parent Directory','','','']]
|
68
|
+
sName, pInfo = env.values_at('SCRIPT_NAME', 'PATH_INFO')
|
69
|
+
Dir.entries(@path).sort.each do |file|
|
70
|
+
next if file[0] == ?.
|
71
|
+
fl = F.join(@path, file)
|
72
|
+
sz = F.size(fl)
|
73
|
+
url = F.join(sName, pInfo, file)
|
74
|
+
type = F.directory?(fl) ? 'directory' :
|
75
|
+
MIME_TYPES.fetch(F.extname(file)[1..-1],'unknown')
|
76
|
+
size = (type!='directory' ? (sz<10240 ? "#{sz}B" : "#{sz/1024}KB") : '-')
|
77
|
+
mtime = F.mtime(fl).httpdate
|
78
|
+
@files << [ url, file, size, type, mtime ]
|
79
|
+
end
|
80
|
+
return [ 200, {'Content-Type'=>'text/html'}, self ]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
body = "Entity not found: #{env["PATH_INFO"]}\n"
|
85
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
86
|
+
return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
|
87
|
+
end
|
88
|
+
|
89
|
+
def each
|
90
|
+
show_path = @path.sub(/^#{@root}/,'')
|
91
|
+
files = @files.map{|f| DIR_FILE % f }*"\n"
|
92
|
+
page = DIR_PAGE % [ show_path, show_path , files ]
|
93
|
+
page.each_line{|l| yield l }
|
94
|
+
end
|
95
|
+
|
96
|
+
def each_entry
|
97
|
+
@files.each{|e| yield e }
|
98
|
+
end
|
99
|
+
|
100
|
+
# From WEBrick.
|
101
|
+
MIME_TYPES = {
|
102
|
+
"ai" => "application/postscript",
|
103
|
+
"asc" => "text/plain",
|
104
|
+
"avi" => "video/x-msvideo",
|
105
|
+
"bin" => "application/octet-stream",
|
106
|
+
"bmp" => "image/bmp",
|
107
|
+
"class" => "application/octet-stream",
|
108
|
+
"cer" => "application/pkix-cert",
|
109
|
+
"crl" => "application/pkix-crl",
|
110
|
+
"crt" => "application/x-x509-ca-cert",
|
111
|
+
#"crl" => "application/x-pkcs7-crl",
|
112
|
+
"css" => "text/css",
|
113
|
+
"dms" => "application/octet-stream",
|
114
|
+
"doc" => "application/msword",
|
115
|
+
"dvi" => "application/x-dvi",
|
116
|
+
"eps" => "application/postscript",
|
117
|
+
"etx" => "text/x-setext",
|
118
|
+
"exe" => "application/octet-stream",
|
119
|
+
"gif" => "image/gif",
|
120
|
+
"htm" => "text/html",
|
121
|
+
"html" => "text/html",
|
122
|
+
"jpe" => "image/jpeg",
|
123
|
+
"jpeg" => "image/jpeg",
|
124
|
+
"jpg" => "image/jpeg",
|
125
|
+
"js" => "text/javascript",
|
126
|
+
"lha" => "application/octet-stream",
|
127
|
+
"lzh" => "application/octet-stream",
|
128
|
+
"mov" => "video/quicktime",
|
129
|
+
"mpe" => "video/mpeg",
|
130
|
+
"mpeg" => "video/mpeg",
|
131
|
+
"mpg" => "video/mpeg",
|
132
|
+
"pbm" => "image/x-portable-bitmap",
|
133
|
+
"pdf" => "application/pdf",
|
134
|
+
"pgm" => "image/x-portable-graymap",
|
135
|
+
"png" => "image/png",
|
136
|
+
"pnm" => "image/x-portable-anymap",
|
137
|
+
"ppm" => "image/x-portable-pixmap",
|
138
|
+
"ppt" => "application/vnd.ms-powerpoint",
|
139
|
+
"ps" => "application/postscript",
|
140
|
+
"qt" => "video/quicktime",
|
141
|
+
"ras" => "image/x-cmu-raster",
|
142
|
+
"rb" => "text/plain",
|
143
|
+
"rd" => "text/plain",
|
144
|
+
"rtf" => "application/rtf",
|
145
|
+
"sgm" => "text/sgml",
|
146
|
+
"sgml" => "text/sgml",
|
147
|
+
"tif" => "image/tiff",
|
148
|
+
"tiff" => "image/tiff",
|
149
|
+
"txt" => "text/plain",
|
150
|
+
"xbm" => "image/x-xbitmap",
|
151
|
+
"xls" => "application/vnd.ms-excel",
|
152
|
+
"xml" => "text/xml",
|
153
|
+
"xpm" => "image/x-xpixmap",
|
154
|
+
"xwd" => "image/x-xwindowdump",
|
155
|
+
"zip" => "application/zip",
|
156
|
+
}
|
157
|
+
end
|
158
|
+
end
|
data/lib/rack/file.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
module Rack
|
2
4
|
# Rack::File serves files below the +root+ given, according to the
|
3
5
|
# path info of the Rack request.
|
@@ -21,7 +23,9 @@ module Rack
|
|
21
23
|
|
22
24
|
def _call(env)
|
23
25
|
if env["PATH_INFO"].include? ".."
|
24
|
-
|
26
|
+
body = "Forbidden\n"
|
27
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
28
|
+
return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
|
25
29
|
end
|
26
30
|
|
27
31
|
@path = F.join(@root, Utils.unescape(env["PATH_INFO"]))
|
@@ -29,13 +33,14 @@ module Rack
|
|
29
33
|
|
30
34
|
if F.file?(@path) && F.readable?(@path)
|
31
35
|
[200, {
|
32
|
-
"Last-Modified" => F.mtime(@path).
|
36
|
+
"Last-Modified" => F.mtime(@path).httpdate,
|
33
37
|
"Content-Type" => MIME_TYPES[ext] || "text/plain",
|
34
38
|
"Content-Length" => F.size(@path).to_s
|
35
39
|
}, self]
|
36
40
|
else
|
37
|
-
|
38
|
-
|
41
|
+
body = "File not found: #{env["PATH_INFO"]}\n"
|
42
|
+
size = body.respond_to?(:bytesize) ? body.bytesize : body.size
|
43
|
+
[404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
|
39
44
|
end
|
40
45
|
end
|
41
46
|
|
@@ -48,7 +53,7 @@ module Rack
|
|
48
53
|
end
|
49
54
|
|
50
55
|
# :stopdoc:
|
51
|
-
# From WEBrick.
|
56
|
+
# From WEBrick with some additions.
|
52
57
|
MIME_TYPES = {
|
53
58
|
"ai" => "application/postscript",
|
54
59
|
"asc" => "text/plain",
|
@@ -77,6 +82,7 @@ module Rack
|
|
77
82
|
"lha" => "application/octet-stream",
|
78
83
|
"lzh" => "application/octet-stream",
|
79
84
|
"mov" => "video/quicktime",
|
85
|
+
"mp3" => "audio/mpeg",
|
80
86
|
"mpe" => "video/mpeg",
|
81
87
|
"mpeg" => "video/mpeg",
|
82
88
|
"mpg" => "video/mpeg",
|
data/lib/rack/handler.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rack
|
2
|
+
# *Handlers* connect web servers with Rack.
|
3
|
+
#
|
4
|
+
# Rack includes Handlers for Mongrel, WEBrick, FastCGI, CGI, SCGI
|
5
|
+
# and LiteSpeed.
|
6
|
+
#
|
7
|
+
# Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>.
|
8
|
+
# A second optional hash can be passed to include server-specific
|
9
|
+
# configuration.
|
10
|
+
module Handler
|
11
|
+
def self.get(server)
|
12
|
+
return unless server
|
13
|
+
|
14
|
+
if klass = @handlers[server]
|
15
|
+
obj = Object
|
16
|
+
klass.split("::").each { |x| obj = obj.const_get(x) }
|
17
|
+
obj
|
18
|
+
else
|
19
|
+
Rack::Handler.const_get(server.capitalize)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.register(server, klass)
|
24
|
+
@handlers ||= {}
|
25
|
+
@handlers[server] = klass
|
26
|
+
end
|
27
|
+
|
28
|
+
autoload :CGI, "rack/handler/cgi"
|
29
|
+
autoload :FastCGI, "rack/handler/fastcgi"
|
30
|
+
autoload :Mongrel, "rack/handler/mongrel"
|
31
|
+
autoload :EventedMongrel, "rack/handler/evented_mongrel"
|
32
|
+
autoload :WEBrick, "rack/handler/webrick"
|
33
|
+
autoload :LSWS, "rack/handler/lsws"
|
34
|
+
autoload :SCGI, "rack/handler/scgi"
|
35
|
+
|
36
|
+
register 'cgi', 'Rack::Handler::CGI'
|
37
|
+
register 'fastcgi', 'Rack::Handler::FastCGI'
|
38
|
+
register 'mongrel', 'Rack::Handler::Mongrel'
|
39
|
+
register 'emongrel', 'Rack::Handler::EventedMongrel'
|
40
|
+
register 'webrick', 'Rack::Handler::WEBrick'
|
41
|
+
register 'lsws', 'Rack::Handler::LSWS'
|
42
|
+
register 'scgi', 'Rack::Handler::SCGI'
|
43
|
+
end
|
44
|
+
end
|
data/lib/rack/handler/fastcgi.rb
CHANGED
data/lib/rack/handler/mongrel.rb
CHANGED
@@ -7,7 +7,27 @@ module Rack
|
|
7
7
|
def self.run(app, options={})
|
8
8
|
server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
|
9
9
|
options[:Port] || 8080)
|
10
|
-
|
10
|
+
# Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
|
11
|
+
# Use is similar to #run, replacing the app argument with a hash of
|
12
|
+
# { path=>app, ... } or an instance of Rack::URLMap.
|
13
|
+
if options[:map]
|
14
|
+
if app.is_a? Hash
|
15
|
+
app.each do |path, appl|
|
16
|
+
path = '/'+path unless path[0] == ?/
|
17
|
+
server.register(path, Rack::Handler::Mongrel.new(appl))
|
18
|
+
end
|
19
|
+
elsif app.is_a? URLMap
|
20
|
+
app.instance_variable_get(:@mapping).each do |(host, path, appl)|
|
21
|
+
next if !host.nil? && !options[:Host].nil? && options[:Host] != host
|
22
|
+
path = '/'+path unless path[0] == ?/
|
23
|
+
server.register(path, Rack::Handler::Mongrel.new(appl))
|
24
|
+
end
|
25
|
+
else
|
26
|
+
raise ArgumentError, "first argument should be a Hash or URLMap"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
server.register('/', Rack::Handler::Mongrel.new(app))
|
30
|
+
end
|
11
31
|
yield server if block_given?
|
12
32
|
server.run.join
|
13
33
|
end
|
data/lib/rack/lint.rb
CHANGED
@@ -29,7 +29,11 @@ module Rack
|
|
29
29
|
|
30
30
|
## A Rack application is an Ruby object (not a class) that
|
31
31
|
## responds to +call+.
|
32
|
-
def call(env=nil)
|
32
|
+
def call(env=nil)
|
33
|
+
dup._call(env)
|
34
|
+
end
|
35
|
+
|
36
|
+
def _call(env)
|
33
37
|
## It takes exactly one argument, the *environment*
|
34
38
|
assert("No env given") { env }
|
35
39
|
check_env env
|
@@ -57,7 +61,7 @@ module Rack
|
|
57
61
|
env.instance_of? Hash
|
58
62
|
}
|
59
63
|
|
60
|
-
##
|
64
|
+
##
|
61
65
|
## The environment is required to include these variables
|
62
66
|
## (adopted from PEP333), except when they'd be empty, but see
|
63
67
|
## below.
|
@@ -115,7 +119,7 @@ module Rack
|
|
115
119
|
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
|
116
120
|
## is reserved for use with the Rack core distribution and must
|
117
121
|
## not be used otherwise.
|
118
|
-
##
|
122
|
+
##
|
119
123
|
|
120
124
|
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
|
121
125
|
QUERY_STRING
|
@@ -141,7 +145,7 @@ module Rack
|
|
141
145
|
}
|
142
146
|
}
|
143
147
|
|
144
|
-
##
|
148
|
+
##
|
145
149
|
## There are the following restrictions:
|
146
150
|
|
147
151
|
## * <tt>rack.version</tt> must be an array of Integers.
|
@@ -301,14 +305,16 @@ module Rack
|
|
301
305
|
|
302
306
|
## === The Status
|
303
307
|
def check_status(status)
|
304
|
-
## The status, if parsed as integer (+to_i+), must be
|
305
|
-
assert("Status must be
|
308
|
+
## The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
|
309
|
+
assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
|
306
310
|
end
|
307
311
|
|
308
312
|
## === The Headers
|
309
313
|
def check_headers(header)
|
310
314
|
## The header must respond to each, and yield values of key and value.
|
311
|
-
assert("
|
315
|
+
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
|
316
|
+
header.respond_to? :each
|
317
|
+
}
|
312
318
|
header.each { |key, value|
|
313
319
|
## The header keys must be Strings.
|
314
320
|
assert("header key must be a string, was #{key.class}") {
|
@@ -323,12 +329,13 @@ module Rack
|
|
323
329
|
## but only contain keys that consist of
|
324
330
|
## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
|
325
331
|
assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
|
326
|
-
##
|
332
|
+
##
|
327
333
|
## The values of the header must respond to #each.
|
328
|
-
assert("header values must respond to #each
|
334
|
+
assert("header values must respond to #each, but the value of " +
|
335
|
+
"'#{key}' doesn't (is #{value.class})") { value.respond_to? :each }
|
329
336
|
value.each { |item|
|
330
337
|
## The values passed on #each must be Strings
|
331
|
-
assert("header values must consist of Strings") {
|
338
|
+
assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") {
|
332
339
|
item.instance_of?(String)
|
333
340
|
}
|
334
341
|
## and not contain characters below 037.
|
@@ -353,7 +360,7 @@ module Rack
|
|
353
360
|
end
|
354
361
|
}
|
355
362
|
assert("No Content-Type header found") {
|
356
|
-
[
|
363
|
+
[204, 304].include? status.to_i
|
357
364
|
}
|
358
365
|
end
|
359
366
|
|
@@ -368,11 +375,11 @@ module Rack
|
|
368
375
|
}
|
369
376
|
yield part
|
370
377
|
}
|
371
|
-
##
|
378
|
+
##
|
372
379
|
## If the Body responds to #close, it will be called after iteration.
|
373
380
|
# XXX howto: assert("Body has not been closed") { @closed }
|
374
381
|
|
375
|
-
##
|
382
|
+
##
|
376
383
|
## The Body commonly is an Array of Strings, the application
|
377
384
|
## instance itself, or a File-like object.
|
378
385
|
end
|