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.
- checksums.yaml +4 -4
- data/SPEC +3 -3
- data/lib/rack.rb +10 -0
- data/lib/rack/auth/abstract/handler.rb +4 -4
- data/lib/rack/auth/digest/request.rb +1 -1
- data/lib/rack/cascade.rb +1 -1
- data/lib/rack/chunked.rb +2 -2
- data/lib/rack/commonlogger.rb +4 -4
- data/lib/rack/conditionalget.rb +3 -3
- data/lib/rack/content_length.rb +2 -2
- data/lib/rack/content_type.rb +1 -1
- data/lib/rack/deflater.rb +4 -5
- data/lib/rack/directory.rb +5 -5
- data/lib/rack/etag.rb +7 -6
- data/lib/rack/file.rb +32 -14
- data/lib/rack/handler.rb +1 -1
- data/lib/rack/handler/cgi.rb +1 -1
- data/lib/rack/handler/fastcgi.rb +1 -1
- data/lib/rack/handler/lsws.rb +1 -1
- data/lib/rack/handler/mongrel.rb +1 -1
- data/lib/rack/handler/scgi.rb +2 -2
- data/lib/rack/handler/webrick.rb +6 -4
- data/lib/rack/head.rb +1 -1
- data/lib/rack/lint.rb +30 -16
- data/lib/rack/lobster.rb +2 -2
- data/lib/rack/methodoverride.rb +3 -3
- data/lib/rack/mock.rb +7 -7
- data/lib/rack/multipart/parser.rb +21 -7
- data/lib/rack/recursive.rb +6 -5
- data/lib/rack/request.rb +6 -6
- data/lib/rack/response.rb +8 -6
- data/lib/rack/runtime.rb +2 -1
- data/lib/rack/sendfile.rb +2 -2
- data/lib/rack/server.rb +7 -10
- data/lib/rack/showexceptions.rb +2 -2
- data/lib/rack/showstatus.rb +3 -3
- data/lib/rack/static.rb +1 -1
- data/lib/rack/urlmap.rb +2 -2
- data/lib/rack/utils.rb +14 -10
- data/rack.gemspec +1 -1
- data/test/spec_body_proxy.rb +16 -0
- data/test/spec_lint.rb +20 -9
- data/test/spec_multipart.rb +38 -16
- data/test/spec_request.rb +17 -0
- data/test/spec_server.rb +22 -13
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67268c360a9006f4c63b876a1dff3d8f4dfbdc51
|
4
|
+
data.tar.gz: 394f5b04b57657f9fe03bbd84ea1d81bb6a438e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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".
|
data/lib/rack.rb
CHANGED
@@ -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
|
-
{
|
21
|
-
|
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
|
-
{
|
30
|
-
|
29
|
+
{ CONTENT_TYPE => 'text/plain',
|
30
|
+
CONTENT_LENGTH => '0' },
|
31
31
|
[]
|
32
32
|
]
|
33
33
|
end
|
data/lib/rack/cascade.rb
CHANGED
data/lib/rack/chunked.rb
CHANGED
@@ -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[
|
59
|
+
headers[CONTENT_LENGTH] ||
|
60
60
|
headers['Transfer-Encoding']
|
61
61
|
[status, headers, body]
|
62
62
|
else
|
63
|
-
headers.delete(
|
63
|
+
headers.delete(CONTENT_LENGTH)
|
64
64
|
headers['Transfer-Encoding'] = 'chunked'
|
65
65
|
[status, headers, Body.new(body)]
|
66
66
|
end
|
data/lib/rack/commonlogger.rb
CHANGED
@@ -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[
|
50
|
-
env[
|
51
|
-
env[
|
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[
|
68
|
+
value = headers[CONTENT_LENGTH] or return '-'
|
69
69
|
value.to_s == '0' ? '-' : value
|
70
70
|
end
|
71
71
|
end
|
data/lib/rack/conditionalget.rb
CHANGED
@@ -20,14 +20,14 @@ module Rack
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def call(env)
|
23
|
-
case env[
|
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(
|
30
|
-
headers.delete(
|
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)
|
data/lib/rack/content_length.rb
CHANGED
@@ -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[
|
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[
|
31
|
+
headers[CONTENT_LENGTH] = length.to_s
|
32
32
|
end
|
33
33
|
|
34
34
|
[status, headers, body]
|
data/lib/rack/content_type.rb
CHANGED
data/lib/rack/deflater.rb
CHANGED
@@ -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(
|
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(
|
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, {
|
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[
|
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
|
data/lib/rack/directory.rb
CHANGED
@@ -55,8 +55,8 @@ table { width:100%%; }
|
|
55
55
|
|
56
56
|
def _call(env)
|
57
57
|
@env = env
|
58
|
-
@script_name = env[
|
59
|
-
@path_info = Utils.unescape(env[
|
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
|
-
|
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, {
|
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
|
-
|
133
|
+
CONTENT_LENGTH => size.to_s,
|
134
134
|
"X-Cascade" => "pass"}, [body]]
|
135
135
|
end
|
136
136
|
|
data/lib/rack/etag.rb
CHANGED
@@ -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[
|
32
|
+
headers[ETAG_STRING] = %(W/"#{digest}") if digest
|
32
33
|
end
|
33
34
|
|
34
|
-
unless headers[
|
35
|
+
unless headers[CACHE_CONTROL]
|
35
36
|
if digest
|
36
|
-
headers[
|
37
|
+
headers[CACHE_CONTROL] = @cache_control if @cache_control
|
37
38
|
else
|
38
|
-
headers[
|
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[
|
57
|
-
headers.key?(
|
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)
|
data/lib/rack/file.rb
CHANGED
@@ -34,11 +34,11 @@ module Rack
|
|
34
34
|
F = ::File
|
35
35
|
|
36
36
|
def _call(env)
|
37
|
-
unless ALLOWED_VERBS.include? env[
|
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[
|
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
|
-
|
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
|
-
|
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[
|
72
|
+
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]
|
74
73
|
|
75
|
-
|
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[
|
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
|
-
|
126
|
-
|
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
|
data/lib/rack/handler.rb
CHANGED
data/lib/rack/handler/cgi.rb
CHANGED
data/lib/rack/handler/fastcgi.rb
CHANGED
@@ -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[
|
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"] == ""
|
data/lib/rack/handler/lsws.rb
CHANGED
@@ -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[
|
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)
|
data/lib/rack/handler/mongrel.rb
CHANGED
data/lib/rack/handler/scgi.rb
CHANGED
@@ -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[
|
38
|
+
env["REQUEST_PATH"], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2)
|
39
39
|
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
40
|
-
env[
|
40
|
+
env[PATH_INFO] = env["REQUEST_PATH"]
|
41
41
|
env["QUERY_STRING"] ||= ""
|
42
42
|
env["SCRIPT_NAME"] = ""
|
43
43
|
|
data/lib/rack/handler/webrick.rb
CHANGED
@@ -79,12 +79,12 @@ module Rack
|
|
79
79
|
})
|
80
80
|
|
81
81
|
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
82
|
-
env[
|
83
|
-
unless env[
|
82
|
+
env[QUERY_STRING] ||= ""
|
83
|
+
unless env[PATH_INFO] == ""
|
84
84
|
path, n = req.request_uri.path, env["SCRIPT_NAME"].length
|
85
|
-
env[
|
85
|
+
env[PATH_INFO] = path[n, path.length-n]
|
86
86
|
end
|
87
|
-
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[
|
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
|