puma 4.1.1 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +149 -10
- data/LICENSE +23 -20
- data/README.md +30 -46
- data/docs/architecture.md +3 -3
- data/docs/deployment.md +9 -3
- data/docs/fork_worker.md +31 -0
- data/docs/jungle/README.md +13 -0
- data/{tools → docs}/jungle/rc.d/README.md +0 -0
- data/{tools → docs}/jungle/rc.d/puma +0 -0
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/{tools → docs}/jungle/upstart/README.md +0 -0
- data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
- data/{tools → docs}/jungle/upstart/puma.conf +0 -0
- data/docs/plugins.md +20 -10
- data/docs/signals.md +7 -6
- data/docs/systemd.md +1 -63
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/extconf.rb +6 -0
- data/ext/puma_http11/http11_parser.c +40 -63
- data/ext/puma_http11/http11_parser.java.rl +21 -37
- data/ext/puma_http11/http11_parser.rl +3 -1
- data/ext/puma_http11/http11_parser_common.rl +3 -3
- data/ext/puma_http11/mini_ssl.c +15 -2
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
- data/ext/puma_http11/puma_http11.c +9 -38
- data/lib/puma.rb +23 -0
- data/lib/puma/app/status.rb +46 -30
- data/lib/puma/binder.rb +112 -124
- data/lib/puma/cli.rb +11 -15
- data/lib/puma/client.rb +250 -209
- data/lib/puma/cluster.rb +203 -85
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +31 -42
- data/lib/puma/const.rb +24 -19
- data/lib/puma/control_cli.rb +46 -17
- data/lib/puma/detect.rb +17 -0
- data/lib/puma/dsl.rb +162 -70
- data/lib/puma/error_logger.rb +97 -0
- data/lib/puma/events.rb +35 -31
- data/lib/puma/io_buffer.rb +9 -2
- data/lib/puma/jruby_restart.rb +0 -58
- data/lib/puma/launcher.rb +117 -58
- data/lib/puma/minissl.rb +60 -18
- data/lib/puma/minissl/context_builder.rb +73 -0
- data/lib/puma/null_io.rb +1 -1
- data/lib/puma/plugin.rb +6 -12
- data/lib/puma/rack/builder.rb +0 -4
- data/lib/puma/reactor.rb +16 -9
- data/lib/puma/runner.rb +11 -32
- data/lib/puma/server.rb +173 -193
- data/lib/puma/single.rb +7 -64
- data/lib/puma/state_file.rb +6 -3
- data/lib/puma/thread_pool.rb +104 -81
- data/lib/rack/handler/puma.rb +1 -5
- data/tools/Dockerfile +16 -0
- data/tools/trickletest.rb +0 -1
- metadata +23 -24
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/convenient.rb +0 -25
- data/lib/puma/daemon_ext.rb +0 -33
- data/lib/puma/delegation.rb +0 -13
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
data/lib/puma/cli.rb
CHANGED
@@ -80,7 +80,7 @@ module Puma
|
|
80
80
|
@launcher.run
|
81
81
|
end
|
82
82
|
|
83
|
-
|
83
|
+
private
|
84
84
|
def unsupported(str)
|
85
85
|
@events.error(str)
|
86
86
|
raise UnsupportedOption
|
@@ -112,21 +112,11 @@ module Puma
|
|
112
112
|
configure_control_url(arg)
|
113
113
|
end
|
114
114
|
|
115
|
-
# alias --control-url for backwards-compatibility
|
116
|
-
o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
|
117
|
-
configure_control_url(arg)
|
118
|
-
end
|
119
|
-
|
120
115
|
o.on "--control-token TOKEN",
|
121
116
|
"The token to use as authentication for the control server" do |arg|
|
122
117
|
@control_options[:auth_token] = arg
|
123
118
|
end
|
124
119
|
|
125
|
-
o.on "-d", "--daemon", "Daemonize the server into the background" do
|
126
|
-
user_config.daemonize
|
127
|
-
user_config.quiet
|
128
|
-
end
|
129
|
-
|
130
120
|
o.on "--debug", "Log lowlevel debugging information" do
|
131
121
|
user_config.debug
|
132
122
|
end
|
@@ -140,6 +130,12 @@ module Puma
|
|
140
130
|
user_config.environment arg
|
141
131
|
end
|
142
132
|
|
133
|
+
o.on "-f", "--fork-worker=[REQUESTS]", OptionParser::DecimalInteger,
|
134
|
+
"Fork new workers from existing worker. Cluster mode only",
|
135
|
+
"Auto-refork after REQUESTS (default 1000)" do |*args|
|
136
|
+
user_config.fork_worker(*args.compact)
|
137
|
+
end
|
138
|
+
|
143
139
|
o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
|
144
140
|
$LOAD_PATH.unshift(*arg.split(':'))
|
145
141
|
end
|
@@ -161,6 +157,10 @@ module Puma
|
|
161
157
|
user_config.prune_bundler
|
162
158
|
end
|
163
159
|
|
160
|
+
o.on "--extra-runtime-dependencies GEM1,GEM2", "Defines any extra needed gems when using --prune-bundler" do |arg|
|
161
|
+
user_config.extra_runtime_dependencies arg.split(',')
|
162
|
+
end
|
163
|
+
|
164
164
|
o.on "-q", "--quiet", "Do not log requests internally (default true)" do
|
165
165
|
user_config.quiet
|
166
166
|
end
|
@@ -188,10 +188,6 @@ module Puma
|
|
188
188
|
end
|
189
189
|
end
|
190
190
|
|
191
|
-
o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
|
192
|
-
user_config.tcp_mode!
|
193
|
-
end
|
194
|
-
|
195
191
|
o.on "--early-hints", "Enable early hints support" do
|
196
192
|
user_config.early_hints
|
197
193
|
end
|
data/lib/puma/client.rb
CHANGED
@@ -9,8 +9,8 @@ class IO
|
|
9
9
|
end
|
10
10
|
|
11
11
|
require 'puma/detect'
|
12
|
-
require 'puma/delegation'
|
13
12
|
require 'tempfile'
|
13
|
+
require 'forwardable'
|
14
14
|
|
15
15
|
if Puma::IS_JRUBY
|
16
16
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
@@ -24,11 +24,11 @@ module Puma
|
|
24
24
|
class ConnectionError < RuntimeError; end
|
25
25
|
|
26
26
|
# An instance of this class represents a unique request from a client.
|
27
|
-
# For example a web request from a browser or from CURL.
|
27
|
+
# For example, this could be a web request from a browser or from CURL.
|
28
28
|
#
|
29
29
|
# An instance of `Puma::Client` can be used as if it were an IO object
|
30
|
-
# by the reactor
|
31
|
-
# on any non-IO objects it polls. For example nio4r internally calls
|
30
|
+
# by the reactor. The reactor is expected to call `#to_io`
|
31
|
+
# on any non-IO objects it polls. For example, nio4r internally calls
|
32
32
|
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
33
33
|
# registered.
|
34
34
|
#
|
@@ -36,8 +36,12 @@ module Puma
|
|
36
36
|
# the header and body are fully buffered via the `try_to_finish` method.
|
37
37
|
# They can be used to "time out" a response via the `timeout_at` reader.
|
38
38
|
class Client
|
39
|
+
# The object used for a request with no body. All requests with
|
40
|
+
# no body share this one object since it has no state.
|
41
|
+
EmptyBody = NullIO.new
|
42
|
+
|
39
43
|
include Puma::Const
|
40
|
-
extend
|
44
|
+
extend Forwardable
|
41
45
|
|
42
46
|
def initialize(io, env=nil)
|
43
47
|
@io = io
|
@@ -79,7 +83,7 @@ module Puma
|
|
79
83
|
|
80
84
|
attr_accessor :remote_addr_header
|
81
85
|
|
82
|
-
|
86
|
+
def_delegators :@io, :closed?
|
83
87
|
|
84
88
|
def inspect
|
85
89
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
@@ -144,185 +148,12 @@ module Puma
|
|
144
148
|
end
|
145
149
|
end
|
146
150
|
|
147
|
-
# The object used for a request with no body. All requests with
|
148
|
-
# no body share this one object since it has no state.
|
149
|
-
EmptyBody = NullIO.new
|
150
|
-
|
151
|
-
def setup_chunked_body(body)
|
152
|
-
@chunked_body = true
|
153
|
-
@partial_part_left = 0
|
154
|
-
@prev_chunk = ""
|
155
|
-
|
156
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
157
|
-
@body.binmode
|
158
|
-
@tempfile = @body
|
159
|
-
|
160
|
-
return decode_chunk(body)
|
161
|
-
end
|
162
|
-
|
163
|
-
def decode_chunk(chunk)
|
164
|
-
if @partial_part_left > 0
|
165
|
-
if @partial_part_left <= chunk.size
|
166
|
-
if @partial_part_left > 2
|
167
|
-
@body << chunk[0..(@partial_part_left-3)] # skip the \r\n
|
168
|
-
end
|
169
|
-
chunk = chunk[@partial_part_left..-1]
|
170
|
-
@partial_part_left = 0
|
171
|
-
else
|
172
|
-
@body << chunk if @partial_part_left > 2 # don't include the last \r\n
|
173
|
-
@partial_part_left -= chunk.size
|
174
|
-
return false
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
if @prev_chunk.empty?
|
179
|
-
io = StringIO.new(chunk)
|
180
|
-
else
|
181
|
-
io = StringIO.new(@prev_chunk+chunk)
|
182
|
-
@prev_chunk = ""
|
183
|
-
end
|
184
|
-
|
185
|
-
while !io.eof?
|
186
|
-
line = io.gets
|
187
|
-
if line.end_with?("\r\n")
|
188
|
-
len = line.strip.to_i(16)
|
189
|
-
if len == 0
|
190
|
-
@in_last_chunk = true
|
191
|
-
@body.rewind
|
192
|
-
rest = io.read
|
193
|
-
last_crlf_size = "\r\n".bytesize
|
194
|
-
if rest.bytesize < last_crlf_size
|
195
|
-
@buffer = nil
|
196
|
-
@partial_part_left = last_crlf_size - rest.bytesize
|
197
|
-
return false
|
198
|
-
else
|
199
|
-
@buffer = rest[last_crlf_size..-1]
|
200
|
-
@buffer = nil if @buffer.empty?
|
201
|
-
set_ready
|
202
|
-
return true
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
len += 2
|
207
|
-
|
208
|
-
part = io.read(len)
|
209
|
-
|
210
|
-
unless part
|
211
|
-
@partial_part_left = len
|
212
|
-
next
|
213
|
-
end
|
214
|
-
|
215
|
-
got = part.size
|
216
|
-
|
217
|
-
case
|
218
|
-
when got == len
|
219
|
-
@body << part[0..-3] # to skip the ending \r\n
|
220
|
-
when got <= len - 2
|
221
|
-
@body << part
|
222
|
-
@partial_part_left = len - part.size
|
223
|
-
when got == len - 1 # edge where we get just \r but not \n
|
224
|
-
@body << part[0..-2]
|
225
|
-
@partial_part_left = len - part.size
|
226
|
-
end
|
227
|
-
else
|
228
|
-
@prev_chunk = line
|
229
|
-
return false
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
if @in_last_chunk
|
234
|
-
set_ready
|
235
|
-
true
|
236
|
-
else
|
237
|
-
false
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def read_chunked_body
|
242
|
-
while true
|
243
|
-
begin
|
244
|
-
chunk = @io.read_nonblock(4096)
|
245
|
-
rescue IO::WaitReadable
|
246
|
-
return false
|
247
|
-
rescue SystemCallError, IOError
|
248
|
-
raise ConnectionError, "Connection error detected during read"
|
249
|
-
end
|
250
|
-
|
251
|
-
# No chunk means a closed socket
|
252
|
-
unless chunk
|
253
|
-
@body.close
|
254
|
-
@buffer = nil
|
255
|
-
set_ready
|
256
|
-
raise EOFError
|
257
|
-
end
|
258
|
-
|
259
|
-
return true if decode_chunk(chunk)
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
def setup_body
|
264
|
-
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
265
|
-
|
266
|
-
if @env[HTTP_EXPECT] == CONTINUE
|
267
|
-
# TODO allow a hook here to check the headers before
|
268
|
-
# going forward
|
269
|
-
@io << HTTP_11_100
|
270
|
-
@io.flush
|
271
|
-
end
|
272
|
-
|
273
|
-
@read_header = false
|
274
|
-
|
275
|
-
body = @parser.body
|
276
|
-
|
277
|
-
te = @env[TRANSFER_ENCODING2]
|
278
|
-
|
279
|
-
if te && CHUNKED.casecmp(te) == 0
|
280
|
-
return setup_chunked_body(body)
|
281
|
-
end
|
282
|
-
|
283
|
-
@chunked_body = false
|
284
|
-
|
285
|
-
cl = @env[CONTENT_LENGTH]
|
286
|
-
|
287
|
-
unless cl
|
288
|
-
@buffer = body.empty? ? nil : body
|
289
|
-
@body = EmptyBody
|
290
|
-
set_ready
|
291
|
-
return true
|
292
|
-
end
|
293
|
-
|
294
|
-
remain = cl.to_i - body.bytesize
|
295
|
-
|
296
|
-
if remain <= 0
|
297
|
-
@body = StringIO.new(body)
|
298
|
-
@buffer = nil
|
299
|
-
set_ready
|
300
|
-
return true
|
301
|
-
end
|
302
|
-
|
303
|
-
if remain > MAX_BODY
|
304
|
-
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
305
|
-
@body.binmode
|
306
|
-
@tempfile = @body
|
307
|
-
else
|
308
|
-
# The body[0,0] trick is to get an empty string in the same
|
309
|
-
# encoding as body.
|
310
|
-
@body = StringIO.new body[0,0]
|
311
|
-
end
|
312
|
-
|
313
|
-
@body.write body
|
314
|
-
|
315
|
-
@body_remain = remain
|
316
|
-
|
317
|
-
return false
|
318
|
-
end
|
319
|
-
|
320
151
|
def try_to_finish
|
321
152
|
return read_body unless @read_header
|
322
153
|
|
323
154
|
begin
|
324
155
|
data = @io.read_nonblock(CHUNK_SIZE)
|
325
|
-
rescue
|
156
|
+
rescue IO::WaitReadable
|
326
157
|
return false
|
327
158
|
rescue SystemCallError, IOError, EOFError
|
328
159
|
raise ConnectionError, "Connection error detected during read"
|
@@ -407,16 +238,127 @@ module Puma
|
|
407
238
|
return false unless IO.select([@to_io], nil, nil, 0)
|
408
239
|
try_to_finish
|
409
240
|
end
|
241
|
+
|
242
|
+
# For documentation, see https://github.com/puma/puma/issues/1754
|
243
|
+
send(:alias_method, :jruby_eagerly_finish, :eagerly_finish)
|
410
244
|
end # IS_JRUBY
|
411
245
|
|
412
|
-
def finish
|
246
|
+
def finish(timeout)
|
413
247
|
return true if @ready
|
414
248
|
until try_to_finish
|
415
|
-
|
249
|
+
can_read = begin
|
250
|
+
IO.select([@to_io], nil, nil, timeout)
|
251
|
+
rescue ThreadPool::ForceShutdown
|
252
|
+
nil
|
253
|
+
end
|
254
|
+
unless can_read
|
255
|
+
write_error(408) if in_data_phase
|
256
|
+
raise ConnectionError
|
257
|
+
end
|
416
258
|
end
|
417
259
|
true
|
418
260
|
end
|
419
261
|
|
262
|
+
def write_error(status_code)
|
263
|
+
begin
|
264
|
+
@io << ERROR_RESPONSE[status_code]
|
265
|
+
rescue StandardError
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def peerip
|
270
|
+
return @peerip if @peerip
|
271
|
+
|
272
|
+
if @remote_addr_header
|
273
|
+
hdr = (@env[@remote_addr_header] || LOCALHOST_IP).split(/[\s,]/).first
|
274
|
+
@peerip = hdr
|
275
|
+
return hdr
|
276
|
+
end
|
277
|
+
|
278
|
+
@peerip ||= @io.peeraddr.last
|
279
|
+
end
|
280
|
+
|
281
|
+
# Returns true if the persistent connection can be closed immediately
|
282
|
+
# without waiting for the configured idle/shutdown timeout.
|
283
|
+
# @version 5.0.0
|
284
|
+
#
|
285
|
+
def can_close?
|
286
|
+
# Allow connection to close if it's received at least one full request
|
287
|
+
# and hasn't received any data for a future request.
|
288
|
+
#
|
289
|
+
# From RFC 2616 section 8.1.4:
|
290
|
+
# Servers SHOULD always respond to at least one request per connection,
|
291
|
+
# if at all possible.
|
292
|
+
@requests_served > 0 && @parsed_bytes == 0
|
293
|
+
end
|
294
|
+
|
295
|
+
private
|
296
|
+
|
297
|
+
def setup_body
|
298
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
299
|
+
|
300
|
+
if @env[HTTP_EXPECT] == CONTINUE
|
301
|
+
# TODO allow a hook here to check the headers before
|
302
|
+
# going forward
|
303
|
+
@io << HTTP_11_100
|
304
|
+
@io.flush
|
305
|
+
end
|
306
|
+
|
307
|
+
@read_header = false
|
308
|
+
|
309
|
+
body = @parser.body
|
310
|
+
|
311
|
+
te = @env[TRANSFER_ENCODING2]
|
312
|
+
|
313
|
+
if te
|
314
|
+
if te.include?(",")
|
315
|
+
te.split(",").each do |part|
|
316
|
+
if CHUNKED.casecmp(part.strip) == 0
|
317
|
+
return setup_chunked_body(body)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
elsif CHUNKED.casecmp(te) == 0
|
321
|
+
return setup_chunked_body(body)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
@chunked_body = false
|
326
|
+
|
327
|
+
cl = @env[CONTENT_LENGTH]
|
328
|
+
|
329
|
+
unless cl
|
330
|
+
@buffer = body.empty? ? nil : body
|
331
|
+
@body = EmptyBody
|
332
|
+
set_ready
|
333
|
+
return true
|
334
|
+
end
|
335
|
+
|
336
|
+
remain = cl.to_i - body.bytesize
|
337
|
+
|
338
|
+
if remain <= 0
|
339
|
+
@body = StringIO.new(body)
|
340
|
+
@buffer = nil
|
341
|
+
set_ready
|
342
|
+
return true
|
343
|
+
end
|
344
|
+
|
345
|
+
if remain > MAX_BODY
|
346
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
347
|
+
@body.binmode
|
348
|
+
@tempfile = @body
|
349
|
+
else
|
350
|
+
# The body[0,0] trick is to get an empty string in the same
|
351
|
+
# encoding as body.
|
352
|
+
@body = StringIO.new body[0,0]
|
353
|
+
end
|
354
|
+
|
355
|
+
@body.write body
|
356
|
+
|
357
|
+
@body_remain = remain
|
358
|
+
|
359
|
+
return false
|
360
|
+
end
|
361
|
+
|
420
362
|
def read_body
|
421
363
|
if @chunked_body
|
422
364
|
return read_chunked_body
|
@@ -434,7 +376,7 @@ module Puma
|
|
434
376
|
|
435
377
|
begin
|
436
378
|
chunk = @io.read_nonblock(want)
|
437
|
-
rescue
|
379
|
+
rescue IO::WaitReadable
|
438
380
|
return false
|
439
381
|
rescue SystemCallError, IOError
|
440
382
|
raise ConnectionError, "Connection error detected during read"
|
@@ -462,45 +404,144 @@ module Puma
|
|
462
404
|
false
|
463
405
|
end
|
464
406
|
|
465
|
-
def
|
466
|
-
|
467
|
-
|
407
|
+
def read_chunked_body
|
408
|
+
while true
|
409
|
+
begin
|
410
|
+
chunk = @io.read_nonblock(4096)
|
411
|
+
rescue IO::WaitReadable
|
412
|
+
return false
|
413
|
+
rescue SystemCallError, IOError
|
414
|
+
raise ConnectionError, "Connection error detected during read"
|
415
|
+
end
|
416
|
+
|
417
|
+
# No chunk means a closed socket
|
418
|
+
unless chunk
|
419
|
+
@body.close
|
420
|
+
@buffer = nil
|
421
|
+
set_ready
|
422
|
+
raise EOFError
|
423
|
+
end
|
424
|
+
|
425
|
+
if decode_chunk(chunk)
|
426
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
427
|
+
return true
|
428
|
+
end
|
468
429
|
end
|
469
|
-
@requests_served += 1
|
470
|
-
@ready = true
|
471
430
|
end
|
472
431
|
|
473
|
-
def
|
474
|
-
|
475
|
-
|
476
|
-
|
432
|
+
def setup_chunked_body(body)
|
433
|
+
@chunked_body = true
|
434
|
+
@partial_part_left = 0
|
435
|
+
@prev_chunk = ""
|
436
|
+
|
437
|
+
@body = Tempfile.new(Const::PUMA_TMP_BASE)
|
438
|
+
@body.binmode
|
439
|
+
@tempfile = @body
|
440
|
+
@chunked_content_length = 0
|
441
|
+
|
442
|
+
if decode_chunk(body)
|
443
|
+
@env[CONTENT_LENGTH] = @chunked_content_length
|
444
|
+
return true
|
477
445
|
end
|
478
446
|
end
|
479
447
|
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
rescue StandardError
|
484
|
-
end
|
448
|
+
# @version 5.0.0
|
449
|
+
def write_chunk(str)
|
450
|
+
@chunked_content_length += @body.write(str)
|
485
451
|
end
|
486
452
|
|
487
|
-
def
|
488
|
-
|
489
|
-
@
|
490
|
-
|
453
|
+
def decode_chunk(chunk)
|
454
|
+
if @partial_part_left > 0
|
455
|
+
if @partial_part_left <= chunk.size
|
456
|
+
if @partial_part_left > 2
|
457
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
458
|
+
end
|
459
|
+
chunk = chunk[@partial_part_left..-1]
|
460
|
+
@partial_part_left = 0
|
461
|
+
else
|
462
|
+
if @partial_part_left > 2
|
463
|
+
if @partial_part_left == chunk.size + 1
|
464
|
+
# Don't include the last \r
|
465
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
466
|
+
else
|
467
|
+
# don't include the last \r\n
|
468
|
+
write_chunk(chunk)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
@partial_part_left -= chunk.size
|
472
|
+
return false
|
473
|
+
end
|
491
474
|
end
|
492
|
-
end
|
493
475
|
|
494
|
-
|
495
|
-
|
476
|
+
if @prev_chunk.empty?
|
477
|
+
io = StringIO.new(chunk)
|
478
|
+
else
|
479
|
+
io = StringIO.new(@prev_chunk+chunk)
|
480
|
+
@prev_chunk = ""
|
481
|
+
end
|
496
482
|
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
483
|
+
while !io.eof?
|
484
|
+
line = io.gets
|
485
|
+
if line.end_with?("\r\n")
|
486
|
+
len = line.strip.to_i(16)
|
487
|
+
if len == 0
|
488
|
+
@in_last_chunk = true
|
489
|
+
@body.rewind
|
490
|
+
rest = io.read
|
491
|
+
last_crlf_size = "\r\n".bytesize
|
492
|
+
if rest.bytesize < last_crlf_size
|
493
|
+
@buffer = nil
|
494
|
+
@partial_part_left = last_crlf_size - rest.bytesize
|
495
|
+
return false
|
496
|
+
else
|
497
|
+
@buffer = rest[last_crlf_size..-1]
|
498
|
+
@buffer = nil if @buffer.empty?
|
499
|
+
set_ready
|
500
|
+
return true
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
len += 2
|
505
|
+
|
506
|
+
part = io.read(len)
|
507
|
+
|
508
|
+
unless part
|
509
|
+
@partial_part_left = len
|
510
|
+
next
|
511
|
+
end
|
512
|
+
|
513
|
+
got = part.size
|
514
|
+
|
515
|
+
case
|
516
|
+
when got == len
|
517
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
518
|
+
when got <= len - 2
|
519
|
+
write_chunk(part)
|
520
|
+
@partial_part_left = len - part.size
|
521
|
+
when got == len - 1 # edge where we get just \r but not \n
|
522
|
+
write_chunk(part[0..-2])
|
523
|
+
@partial_part_left = len - part.size
|
524
|
+
end
|
525
|
+
else
|
526
|
+
@prev_chunk = line
|
527
|
+
return false
|
528
|
+
end
|
501
529
|
end
|
502
530
|
|
503
|
-
|
531
|
+
if @in_last_chunk
|
532
|
+
set_ready
|
533
|
+
true
|
534
|
+
else
|
535
|
+
false
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def set_ready
|
540
|
+
if @body_read_start
|
541
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
|
542
|
+
end
|
543
|
+
@requests_served += 1
|
544
|
+
@ready = true
|
504
545
|
end
|
505
546
|
end
|
506
547
|
end
|