rack 1.2.8 → 1.3.0.beta
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/README +9 -177
- data/Rakefile +2 -1
- data/SPEC +2 -2
- data/lib/rack.rb +2 -13
- data/lib/rack/auth/abstract/request.rb +7 -5
- data/lib/rack/auth/digest/md5.rb +6 -2
- data/lib/rack/auth/digest/params.rb +5 -7
- data/lib/rack/auth/digest/request.rb +1 -1
- data/lib/rack/backports/uri/common.rb +64 -0
- data/lib/rack/builder.rb +60 -3
- data/lib/rack/chunked.rb +29 -22
- data/lib/rack/conditionalget.rb +35 -16
- data/lib/rack/content_length.rb +3 -3
- data/lib/rack/deflater.rb +5 -2
- data/lib/rack/etag.rb +38 -10
- data/lib/rack/file.rb +76 -43
- data/lib/rack/handler.rb +13 -7
- data/lib/rack/handler/cgi.rb +0 -2
- data/lib/rack/handler/fastcgi.rb +13 -4
- data/lib/rack/handler/lsws.rb +0 -2
- data/lib/rack/handler/mongrel.rb +12 -2
- data/lib/rack/handler/scgi.rb +9 -1
- data/lib/rack/handler/thin.rb +7 -1
- data/lib/rack/handler/webrick.rb +12 -5
- data/lib/rack/lint.rb +2 -2
- data/lib/rack/lock.rb +29 -3
- data/lib/rack/methodoverride.rb +1 -1
- data/lib/rack/mime.rb +2 -2
- data/lib/rack/mock.rb +28 -33
- data/lib/rack/multipart.rb +34 -0
- data/lib/rack/multipart/generator.rb +93 -0
- data/lib/rack/multipart/parser.rb +164 -0
- data/lib/rack/multipart/uploaded_file.rb +30 -0
- data/lib/rack/request.rb +55 -19
- data/lib/rack/response.rb +10 -8
- data/lib/rack/sendfile.rb +14 -18
- data/lib/rack/server.rb +55 -8
- data/lib/rack/session/abstract/id.rb +233 -22
- data/lib/rack/session/cookie.rb +99 -46
- data/lib/rack/session/memcache.rb +30 -56
- data/lib/rack/session/pool.rb +22 -43
- data/lib/rack/showexceptions.rb +40 -11
- data/lib/rack/showstatus.rb +9 -2
- data/lib/rack/static.rb +29 -9
- data/lib/rack/urlmap.rb +6 -1
- data/lib/rack/utils.rb +67 -326
- data/rack.gemspec +2 -3
- data/test/builder/anything.rb +5 -0
- data/test/builder/comment.ru +4 -0
- data/test/builder/end.ru +3 -0
- data/test/builder/options.ru +2 -0
- data/test/cgi/lighttpd.conf +1 -1
- data/test/cgi/lighttpd.errors +412 -0
- data/test/multipart/content_type_and_no_filename +6 -0
- data/test/multipart/text +5 -0
- data/test/multipart/webkit +32 -0
- data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
- data/test/spec_auth_digest.rb +20 -5
- data/test/spec_builder.rb +29 -0
- data/test/spec_cgi.rb +11 -0
- data/test/spec_chunked.rb +1 -1
- data/test/spec_commonlogger.rb +1 -1
- data/test/spec_conditionalget.rb +47 -0
- data/test/spec_content_length.rb +0 -6
- data/test/spec_content_type.rb +5 -5
- data/test/spec_deflater.rb +46 -2
- data/test/spec_etag.rb +68 -1
- data/test/spec_fastcgi.rb +11 -0
- data/test/spec_file.rb +54 -3
- data/test/spec_handler.rb +23 -5
- data/test/spec_lint.rb +2 -2
- data/test/spec_lock.rb +111 -5
- data/test/spec_methodoverride.rb +2 -2
- data/test/spec_mock.rb +3 -3
- data/test/spec_mongrel.rb +1 -2
- data/test/spec_multipart.rb +279 -0
- data/test/spec_request.rb +222 -38
- data/test/spec_response.rb +9 -3
- data/test/spec_server.rb +74 -0
- data/test/spec_session_abstract_id.rb +43 -0
- data/test/spec_session_cookie.rb +97 -15
- data/test/spec_session_memcache.rb +60 -50
- data/test/spec_session_pool.rb +63 -40
- data/test/spec_showexceptions.rb +64 -0
- data/test/spec_static.rb +23 -0
- data/test/spec_utils.rb +65 -351
- data/test/spec_webrick.rb +23 -4
- metadata +35 -15
- data/test/spec_auth.rb +0 -57
data/lib/rack/builder.rb
CHANGED
@@ -4,14 +4,17 @@ module Rack
|
|
4
4
|
#
|
5
5
|
# Example:
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# require 'rack/lobster'
|
8
|
+
# app = Rack::Builder.new do
|
8
9
|
# use Rack::CommonLogger
|
9
10
|
# use Rack::ShowExceptions
|
10
11
|
# map "/lobster" do
|
11
12
|
# use Rack::Lint
|
12
13
|
# run Rack::Lobster.new
|
13
14
|
# end
|
14
|
-
#
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# run app
|
15
18
|
#
|
16
19
|
# Or
|
17
20
|
#
|
@@ -20,6 +23,8 @@ module Rack
|
|
20
23
|
# lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] }
|
21
24
|
# end
|
22
25
|
#
|
26
|
+
# run app
|
27
|
+
#
|
23
28
|
# +use+ adds a middleware to the stack, +run+ dispatches to an application.
|
24
29
|
# You can use +map+ to construct a Rack::URLMap in a convenient way.
|
25
30
|
|
@@ -32,7 +37,7 @@ module Rack
|
|
32
37
|
options = opts.parse! $1.split(/\s+/)
|
33
38
|
end
|
34
39
|
cfgfile.sub!(/^__END__\n.*/, '')
|
35
|
-
app = eval "Rack::Builder.new {
|
40
|
+
app = eval "Rack::Builder.new {\n" + cfgfile + "\n}.to_app",
|
36
41
|
TOPLEVEL_BINDING, config
|
37
42
|
else
|
38
43
|
require config
|
@@ -50,14 +55,66 @@ module Rack
|
|
50
55
|
self.new(&block).to_app
|
51
56
|
end
|
52
57
|
|
58
|
+
# Specifies a middleware to use in a stack.
|
59
|
+
#
|
60
|
+
# class Middleware
|
61
|
+
# def initialize(app)
|
62
|
+
# @app = app
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# def call(env)
|
66
|
+
# env["rack.some_header"] = "setting an example"
|
67
|
+
# @app.call(env)
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# use Middleware
|
72
|
+
# run lambda { |env| [200, { "Content-Type => "text/plain" }, ["OK"]] }
|
73
|
+
#
|
74
|
+
# All requests through to this application will first be processed by the middleware class.
|
75
|
+
# The +call+ method in this example sets an additional environment key which then can be
|
76
|
+
# referenced in the application if required.
|
53
77
|
def use(middleware, *args, &block)
|
54
78
|
@ins << lambda { |app| middleware.new(app, *args, &block) }
|
55
79
|
end
|
56
80
|
|
81
|
+
# Takes an argument that is an object that responds to #call and returns a Rack response.
|
82
|
+
# The simplest form of this is a lambda object:
|
83
|
+
#
|
84
|
+
# run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
|
85
|
+
#
|
86
|
+
# However this could also be a class:
|
87
|
+
#
|
88
|
+
# class Heartbeat
|
89
|
+
# def self.call(env)
|
90
|
+
# [200, { "Content-Type" => "text/plain" }, ["OK"]]
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# run Heartbeat
|
57
95
|
def run(app)
|
58
96
|
@ins << app #lambda { |nothing| app }
|
59
97
|
end
|
60
98
|
|
99
|
+
# Creates a route within the application.
|
100
|
+
#
|
101
|
+
# Rack::Builder.app do
|
102
|
+
# map '/' do
|
103
|
+
# run Heartbeat
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# The +use+ method can also be used here to specify middleware to run under a specific path:
|
108
|
+
#
|
109
|
+
# Rack::Builder.app do
|
110
|
+
# map '/' do
|
111
|
+
# use Middleware
|
112
|
+
# run Heartbeat
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# This example includes a piece of middleware which will run before requests hit +Heartbeat+.
|
117
|
+
#
|
61
118
|
def map(path, &block)
|
62
119
|
if @ins.last.kind_of? Hash
|
63
120
|
@ins.last[path] = self.class.new(&block).to_app
|
data/lib/rack/chunked.rb
CHANGED
@@ -7,6 +7,32 @@ module Rack
|
|
7
7
|
class Chunked
|
8
8
|
include Rack::Utils
|
9
9
|
|
10
|
+
# A body wrapper that emits chunked responses
|
11
|
+
class Body
|
12
|
+
TERM = "\r\n"
|
13
|
+
TAIL = "0#{TERM}#{TERM}"
|
14
|
+
|
15
|
+
include Rack::Utils
|
16
|
+
|
17
|
+
def initialize(body)
|
18
|
+
@body = body
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
term = TERM
|
23
|
+
@body.each do |chunk|
|
24
|
+
size = bytesize(chunk)
|
25
|
+
next if size == 0
|
26
|
+
yield [size.to_s(16), term, chunk, term].join
|
27
|
+
end
|
28
|
+
yield TAIL
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@body.close if @body.respond_to?(:close)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
10
36
|
def initialize(app)
|
11
37
|
@app = app
|
12
38
|
end
|
@@ -21,29 +47,10 @@ module Rack
|
|
21
47
|
headers['Transfer-Encoding']
|
22
48
|
[status, headers, body]
|
23
49
|
else
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
def chunk(status, headers, body)
|
29
|
-
@body = body
|
30
|
-
headers.delete('Content-Length')
|
31
|
-
headers['Transfer-Encoding'] = 'chunked'
|
32
|
-
[status, headers, self]
|
33
|
-
end
|
34
|
-
|
35
|
-
def each
|
36
|
-
term = "\r\n"
|
37
|
-
@body.each do |chunk|
|
38
|
-
size = bytesize(chunk)
|
39
|
-
next if size == 0
|
40
|
-
yield [size.to_s(16), term, chunk, term].join
|
50
|
+
headers.delete('Content-Length')
|
51
|
+
headers['Transfer-Encoding'] = 'chunked'
|
52
|
+
[status, headers, Body.new(body)]
|
41
53
|
end
|
42
|
-
yield ["0", term, "", term].join
|
43
|
-
end
|
44
|
-
|
45
|
-
def close
|
46
|
-
@body.close if @body.respond_to?(:close)
|
47
54
|
end
|
48
55
|
end
|
49
56
|
end
|
data/lib/rack/conditionalget.rb
CHANGED
@@ -20,28 +20,47 @@ module Rack
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def call(env)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
23
|
+
case env['REQUEST_METHOD']
|
24
|
+
when "GET", "HEAD"
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
headers = Utils::HeaderHash.new(headers)
|
27
|
+
if status == 200 && fresh?(env, headers)
|
28
|
+
status = 304
|
29
|
+
headers.delete('Content-Type')
|
30
|
+
headers.delete('Content-Length')
|
31
|
+
body = []
|
32
|
+
end
|
33
|
+
[status, headers, body]
|
34
|
+
else
|
35
|
+
@app.call(env)
|
32
36
|
end
|
33
|
-
[status, headers, body]
|
34
37
|
end
|
35
38
|
|
36
39
|
private
|
37
|
-
|
38
|
-
|
40
|
+
|
41
|
+
def fresh?(env, headers)
|
42
|
+
modified_since = env['HTTP_IF_MODIFIED_SINCE']
|
43
|
+
none_match = env['HTTP_IF_NONE_MATCH']
|
44
|
+
|
45
|
+
return false unless modified_since || none_match
|
46
|
+
|
47
|
+
success = true
|
48
|
+
success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
|
49
|
+
success &&= etag_matches?(none_match, headers) if none_match
|
50
|
+
success
|
39
51
|
end
|
40
52
|
|
41
|
-
def
|
42
|
-
|
43
|
-
last_modified == env['HTTP_IF_MODIFIED_SINCE']
|
53
|
+
def etag_matches?(none_match, headers)
|
54
|
+
etag = headers['ETag'] and etag == none_match
|
44
55
|
end
|
45
|
-
end
|
46
56
|
|
57
|
+
def modified_since?(modified_since, headers)
|
58
|
+
last_modified = to_rfc2822(headers['Last-Modified']) and
|
59
|
+
modified_since >= last_modified
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_rfc2822(since)
|
63
|
+
Time.rfc2822(since) rescue nil
|
64
|
+
end
|
65
|
+
end
|
47
66
|
end
|
data/lib/rack/content_length.rb
CHANGED
@@ -16,10 +16,10 @@ module Rack
|
|
16
16
|
if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
|
17
17
|
!headers['Content-Length'] &&
|
18
18
|
!headers['Transfer-Encoding'] &&
|
19
|
-
|
19
|
+
body.respond_to?(:to_ary)
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
length = 0
|
22
|
+
body.each { |part| length += bytesize(part) }
|
23
23
|
headers['Content-Length'] = length.to_s
|
24
24
|
end
|
25
25
|
|
data/lib/rack/deflater.rb
CHANGED
@@ -60,7 +60,10 @@ module Rack
|
|
60
60
|
@writer = block
|
61
61
|
gzip =::Zlib::GzipWriter.new(self)
|
62
62
|
gzip.mtime = @mtime
|
63
|
-
@body.each { |part|
|
63
|
+
@body.each { |part|
|
64
|
+
gzip.write(part)
|
65
|
+
gzip.flush
|
66
|
+
}
|
64
67
|
@body.close if @body.respond_to?(:close)
|
65
68
|
gzip.close
|
66
69
|
@writer = nil
|
@@ -86,7 +89,7 @@ module Rack
|
|
86
89
|
|
87
90
|
def each
|
88
91
|
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
|
89
|
-
@body.each { |part| yield deflater.deflate(part) }
|
92
|
+
@body.each { |part| yield deflater.deflate(part, Zlib::SYNC_FLUSH) }
|
90
93
|
@body.close if @body.respond_to?(:close)
|
91
94
|
yield deflater.finish
|
92
95
|
nil
|
data/lib/rack/etag.rb
CHANGED
@@ -1,32 +1,60 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
|
3
3
|
module Rack
|
4
|
-
# Automatically sets the ETag header on all String bodies
|
4
|
+
# Automatically sets the ETag header on all String bodies.
|
5
|
+
#
|
6
|
+
# The ETag header is skipped if ETag or Last-Modified headers are sent or if
|
7
|
+
# a sendfile body (body.responds_to :to_path) is given (since such cases
|
8
|
+
# should be handled by apache/nginx).
|
9
|
+
#
|
10
|
+
# On initialization, you can pass two parameters: a Cache-Control directive
|
11
|
+
# used when Etag is absent and a directive when it is present. The first
|
12
|
+
# defaults to nil, while the second defaults to "max-age=0, privaute, must-revalidate"
|
5
13
|
class ETag
|
6
|
-
|
14
|
+
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
|
15
|
+
|
16
|
+
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
|
7
17
|
@app = app
|
18
|
+
@cache_control = cache_control
|
19
|
+
@no_cache_control = no_cache_control
|
8
20
|
end
|
9
21
|
|
10
22
|
def call(env)
|
11
23
|
status, headers, body = @app.call(env)
|
12
24
|
|
13
|
-
if !
|
25
|
+
if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
|
14
26
|
digest, body = digest_body(body)
|
15
|
-
headers['ETag'] = %("#{digest}")
|
27
|
+
headers['ETag'] = %("#{digest}") if digest
|
28
|
+
end
|
29
|
+
|
30
|
+
unless headers['Cache-Control']
|
31
|
+
headers['Cache-Control'] = digest ? @cache_control : @no_cache_control
|
16
32
|
end
|
17
33
|
|
18
34
|
[status, headers, body]
|
19
35
|
end
|
20
36
|
|
21
37
|
private
|
38
|
+
|
39
|
+
def etag_status?(status)
|
40
|
+
status == 200 || status == 201
|
41
|
+
end
|
42
|
+
|
43
|
+
def etag_body?(body)
|
44
|
+
!body.respond_to?(:to_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def skip_caching?(headers)
|
48
|
+
headers['Cache-Control'] == 'no-cache' ||
|
49
|
+
headers.key?('ETag') || headers.key?('Last-Modified')
|
50
|
+
end
|
51
|
+
|
22
52
|
def digest_body(body)
|
23
|
-
digest = Digest::MD5.new
|
24
53
|
parts = []
|
25
|
-
body.each
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
[digest.hexdigest, parts]
|
54
|
+
body.each { |part| parts << part }
|
55
|
+
string_body = parts.join
|
56
|
+
digest = Digest::MD5.hexdigest(string_body) unless string_body.empty?
|
57
|
+
[digest, parts]
|
30
58
|
end
|
31
59
|
end
|
32
60
|
end
|
data/lib/rack/file.rb
CHANGED
@@ -12,13 +12,17 @@ module Rack
|
|
12
12
|
# like sendfile on the +path+.
|
13
13
|
|
14
14
|
class File
|
15
|
+
SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
|
16
|
+
|
15
17
|
attr_accessor :root
|
16
18
|
attr_accessor :path
|
19
|
+
attr_accessor :cache_control
|
17
20
|
|
18
21
|
alias :to_path :path
|
19
22
|
|
20
|
-
def initialize(root)
|
23
|
+
def initialize(root, cache_control = nil)
|
21
24
|
@root = root
|
25
|
+
@cache_control = cache_control
|
22
26
|
end
|
23
27
|
|
24
28
|
def call(env)
|
@@ -29,64 +33,93 @@ module Rack
|
|
29
33
|
|
30
34
|
def _call(env)
|
31
35
|
@path_info = Utils.unescape(env["PATH_INFO"])
|
32
|
-
|
36
|
+
parts = @path_info.split SEPS
|
33
37
|
|
34
|
-
|
38
|
+
return fail(403, "Forbidden") if parts.include? ".."
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
raise Errno::EPERM
|
41
|
-
end
|
40
|
+
@path = F.join(@root, *parts)
|
41
|
+
|
42
|
+
available = begin
|
43
|
+
F.file?(@path) && F.readable?(@path)
|
42
44
|
rescue SystemCallError
|
43
|
-
|
45
|
+
false
|
44
46
|
end
|
45
|
-
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
[body]]
|
48
|
+
if available
|
49
|
+
serving(env)
|
50
|
+
else
|
51
|
+
fail(404, "File not found: #{@path_info}")
|
52
|
+
end
|
53
53
|
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
55
|
+
def serving(env)
|
56
|
+
# NOTE:
|
57
|
+
# We check via File::size? whether this file provides size info
|
58
|
+
# via stat (e.g. /proc files often don't), otherwise we have to
|
59
|
+
# figure it out by reading the whole file into memory.
|
60
|
+
size = F.size?(@path) || Utils.bytesize(F.read(@path))
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
62
|
+
response = [
|
63
|
+
200,
|
64
|
+
{
|
65
|
+
"Last-Modified" => F.mtime(@path).httpdate,
|
66
|
+
"Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain')
|
67
|
+
},
|
68
|
+
self
|
69
|
+
]
|
70
|
+
response[1].merge! 'Cache-Control' => @cache_control if @cache_control
|
71
|
+
|
72
|
+
ranges = Rack::Utils.byte_ranges(env, size)
|
73
|
+
if ranges.nil? || ranges.length > 1
|
74
|
+
# No ranges, or multiple ranges (which we don't support):
|
75
|
+
# TODO: Support multiple byte-ranges
|
76
|
+
response[0] = 200
|
77
|
+
@range = 0..size-1
|
78
|
+
elsif ranges.empty?
|
79
|
+
# Unsatisfiable. Return error, and file size:
|
80
|
+
response = fail(416, "Byte range unsatisfiable")
|
81
|
+
response[1]["Content-Range"] = "bytes */#{size}"
|
82
|
+
return response
|
64
83
|
else
|
65
|
-
|
66
|
-
|
84
|
+
# Partial content:
|
85
|
+
@range = ranges[0]
|
86
|
+
response[0] = 206
|
87
|
+
response[1]["Content-Range"] = "bytes #{@range.begin}-#{@range.end}/#{size}"
|
88
|
+
size = @range.end - @range.begin + 1
|
67
89
|
end
|
68
90
|
|
69
|
-
[
|
70
|
-
|
71
|
-
"Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'),
|
72
|
-
"Content-Length" => size.to_s
|
73
|
-
}, body]
|
74
|
-
end
|
75
|
-
|
76
|
-
def not_found
|
77
|
-
body = "File not found: #{@path_info}\n"
|
78
|
-
[404, {"Content-Type" => "text/plain",
|
79
|
-
"Content-Length" => body.size.to_s,
|
80
|
-
"X-Cascade" => "pass"},
|
81
|
-
[body]]
|
91
|
+
response[1]["Content-Length"] = size.to_s
|
92
|
+
response
|
82
93
|
end
|
83
94
|
|
84
95
|
def each
|
85
|
-
F.open(@path, "rb")
|
86
|
-
|
96
|
+
F.open(@path, "rb") do |file|
|
97
|
+
file.seek(@range.begin)
|
98
|
+
remaining_len = @range.end-@range.begin+1
|
99
|
+
while remaining_len > 0
|
100
|
+
part = file.read([8192, remaining_len].min)
|
101
|
+
break unless part
|
102
|
+
remaining_len -= part.length
|
103
|
+
|
87
104
|
yield part
|
88
105
|
end
|
89
|
-
|
106
|
+
end
|
90
107
|
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def fail(status, body)
|
112
|
+
body += "\n"
|
113
|
+
[
|
114
|
+
status,
|
115
|
+
{
|
116
|
+
"Content-Type" => "text/plain",
|
117
|
+
"Content-Length" => body.size.to_s,
|
118
|
+
"X-Cascade" => "pass"
|
119
|
+
},
|
120
|
+
[body]
|
121
|
+
]
|
122
|
+
end
|
123
|
+
|
91
124
|
end
|
92
125
|
end
|