summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScytrin dai Kinthra <scytrin@gmail.com>2008-08-07 03:01:31 -0700
committerScytrin dai Kinthra <scytrin@gmail.com>2008-08-07 03:01:31 -0700
commit90c0938c76c77353e480960bdcfeefc26ac24dbe (patch)
tree13c93041aebd385c4592b13b36b529c9f05739e7
parent835cfd6860864d0d0d559b3e997544aeb374dc00 (diff)
parentf58c3a4cdf6192dcce880e722b3398049066a571 (diff)
downloadrack-90c0938c76c77353e480960bdcfeefc26ac24dbe.tar.gz
Merge commit 'core/master'
-rw-r--r--lib/rack.rb1
-rw-r--r--lib/rack/auth/digest/params.rb4
-rw-r--r--lib/rack/deflater.rb63
-rw-r--r--lib/rack/directory.rb11
-rw-r--r--lib/rack/file.rb9
-rw-r--r--lib/rack/lint.rb93
-rw-r--r--lib/rack/request.rb12
-rw-r--r--lib/rack/showstatus.rb4
-rw-r--r--lib/rack/utils.rb30
-rw-r--r--test/spec_rack_deflater.rb70
-rw-r--r--test/spec_rack_directory.rb2
-rw-r--r--test/spec_rack_handler.rb2
-rw-r--r--test/spec_rack_lint.rb73
-rw-r--r--test/spec_rack_request.rb19
-rw-r--r--test/spec_rack_showstatus.rb9
-rw-r--r--test/spec_rack_utils.rb29
-rw-r--r--test/testrequest.rb4
17 files changed, 376 insertions, 59 deletions
diff --git a/lib/rack.rb b/lib/rack.rb
index 607d0f5c..933f5dcb 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -30,6 +30,7 @@ module Rack
autoload :Cascade, "rack/cascade"
autoload :CommonLogger, "rack/commonlogger"
autoload :File, "rack/file"
+ autoload :Deflater, "rack/deflater"
autoload :Directory, "rack/directory"
autoload :ForwardRequest, "rack/recursive"
autoload :Handler, "rack/handler"
diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb
index b4dcffd0..730e2efd 100644
--- a/lib/rack/auth/digest/params.rb
+++ b/lib/rack/auth/digest/params.rb
@@ -17,8 +17,8 @@ module Rack
ret
end
- def self.split_header_value(str) # From WEBrick::HTTPUtils
- str.scan(/((?:"(?:\\.|[^"])+?"|[^",]+)+)(?:,\s*|\Z)/n).collect{ |v| v[0] }
+ def self.split_header_value(str)
+ str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
end
def initialize
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
new file mode 100644
index 00000000..1341eece
--- /dev/null
+++ b/lib/rack/deflater.rb
@@ -0,0 +1,63 @@
+require "zlib"
+require "stringio"
+
+module Rack
+
+class Deflater
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ status, headers, body = @app.call(env)
+
+ request = Request.new(env)
+
+ encoding = Utils.select_best_encoding(%w(gzip deflate identity), request.accept_encoding)
+
+ case encoding
+ when "gzip"
+ mtime = headers["Last-Modified"] || Time.now
+ [status, headers.merge("Content-Encoding" => "gzip"), self.class.gzip(body, mtime)]
+ when "deflate"
+ [status, headers.merge("Content-Encoding" => "deflate"), self.class.deflate(body)]
+ when "identity"
+ [status, headers, body]
+ when nil
+ message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
+ [406, {"Content-Type" => "text/plain"}, message]
+ end
+ end
+
+ def self.gzip(body, mtime)
+ io = StringIO.new
+ gzip = Zlib::GzipWriter.new(io)
+ gzip.mtime = mtime
+
+ # TODO: Add streaming
+ body.each { |part| gzip << part }
+
+ gzip.close
+ return io.string
+ end
+
+ DEFLATE_ARGS = [
+ Zlib::DEFAULT_COMPRESSION,
+ # drop the zlib header which causes both Safari and IE to choke
+ -Zlib::MAX_WBITS,
+ Zlib::DEF_MEM_LEVEL,
+ Zlib::DEFAULT_STRATEGY
+ ]
+
+ # Loosely based on Mongrel's Deflate handler
+ def self.deflate(body)
+ deflater = Zlib::Deflate.new(*DEFLATE_ARGS)
+
+ # TODO: Add streaming
+ body.each { |part| deflater << part }
+
+ return deflater.finish
+ end
+end
+
+end
diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb
index 972b4bc6..31e0db84 100644
--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -1,3 +1,5 @@
+require 'time'
+
module Rack
# Rack::Directory serves entries below the +root+ given, according to the
# path info of the Rack request. If a directory is found, the file's contents
@@ -51,7 +53,9 @@ table { width:100%%; }
def _call(env)
if env["PATH_INFO"].include? ".."
- return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]]
+ body = "Forbidden\n"
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
end
@path = F.join(@root, Utils.unescape(env['PATH_INFO']))
@@ -77,8 +81,9 @@ table { width:100%%; }
end
end
- return [404, {"Content-Type" => "text/plain"},
- ["Entity not found: #{env["PATH_INFO"]}\n"]]
+ body = "Entity not found: #{env["PATH_INFO"]}\n"
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
end
def each
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 7538fd69..afb21383 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -23,7 +23,9 @@ module Rack
def _call(env)
if env["PATH_INFO"].include? ".."
- return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]]
+ body = "Forbidden\n"
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]]
end
@path = F.join(@root, Utils.unescape(env["PATH_INFO"]))
@@ -36,8 +38,9 @@ module Rack
"Content-Length" => F.size(@path).to_s
}, self]
else
- return [404, {"Content-Type" => "text/plain"},
- ["File not found: #{env["PATH_INFO"]}\n"]]
+ body = "File not found: #{env["PATH_INFO"]}\n"
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]]
end
end
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index b5232e00..2b81f10d 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -3,6 +3,8 @@ module Rack
# responses according to the Rack spec.
class Lint
+ STATUS_WITH_NO_ENTITY_BODY = (100..199).to_a << 204 << 304
+
def initialize(app)
@app = app
end
@@ -29,7 +31,11 @@ module Rack
## A Rack application is an Ruby object (not a class) that
## responds to +call+.
- def call(env=nil)
+ def call(env=nil)
+ dup._call(env)
+ end
+
+ def _call(env)
## It takes exactly one argument, the *environment*
assert("No env given") { env }
check_env env
@@ -45,6 +51,7 @@ module Rack
check_headers headers
## and the *body*.
check_content_type status, headers
+ check_content_length status, headers
[status, headers, self]
end
@@ -57,7 +64,7 @@ module Rack
env.instance_of? Hash
}
- ##
+ ##
## The environment is required to include these variables
## (adopted from PEP333), except when they'd be empty, but see
## below.
@@ -115,7 +122,7 @@ module Rack
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
## is reserved for use with the Rack core distribution and must
## not be used otherwise.
- ##
+ ##
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
QUERY_STRING
@@ -141,7 +148,7 @@ module Rack
}
}
- ##
+ ##
## There are the following restrictions:
## * <tt>rack.version</tt> must be an array of Integers.
@@ -301,14 +308,16 @@ module Rack
## === The Status
def check_status(status)
- ## The status, if parsed as integer (+to_i+), must be bigger than 100.
- assert("Status must be >100 seen as integer") { status.to_i > 100 }
+ ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
+ assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
end
## === The Headers
def check_headers(header)
## The header must respond to each, and yield values of key and value.
- assert("header should respond to #each") { header.respond_to? :each }
+ assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
+ header.respond_to? :each
+ }
header.each { |key, value|
## The header keys must be Strings.
assert("header key must be a string, was #{key.class}") {
@@ -323,12 +332,13 @@ module Rack
## but only contain keys that consist of
## letters, digits, <tt>_</tt> or <tt>-</tt> and start with a letter.
assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ }
- ##
+ ##
## The values of the header must respond to #each.
- assert("header values must respond to #each") { value.respond_to? :each }
+ assert("header values must respond to #each, but the value of " +
+ "'#{key}' doesn't (is #{value.class})") { value.respond_to? :each }
value.each { |item|
## The values passed on #each must be Strings
- assert("header values must consist of Strings") {
+ assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") {
item.instance_of?(String)
}
## and not contain characters below 037.
@@ -343,18 +353,69 @@ module Rack
def check_content_type(status, headers)
headers.each { |key, value|
## There must be a <tt>Content-Type</tt>, except when the
- ## +Status+ is 204 or 304, in which case there must be none
+ ## +Status+ is 1xx, 204 or 304, in which case there must be none
## given.
if key.downcase == "content-type"
- assert("Content-Type header found in #{status} response, not allowed"){
- not [204, 304].include? status.to_i
+ assert("Content-Type header found in #{status} response, not allowed") {
+ not STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
}
return
end
}
assert("No Content-Type header found") {
- [201, 204, 304].include? status.to_i
+ STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+ end
+
+ ## === The Content-Length
+ def check_content_length(status, headers)
+ chunked_response = false
+ headers.each { |key, value|
+ if key.downcase == 'transfer-encoding'
+ chunked_response = value.downcase != 'identity'
+ end
+ }
+
+ headers.each { |key, value|
+ if key.downcase == 'content-length'
+ ## There must be a <tt>Content-Length</tt>, except when the
+ ## +Status+ is 1xx, 204 or 304, in which case there must be none
+ ## given.
+ assert("Content-Length header found in #{status} response, not allowed") {
+ not STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+
+ assert('Content-Length header should not be used if body is chunked') {
+ not chunked_response
+ }
+
+ bytes = 0
+ string_body = true
+
+ @body.each { |part|
+ unless part.kind_of?(String)
+ string_body = false
+ break
+ end
+
+ bytes += (part.respond_to?(:bytesize) ? part.bytesize : part.size)
+ }
+
+ if string_body
+ assert("Content-Length header was #{value}, but should be #{bytes}") {
+ value == bytes.to_s
+ }
+ end
+
+ return
+ end
}
+
+ if [ String, Array ].include?(@body.class) && !chunked_response
+ assert('No Content-Length header found') {
+ STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
+ }
+ end
end
## === The Body
@@ -368,11 +429,11 @@ module Rack
}
yield part
}
- ##
+ ##
## If the Body responds to #close, it will be called after iteration.
# XXX howto: assert("Body has not been closed") { @closed }
- ##
+ ##
## The Body commonly is an Array of Strings, the application
## instance itself, or a File-like object.
end
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 68c11198..2a9bcc15 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -193,5 +193,17 @@ module Rack
path << "?" << query_string unless query_string.empty?
path
end
+
+ def accept_encoding
+ @env["HTTP_ACCEPT_ENCODING"].to_s.split(/,\s*/).map do |part|
+ m = /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) # From WEBrick
+
+ if m
+ [m[1], (m[2] || 1.0).to_f]
+ else
+ raise "Invalid value for Accept-Encoding: #{part.inspect}"
+ end
+ end
+ end
end
end
diff --git a/lib/rack/showstatus.rb b/lib/rack/showstatus.rb
index 8aa1d441..ca81f7d8 100644
--- a/lib/rack/showstatus.rb
+++ b/lib/rack/showstatus.rb
@@ -25,7 +25,9 @@ module Rack
req = Rack::Request.new(env)
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
detail = env["rack.showstatus.detail"] || message
- [status, headers.merge("Content-Type" => "text/html"), [@template.result(binding)]]
+ body = @template.result(binding)
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]]
else
[status, headers, body]
end
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 4329b988..25254bbd 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -71,6 +71,36 @@ module Rack
end
module_function :escape_html
+ def select_best_encoding(available_encodings, accept_encoding)
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+
+ expanded_accept_encoding =
+ accept_encoding.map { |m, q|
+ if m == "*"
+ (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
+ else
+ [[m, q]]
+ end
+ }.inject([]) { |mem, list|
+ mem + list
+ }
+
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
+
+ unless encoding_candidates.include?("identity")
+ encoding_candidates.push("identity")
+ end
+
+ expanded_accept_encoding.find_all { |m, q|
+ q == 0.0
+ }.each { |m, _|
+ encoding_candidates.delete(m)
+ }
+
+ return (encoding_candidates & available_encodings)[0]
+ end
+ module_function :select_best_encoding
+
# The recommended manner in which to implement a contexting application
# is to define a method #context in which a new Context is instantiated.
#
diff --git a/test/spec_rack_deflater.rb b/test/spec_rack_deflater.rb
new file mode 100644
index 00000000..db75d392
--- /dev/null
+++ b/test/spec_rack_deflater.rb
@@ -0,0 +1,70 @@
+require 'test/spec'
+
+require 'rack/mock'
+require 'rack/deflater'
+require 'stringio'
+
+context "Rack::Deflater" do
+ def build_response(body, accept_encoding, headers = {})
+ app = lambda { |env| [200, {}, body] }
+ request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding))
+ response = Rack::Deflater.new(app).call(request)
+
+ return response
+ end
+
+ specify "should be able to deflate bodies that respond to each" do
+ body = Object.new
+ class << body; def each; yield("foo"); yield("bar"); end; end
+
+ response = build_response(body, "deflate")
+
+ response[0].should.equal(200)
+ response[1].should.equal({ "Content-Encoding" => "deflate" })
+ response[2].to_s.should.equal("K\313\317OJ,\002\000")
+ end
+
+ # TODO: This is really just a special case of the above...
+ specify "should be able to deflate String bodies" do
+ response = build_response("Hello world!", "deflate")
+
+ response[0].should.equal(200)
+ response[1].should.equal({ "Content-Encoding" => "deflate" })
+ response[2].to_s.should.equal("\363H\315\311\311W(\317/\312IQ\004\000")
+ end
+
+ specify "should be able to gzip bodies that respond to each" do
+ body = Object.new
+ class << body; def each; yield("foo"); yield("bar"); end; end
+
+ response = build_response(body, "gzip")
+
+ response[0].should.equal(200)
+ response[1].should.equal({ "Content-Encoding" => "gzip" })
+
+ io = StringIO.new(response[2].to_s)
+ gz = Zlib::GzipReader.new(io)
+ gz.read.should.equal("foobar")
+ gz.close
+ end
+
+ specify "should be able to fallback to no deflation" do
+ response = build_response("Hello world!", "superzip")
+
+ response[0].should.equal(200)
+ response[1].should.equal({})
+ response[2].should.equal("Hello world!")
+ end
+
+ specify "should handle the lack of an acceptable encoding" do
+ response1 = build_response("Hello world!", "identity;q=0", "PATH_INFO" => "/")
+ response1[0].should.equal(406)
+ response1[1].should.equal({"Content-Type" => "text/plain"})
+ response1[2].should.equal("An acceptable encoding for the requested resource / could not be found.")
+
+ response2 = build_response("Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar")
+ response2[0].should.equal(406)
+ response2[1].should.equal({"Content-Type" => "text/plain"})
+ response2[2].should.equal("An acceptable encoding for the requested resource /foo/bar could not be found.")
+ end
+end
diff --git a/test/spec_rack_directory.rb b/test/spec_rack_directory.rb
index c791b197..bf0b8794 100644
--- a/test/spec_rack_directory.rb
+++ b/test/spec_rack_directory.rb
@@ -7,7 +7,7 @@ require 'rack/mock'
context "Rack::Directory" do
DOCROOT = File.expand_path(File.dirname(__FILE__))
- FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain'}, 'passed!'] }
+ FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, 'passed!'] }
app = Rack::Directory.new DOCROOT, FILE_CATCH
specify "serves directory indices" do
diff --git a/test/spec_rack_handler.rb b/test/spec_rack_handler.rb
index be188afb..e961d2b7 100644
--- a/test/spec_rack_handler.rb
+++ b/test/spec_rack_handler.rb
@@ -13,7 +13,7 @@ context "Rack::Handler" do
Rack::Handler.get('webrick').should.equal Rack::Handler::WEBrick
end
- specify "should get unregisted hanlder by name" do
+ specify "should get unregistered handler by name" do
Rack::Handler.get('lobster').should.equal Rack::Handler::Lobster
end
diff --git a/test/spec_rack_lint.rb b/test/spec_rack_lint.rb
index e863559c..a8112b85 100644
--- a/test/spec_rack_lint.rb
+++ b/test/spec_rack_lint.rb
@@ -8,11 +8,11 @@ context "Rack::Lint" do
def env(*args)
Rack::MockRequest.env_for("/", *args)
end
-
+
specify "passes valid request" do
lambda {
Rack::Lint.new(lambda { |env|
- [200, {"Content-type" => "test/plain"}, "foo"]
+ [200, {"Content-type" => "test/plain", "Content-length" => "3"}, "foo"]
}).call(env({}))
}.should.not.raise
end
@@ -120,14 +120,14 @@ context "Rack::Lint" do
["cc", {}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/must be >100 seen as integer/)
+ message.should.match(/must be >=100 seen as integer/)
lambda {
Rack::Lint.new(lambda { |env|
[42, {}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/must be >100 seen as integer/)
+ message.should.match(/must be >=100 seen as integer/)
end
specify "notices header errors" do
@@ -136,14 +136,14 @@ context "Rack::Lint" do
[200, Object.new, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/should respond to #each/)
+ message.should.equal("headers object should respond to #each, but doesn't (got Object as headers)")
lambda {
Rack::Lint.new(lambda { |env|
[200, {true=>false}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/header key must be a string/)
+ message.should.equal("header key must be a string, was TrueClass")
lambda {
Rack::Lint.new(lambda { |env|
@@ -171,21 +171,21 @@ context "Rack::Lint" do
[200, {"..%%quark%%.." => "text/plain"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/invalid header/)
+ message.should.equal("invalid header name: ..%%quark%%..")
lambda {
Rack::Lint.new(lambda { |env|
[200, {"Foo" => Object.new}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/must respond to #each/)
+ message.should.equal("header values must respond to #each, but the value of 'Foo' doesn't (is Object)")
lambda {
Rack::Lint.new(lambda { |env|
[200, {"Foo" => [1,2,3]}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/must consist of Strings/)
+ message.should.equal("header values must consist of Strings, but 'Foo' also contains a Fixnum")
lambda {
@@ -199,30 +199,57 @@ context "Rack::Lint" do
specify "notices content-type errors" do
lambda {
Rack::Lint.new(lambda { |env|
- [200, {}, ""]
+ [200, {"Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/No Content-Type/)
+ [100, 101, 204, 304].each do |status|
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [status, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Content-Type header found/)
+ end
+ end
+
+ specify "notices content-length errors" do
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [200, {"Content-type" => "text/plain"}, ""]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/No Content-Length/)
+
+ [100, 101, 204, 304].each do |status|
+ lambda {
+ Rack::Lint.new(lambda { |env|
+ [status, {"Content-length" => "0"}, ""]
+ }).call(env({}))
+ }.should.raise(Rack::Lint::LintError).
+ message.should.match(/Content-Length header found/)
+ end
+
lambda {
Rack::Lint.new(lambda { |env|
- [204, {"Content-Type" => "text/plain"}, ""]
+ [200, {"Content-type" => "text/plain", "Content-Length" => "0", "Transfer-Encoding" => "chunked"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/Content-Type header found/)
+ message.should.match(/Content-Length header should not be used/)
lambda {
Rack::Lint.new(lambda { |env|
- [204, {"Content-type" => "text/plain"}, ""]
+ [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
- message.should.match(/Content-Type header found/)
+ message.should.match(/Content-Length header was 1, but should be 0/)
end
specify "notices body errors" do
lambda {
status, header, body = Rack::Lint.new(lambda { |env|
- [200, {"Content-type" => "text/plain"}, [1,2,3]]
+ [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]]
}).call(env({}))
body.each { |part| }
}.should.raise(Rack::Lint::LintError).
@@ -233,7 +260,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].gets("\r\n")
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/gets called with arguments/)
@@ -241,7 +268,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read("foo")
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/read called with non-integer argument/)
@@ -265,7 +292,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].gets
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env("rack.input" => weirdio))
}.should.raise(Rack::Lint::LintError).
message.should.match(/gets didn't return a String/)
@@ -273,7 +300,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].each { |x| }
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env("rack.input" => weirdio))
}.should.raise(Rack::Lint::LintError).
message.should.match(/each didn't yield a String/)
@@ -281,7 +308,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].read
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env("rack.input" => weirdio))
}.should.raise(Rack::Lint::LintError).
message.should.match(/read didn't return a String/)
@@ -290,7 +317,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.input"].close
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/close must not be called/)
@@ -300,7 +327,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.errors"].write(42)
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/write not called with a String/)
@@ -308,7 +335,7 @@ context "Rack::Lint" do
lambda {
Rack::Lint.new(lambda { |env|
env["rack.errors"].close
- [201, {"Content-type" => "text/plain"}, ""]
+ [201, {"Content-type" => "text/plain", "Content-length" => "0"}, ""]
}).call(env({}))
}.should.raise(Rack::Lint::LintError).
message.should.match(/close must not be called/)
diff --git a/test/spec_rack_request.rb b/test/spec_rack_request.rb
index 4321516e..0b2c9fc6 100644
--- a/test/spec_rack_request.rb
+++ b/test/spec_rack_request.rb
@@ -359,7 +359,8 @@ EOF
specify "does conform to the Rack spec" do
app = lambda { |env|
content = Rack::Request.new(env).POST["file"].inspect
- [200, {"Content-Type" => "text/html"}, content]
+ size = content.respond_to?(:bytesize) ? content.bytesize : content.size
+ [200, {"Content-Type" => "text/html", "Content-Length" => size.to_s}, content]
}
input = <<EOF
@@ -381,4 +382,20 @@ EOF
res.should.be.ok
end
+
+ specify "should parse Accept-Encoding correctly" do
+ parser = lambda do |x|
+ Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => x)).accept_encoding
+ end
+
+ parser.call(nil).should.equal([])
+
+ parser.call("compress, gzip").should.equal([["compress", 1.0], ["gzip", 1.0]])
+ parser.call("").should.equal([])
+ parser.call("*").should.equal([["*", 1.0]])
+ parser.call("compress;q=0.5, gzip;q=1.0").should.equal([["compress", 0.5], ["gzip", 1.0]])
+ parser.call("gzip;q=1.0, identity; q=0.5, *;q=0").should.equal([["gzip", 1.0], ["identity", 0.5], ["*", 0] ])
+
+ lambda { parser.call("gzip ; q=1.0") }.should.raise(RuntimeError)
+ end
end
diff --git a/test/spec_rack_showstatus.rb b/test/spec_rack_showstatus.rb
index 03e57032..78700134 100644
--- a/test/spec_rack_showstatus.rb
+++ b/test/spec_rack_showstatus.rb
@@ -6,7 +6,7 @@ require 'rack/mock'
context "Rack::ShowStatus" do
specify "should provide a default status message" do
req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
- [404, {"Content-Type" => "text/plain"}, []]
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
}))
res = req.get("/", :lint => true)
@@ -21,7 +21,7 @@ context "Rack::ShowStatus" do
specify "should let the app provide additional information" do
req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
env["rack.showstatus.detail"] = "gone too meta."
- [404, {"Content-Type" => "text/plain"}, []]
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
}))
res = req.get("/", :lint => true)
@@ -36,7 +36,7 @@ context "Rack::ShowStatus" do
specify "should not replace existing messages" do
req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
- [404, {"Content-Type" => "text/plain"}, ["foo!"]]
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
}))
res = req.get("/", :lint => true)
res.should.be.not_found
@@ -56,7 +56,7 @@ context "Rack::ShowStatus" do
specify "should replace existing messages if there is detail" do
req = Rack::MockRequest.new(Rack::ShowStatus.new(lambda { |env|
env["rack.showstatus.detail"] = "gone too meta."
- [404, {"Content-Type" => "text/plain"}, ["foo!"]]
+ [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
}))
res = req.get("/", :lint => true)
@@ -64,6 +64,7 @@ context "Rack::ShowStatus" do
res.should.be.not.empty
res["Content-Type"].should.equal("text/html")
+ res["Content-Length"].should.not.equal("4")
res.should =~ /404/
res.should =~ /too meta/
res.body.should.not =~ /foo/
diff --git a/test/spec_rack_utils.rb b/test/spec_rack_utils.rb
index 8256e12f..0ec0a39f 100644
--- a/test/spec_rack_utils.rb
+++ b/test/spec_rack_utils.rb
@@ -20,7 +20,7 @@ context "Rack::Utils" do
should.equal "q1!2\"'w$5&7/z8)?\\"
end
- specify "should parse queries correctly" do
+ specify "should parse query strings correctly" do
Rack::Utils.parse_query("foo=bar").should.equal "foo" => "bar"
Rack::Utils.parse_query("foo=bar&foo=quux").
should.equal "foo" => ["bar", "quux"]
@@ -30,7 +30,7 @@ context "Rack::Utils" do
should.equal "my weird field" => "q1!2\"'w$5&7/z8)?"
end
- specify "should create queries correctly" do
+ specify "should build query strings correctly" do
Rack::Utils.build_query("foo" => "bar").should.equal "foo=bar"
Rack::Utils.build_query("foo" => ["bar", "quux"]).
should.equal "foo=bar&foo=quux"
@@ -39,6 +39,29 @@ context "Rack::Utils" do
Rack::Utils.build_query("my weird field" => "q1!2\"'w$5&7/z8)?").
should.equal "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F"
end
+
+ specify "should figure out which encodings are acceptable" do
+ helper = lambda do |a, b|
+ request = Rack::Request.new(Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => a))
+ Rack::Utils.select_best_encoding(a, b)
+ end
+
+ helper.call(%w(), [["x", 1]]).should.equal(nil)
+ helper.call(%w(identity), [["identity", 0.0]]).should.equal(nil)
+ helper.call(%w(identity), [["*", 0.0]]).should.equal(nil)
+
+ helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("identity")
+
+ helper.call(%w(compress gzip identity), [["compress", 1.0], ["gzip", 1.0]]).should.equal("compress")
+ helper.call(%w(compress gzip identity), [["compress", 0.5], ["gzip", 1.0]]).should.equal("gzip")
+
+ helper.call(%w(foo bar identity), []).should.equal("identity")
+ helper.call(%w(foo bar identity), [["*", 1.0]]).should.equal("foo")
+ helper.call(%w(foo bar identity), [["*", 1.0], ["foo", 0.9]]).should.equal("bar")
+
+ helper.call(%w(foo bar identity), [["foo", 0], ["bar", 0]]).should.equal("identity")
+ helper.call(%w(foo bar baz identity), [["*", 0], ["identity", 0.1]]).should.equal("identity")
+ end
end
context "Rack::Utils::HeaderHash" do
@@ -85,7 +108,7 @@ context "Rack::Utils::Context" do
test_target1 = proc{|e| e.to_s+' world' }
test_target2 = proc{|e| e.to_i+2 }
test_target3 = proc{|e| nil }
- test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain'},['']] }
+ test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain', 'Content-Length'=>'0'},['']] }
test_target5 = Object.new
specify "should perform checks on both arguments" do
diff --git a/test/testrequest.rb b/test/testrequest.rb
index 1b045ea7..348cd495 100644
--- a/test/testrequest.rb
+++ b/test/testrequest.rb
@@ -5,7 +5,9 @@ class TestRequest
def call(env)
status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
env["test.postdata"] = env["rack.input"].read
- [status, {"Content-Type" => "text/yaml"}, [env.to_yaml]]
+ body = env.to_yaml
+ size = body.respond_to?(:bytesize) ? body.bytesize : body.size
+ [status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]]
end
module Helpers