puma 6.6.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +309 -5
  3. data/README.md +41 -42
  4. data/docs/5.0-Upgrade.md +98 -0
  5. data/docs/6.0-Upgrade.md +56 -0
  6. data/docs/7.0-Upgrade.md +52 -0
  7. data/docs/8.0-Upgrade.md +100 -0
  8. data/docs/deployment.md +58 -23
  9. data/docs/fork_worker.md +5 -5
  10. data/docs/grpc.md +62 -0
  11. data/docs/images/favicon.svg +1 -0
  12. data/docs/images/running-puma.svg +1 -0
  13. data/docs/images/standard-logo.svg +1 -0
  14. data/docs/jungle/README.md +1 -1
  15. data/docs/kubernetes.md +11 -16
  16. data/docs/plugins.md +2 -2
  17. data/docs/restart.md +2 -2
  18. data/docs/signals.md +21 -21
  19. data/docs/stats.md +4 -3
  20. data/docs/systemd.md +4 -4
  21. data/ext/puma_http11/extconf.rb +2 -17
  22. data/ext/puma_http11/http11_parser.java.rl +51 -65
  23. data/ext/puma_http11/mini_ssl.c +18 -8
  24. data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  25. data/ext/puma_http11/org/jruby/puma/Http11.java +174 -102
  26. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +71 -85
  27. data/ext/puma_http11/puma_http11.c +122 -118
  28. data/lib/puma/app/status.rb +10 -2
  29. data/lib/puma/binder.rb +12 -10
  30. data/lib/puma/cli.rb +4 -6
  31. data/lib/puma/client.rb +205 -131
  32. data/lib/puma/client_env.rb +171 -0
  33. data/lib/puma/cluster/worker.rb +17 -17
  34. data/lib/puma/cluster/worker_handle.rb +38 -7
  35. data/lib/puma/cluster.rb +44 -30
  36. data/lib/puma/cluster_accept_loop_delay.rb +91 -0
  37. data/lib/puma/commonlogger.rb +3 -3
  38. data/lib/puma/configuration.rb +173 -58
  39. data/lib/puma/const.rb +11 -11
  40. data/lib/puma/control_cli.rb +7 -3
  41. data/lib/puma/detect.rb +13 -0
  42. data/lib/puma/dsl.rb +225 -108
  43. data/lib/puma/error_logger.rb +3 -1
  44. data/lib/puma/events.rb +25 -10
  45. data/lib/puma/io_buffer.rb +8 -4
  46. data/lib/puma/launcher/bundle_pruner.rb +3 -5
  47. data/lib/puma/launcher.rb +57 -53
  48. data/lib/puma/log_writer.rb +8 -2
  49. data/lib/puma/minissl.rb +0 -1
  50. data/lib/puma/plugin/systemd.rb +3 -3
  51. data/lib/puma/rack/urlmap.rb +1 -1
  52. data/lib/puma/reactor.rb +19 -13
  53. data/lib/puma/{request.rb → response.rb} +56 -212
  54. data/lib/puma/runner.rb +9 -18
  55. data/lib/puma/server.rb +182 -97
  56. data/lib/puma/server_plugin_control.rb +32 -0
  57. data/lib/puma/single.rb +7 -4
  58. data/lib/puma/state_file.rb +3 -2
  59. data/lib/puma/thread_pool.rb +170 -99
  60. data/lib/puma/util.rb +0 -7
  61. data/lib/puma.rb +10 -0
  62. data/lib/rack/handler/puma.rb +3 -3
  63. data/tools/Dockerfile +15 -5
  64. metadata +19 -7
  65. data/ext/puma_http11/ext_help.h +0 -15
data/lib/puma/client.rb CHANGED
@@ -1,15 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class IO
4
- # We need to use this for a jruby work around on both 1.8 and 1.9.
5
- # So this either creates the constant (on 1.8), or harmlessly
6
- # reopens it (on 1.9).
7
- module WaitReadable
8
- end
9
- end
10
-
11
3
  require_relative 'detect'
12
4
  require_relative 'io_buffer'
5
+ require_relative 'client_env'
13
6
  require 'tempfile'
14
7
 
15
8
  if Puma::IS_JRUBY
@@ -28,8 +21,8 @@ module Puma
28
21
  #———————————————————————— DO NOT USE — this class is for internal use only ———
29
22
 
30
23
 
31
- # An instance of this class represents a unique request from a client.
32
- # For example, this could be a web request from a browser or from CURL.
24
+ # An instance of this class wraps a connection/socket.
25
+ # For example, this could be an http request from a browser or from CURL.
33
26
  #
34
27
  # An instance of `Puma::Client` can be used as if it were an IO object
35
28
  # by the reactor. The reactor is expected to call `#to_io`
@@ -37,12 +30,18 @@ module Puma
37
30
  # `IO::try_convert` (which may call `#to_io`) when a new socket is
38
31
  # registered.
39
32
  #
40
- # Instances of this class are responsible for knowing if
41
- # the header and body are fully buffered via the `try_to_finish` method.
33
+ # Instances of this class are responsible for knowing if the request line,
34
+ # headers and body are fully buffered and verified via the `try_to_finish` method.
35
+ # All verification of each request is done in the `Client` object.
42
36
  # They can be used to "time out" a response via the `timeout_at` reader.
43
37
  #
38
+ # Most of the code for env processing and verification is contained
39
+ # in `Puma::ClientEnv`, which is included.
40
+ #
44
41
  class Client # :nodoc:
45
42
 
43
+ include ClientEnv
44
+
46
45
  # this tests all values but the last, which must be chunked
47
46
  ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
48
47
 
@@ -64,11 +63,22 @@ module Puma
64
63
 
65
64
  TE_ERR_MSG = 'Invalid Transfer-Encoding'
66
65
 
66
+ # See:
67
+ # https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
68
+ # https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
69
+ STRIP_OWS = /\A[ \t]+|[ \t]+\z/
70
+
67
71
  # The object used for a request with no body. All requests with
68
72
  # no body share this one object since it has no state.
69
73
  EmptyBody = NullIO.new
70
74
 
71
- include Puma::Const
75
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
76
+ :tempfile, :io_buffer, :http_content_length_limit_exceeded,
77
+ :requests_served, :error_status_code
78
+
79
+ attr_writer :peerip, :http_content_length_limit, :supported_http_methods
80
+
81
+ attr_accessor :remote_addr_header, :listener, :env_set_http_version
72
82
 
73
83
  def initialize(io, env=nil)
74
84
  @io = io
@@ -94,7 +104,8 @@ module Puma
94
104
  @hijacked = false
95
105
 
96
106
  @http_content_length_limit = nil
97
- @http_content_length_limit_exceeded = false
107
+ @http_content_length_limit_exceeded = nil
108
+ @error_status_code = nil
98
109
 
99
110
  @peerip = nil
100
111
  @peer_family = nil
@@ -110,13 +121,6 @@ module Puma
110
121
  @read_buffer = String.new # rubocop: disable Performance/UnfreezeString
111
122
  end
112
123
 
113
- attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
114
- :tempfile, :io_buffer, :http_content_length_limit_exceeded
115
-
116
- attr_writer :peerip, :http_content_length_limit
117
-
118
- attr_accessor :remote_addr_header, :listener
119
-
120
124
  # Remove in Puma 7?
121
125
  def closed?
122
126
  @to_io.closed?
@@ -133,9 +137,9 @@ module Puma
133
137
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
134
138
  end
135
139
 
136
- # For the hijack protocol (allows us to just put the Client object
137
- # into the env)
138
- def call
140
+ # For the full hijack protocol, `env['rack.hijack']` is set to
141
+ # `client.method :full_hijack`
142
+ def full_hijack
139
143
  @hijacked = true
140
144
  env[HIJACK_IO] ||= @io
141
145
  end
@@ -150,53 +154,48 @@ module Puma
150
154
  end
151
155
 
152
156
  # Number of seconds until the timeout elapses.
157
+ # @!attribute [r] timeout
153
158
  def timeout
154
159
  [@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
155
160
  end
156
161
 
157
- def reset(fast_check=true)
162
+ def reset
158
163
  @parser.reset
159
164
  @io_buffer.reset
160
165
  @read_header = true
161
- @read_proxy = !!@expect_proxy_proto
166
+ @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
162
167
  @env = @proto_env.dup
163
168
  @parsed_bytes = 0
164
169
  @ready = false
165
170
  @body_remain = 0
166
171
  @peerip = nil if @remote_addr_header
167
172
  @in_last_chunk = false
168
- @http_content_length_limit_exceeded = false
173
+ @http_content_length_limit_exceeded = nil
174
+ @error_status_code = nil
175
+ end
169
176
 
177
+ # only used with back-to-back requests contained in the buffer
178
+ def process_back_to_back_requests
170
179
  if @buffer
171
180
  return false unless try_to_parse_proxy_protocol
172
181
 
173
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
174
-
175
- if @parser.finished?
176
- return setup_body
177
- elsif @parsed_bytes >= MAX_HEADER
178
- raise HttpParserError,
179
- "HEADER is longer than allowed, aborting client early."
180
- end
182
+ @parsed_bytes = parser_execute
181
183
 
182
- return false
183
- else
184
- begin
185
- if fast_check && @to_io.wait_readable(FAST_TRACK_KA_TIMEOUT)
186
- return try_to_finish
187
- end
188
- rescue IOError
189
- # swallow it
190
- end
184
+ @parser.finished? ? process_env_body : false
191
185
  end
192
186
  end
193
187
 
188
+ # if a client sends back-to-back requests, the buffer may contain one or more
189
+ # of them.
190
+ def has_back_to_back_requests?
191
+ !(@buffer.nil? || @buffer.empty?)
192
+ end
193
+
194
194
  def close
195
195
  tempfile_close
196
196
  begin
197
197
  @io.close
198
198
  rescue IOError, Errno::EBADF
199
- Puma::Util.purge_interrupt_queue
200
199
  end
201
200
  end
202
201
 
@@ -214,37 +213,42 @@ module Puma
214
213
  def try_to_parse_proxy_protocol
215
214
  if @read_proxy
216
215
  if @expect_proxy_proto == :v1
217
- if @buffer.include? "\r\n"
218
- if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
219
- if md[1]
220
- @peerip = md[1].split(" ")[0]
216
+ crlf_index = @buffer.index "\r\n"
217
+
218
+ unless crlf_index
219
+ if "PROXY ".start_with? @buffer
220
+ return false
221
+ elsif @buffer.start_with? "PROXY "
222
+ if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
223
+ raise ConnectionError, "PROXY protocol v1 line is too long"
221
224
  end
222
- @buffer = md.post_match
225
+ return false
223
226
  end
224
- # if the buffer has a \r\n but doesn't have a PROXY protocol
225
- # request, this is just HTTP from a non-PROXY client; move on
227
+
226
228
  @read_proxy = false
227
- return @buffer.size > 0
228
- else
229
- return false
229
+ return true
230
+ end
231
+
232
+ if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
233
+ raise ConnectionError, "PROXY protocol v1 line is too long"
234
+ end
235
+
236
+ if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
237
+ if md[1]
238
+ @peerip = md[1].split(" ")[0]
239
+ end
240
+ @buffer = md.post_match
230
241
  end
242
+ # if the buffer has a \r\n but doesn't have a PROXY protocol
243
+ # request, this is just HTTP from a non-PROXY client; move on
244
+ @read_proxy = false
245
+ return @buffer.size > 0
231
246
  end
232
247
  end
233
248
  true
234
249
  end
235
250
 
236
251
  def try_to_finish
237
- if env[CONTENT_LENGTH] && above_http_content_limit(env[CONTENT_LENGTH].to_i)
238
- @http_content_length_limit_exceeded = true
239
- end
240
-
241
- if @http_content_length_limit_exceeded
242
- @buffer = nil
243
- @body = EmptyBody
244
- set_ready
245
- return true
246
- end
247
-
248
252
  return read_body if in_data_phase
249
253
 
250
254
  data = nil
@@ -273,26 +277,17 @@ module Puma
273
277
 
274
278
  return false unless try_to_parse_proxy_protocol
275
279
 
276
- @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
277
-
278
- if @parser.finished? && above_http_content_limit(@parser.body.bytesize)
279
- @http_content_length_limit_exceeded = true
280
- end
281
-
282
- if @parser.finished?
283
- return setup_body
284
- elsif @parsed_bytes >= MAX_HEADER
285
- raise HttpParserError,
286
- "HEADER is longer than allowed, aborting client early."
287
- end
280
+ @parsed_bytes = parser_execute
288
281
 
289
- false
282
+ @parser.finished? ? process_env_body : false
290
283
  end
291
284
 
292
285
  def eagerly_finish
293
286
  return true if @ready
294
- return false unless @to_io.wait_readable(0)
295
- try_to_finish
287
+ while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
288
+ return true if try_to_finish
289
+ end
290
+ false
296
291
  end
297
292
 
298
293
  def finish(timeout)
@@ -300,6 +295,58 @@ module Puma
300
295
  @to_io.wait_readable(timeout) || timeout! until try_to_finish
301
296
  end
302
297
 
298
+ # Wraps `@parser.execute` and adds meaningful error messages
299
+ # @return [Integer] bytes of buffer read by parser
300
+ #
301
+ def parser_execute
302
+ ret = @parser.execute(@env, @buffer, @parsed_bytes)
303
+
304
+ if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
305
+ raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
306
+ end
307
+ ret
308
+ rescue => e
309
+ @env[HTTP_CONNECTION] = 'close'
310
+ raise e unless HttpParserError === e && e.message.include?('non-SSL')
311
+
312
+ req, _ = @buffer.split "\r\n\r\n"
313
+ request_line, headers = req.split "\r\n", 2
314
+
315
+ # below checks for request issues and changes error message accordingly
316
+ if !@env.key? REQUEST_METHOD
317
+ if request_line.count(' ') != 2
318
+ # maybe this is an SSL connection ?
319
+ raise e
320
+ else
321
+ method = request_line[/\A[^ ]+/]
322
+ raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
323
+ end
324
+ elsif !@env.key? REQUEST_PATH
325
+ path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
326
+ raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
327
+ elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
328
+ query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
329
+ raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
330
+ elsif !@env.key? SERVER_PROTOCOL
331
+ # protocol is bad
332
+ text = request_line[/[^ ]*\z/]
333
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
334
+ elsif !headers.empty?
335
+ # headers are bad
336
+ hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
337
+ raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
338
+ end
339
+ end
340
+
341
+ # processes the `env` and the request body
342
+ def process_env_body
343
+ temp = setup_body
344
+ normalize_env
345
+ req_env_post_parse
346
+ raise HttpParserError if @error_status_code
347
+ temp
348
+ end
349
+
303
350
  def timeout!
304
351
  write_error(408) if in_data_phase
305
352
  raise ConnectionError
@@ -316,12 +363,12 @@ module Puma
316
363
  return @peerip if @peerip
317
364
 
318
365
  if @remote_addr_header
319
- hdr = (@env[@remote_addr_header] || @io.peeraddr.last).split(/[\s,]/).first
366
+ hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
320
367
  @peerip = hdr
321
368
  return hdr
322
369
  end
323
370
 
324
- @peerip ||= @io.peeraddr.last
371
+ @peerip ||= socket_peerip
325
372
  end
326
373
 
327
374
  def peer_family
@@ -356,38 +403,56 @@ module Puma
356
403
 
357
404
  private
358
405
 
406
+ IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
407
+ private_constant :IPV4_MAPPED_IPV6_PREFIX
408
+
409
+ def socket_peerip
410
+ unmap_ipv6(@io.peeraddr.last)
411
+ end
412
+
413
+ # Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
414
+ # their IPv4 form. These addresses appear when IPv4 clients connect to
415
+ # a dual-stack IPv6 socket.
416
+ def unmap_ipv6(addr)
417
+ addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
418
+ end
419
+
420
+ # Checks the request `Transfer-Encoding` and/or `Content-Length` to see if
421
+ # they are valid. Raises errors if not, otherwise reads the body.
422
+ # @return [Boolean] true if the body can be completely read, false otherwise
423
+ #
359
424
  def setup_body
360
425
  @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
361
426
 
362
427
  if @env[HTTP_EXPECT] == CONTINUE
363
- # TODO allow a hook here to check the headers before
364
- # going forward
428
+ # TODO allow a hook here to check the headers before going forward
365
429
  @io << HTTP_11_100
366
430
  @io.flush
367
431
  end
368
432
 
369
433
  @read_header = false
370
434
 
371
- body = @parser.body
435
+ parser_body = @parser.body
372
436
 
373
437
  te = @env[TRANSFER_ENCODING2]
374
438
  if te
375
439
  te_lwr = te.downcase
376
440
  if te.include? ','
377
- te_ary = te_lwr.split ','
441
+ te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
378
442
  te_count = te_ary.count CHUNKED
379
443
  te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
380
- if te_ary.last == CHUNKED && te_count == 1 && te_valid
381
- @env.delete TRANSFER_ENCODING2
382
- return setup_chunked_body body
383
- elsif te_count >= 1
444
+ if te_count > 1
384
445
  raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
446
+ elsif te_ary.last != CHUNKED
447
+ raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
385
448
  elsif !te_valid
386
449
  raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
387
450
  end
451
+ @env.delete TRANSFER_ENCODING2
452
+ return setup_chunked_body parser_body
388
453
  elsif te_lwr == CHUNKED
389
454
  @env.delete TRANSFER_ENCODING2
390
- return setup_chunked_body body
455
+ return setup_chunked_body parser_body
391
456
  elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
392
457
  raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
393
458
  else
@@ -402,10 +467,12 @@ module Puma
402
467
  if cl
403
468
  # cannot contain characters that are not \d, or be empty
404
469
  if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
470
+ @error_status_code = 400
471
+ @env[HTTP_CONNECTION] = 'close'
405
472
  raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
406
473
  end
407
474
  else
408
- @buffer = body.empty? ? nil : body
475
+ @buffer = parser_body.empty? ? nil : parser_body
409
476
  @body = EmptyBody
410
477
  set_ready
411
478
  return true
@@ -413,23 +480,25 @@ module Puma
413
480
 
414
481
  content_length = cl.to_i
415
482
 
416
- remain = content_length - body.bytesize
483
+ raise_above_http_content_limit if @http_content_length_limit&.< content_length
484
+
485
+ remain = content_length - parser_body.bytesize
417
486
 
418
487
  if remain <= 0
419
- # Part of the body is a pipelined request OR garbage. We'll deal with that later.
488
+ # Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
420
489
  if content_length == 0
421
490
  @body = EmptyBody
422
- if body.empty?
491
+ if parser_body.empty?
423
492
  @buffer = nil
424
493
  else
425
- @buffer = body
494
+ @buffer = parser_body
426
495
  end
427
496
  elsif remain == 0
428
- @body = StringIO.new body
497
+ @body = StringIO.new parser_body
429
498
  @buffer = nil
430
499
  else
431
- @body = StringIO.new(body[0,content_length])
432
- @buffer = body[content_length..-1]
500
+ @body = StringIO.new(parser_body[0,content_length])
501
+ @buffer = parser_body[content_length..-1]
433
502
  end
434
503
  set_ready
435
504
  return true
@@ -441,12 +510,12 @@ module Puma
441
510
  @body.binmode
442
511
  @tempfile = @body
443
512
  else
444
- # The body[0,0] trick is to get an empty string in the same
445
- # encoding as body.
446
- @body = StringIO.new body[0,0]
513
+ # The parser_body[0,0] trick is to get an empty string in the same
514
+ # encoding as parser_body.
515
+ @body = StringIO.new parser_body[0,0]
447
516
  end
448
517
 
449
- @body.write body
518
+ @body.write parser_body
450
519
 
451
520
  @body_remain = remain
452
521
 
@@ -462,40 +531,36 @@ module Puma
462
531
  # after this
463
532
  remain = @body_remain
464
533
 
465
- if remain > CHUNK_SIZE
466
- want = CHUNK_SIZE
467
- else
468
- want = remain
469
- end
534
+ # don't bother with reading zero bytes
535
+ unless remain.zero?
536
+ begin
537
+ chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
538
+ rescue IO::WaitReadable
539
+ return false
540
+ rescue SystemCallError, IOError
541
+ raise ConnectionError, "Connection error detected during read"
542
+ end
470
543
 
471
- begin
472
- chunk = @io.read_nonblock(want, @read_buffer)
473
- rescue IO::WaitReadable
474
- return false
475
- rescue SystemCallError, IOError
476
- raise ConnectionError, "Connection error detected during read"
477
- end
544
+ # No chunk means a closed socket
545
+ unless chunk
546
+ @body.close
547
+ @buffer = nil
548
+ set_ready
549
+ raise EOFError
550
+ end
478
551
 
479
- # No chunk means a closed socket
480
- unless chunk
481
- @body.close
482
- @buffer = nil
483
- set_ready
484
- raise EOFError
552
+ remain -= @body.write(chunk)
485
553
  end
486
554
 
487
- remain -= @body.write(chunk)
488
-
489
555
  if remain <= 0
490
556
  @body.rewind
491
557
  @buffer = nil
492
558
  set_ready
493
- return true
559
+ true
560
+ else
561
+ @body_remain = remain
562
+ false
494
563
  end
495
-
496
- @body_remain = remain
497
-
498
- false
499
564
  end
500
565
 
501
566
  def read_chunked_body
@@ -543,6 +608,10 @@ module Puma
543
608
 
544
609
  # @version 5.0.0
545
610
  def write_chunk(str)
611
+ if @http_content_length_limit&.< @chunked_content_length + str.bytesize
612
+ raise_above_http_content_limit
613
+ end
614
+
546
615
  @chunked_content_length += @body.write(str)
547
616
  end
548
617
 
@@ -675,8 +744,13 @@ module Puma
675
744
  @ready = true
676
745
  end
677
746
 
678
- def above_http_content_limit(value)
679
- @http_content_length_limit&.< value
747
+ def raise_above_http_content_limit
748
+ @http_content_length_limit_exceeded = true
749
+ @buffer = nil
750
+ @body = EmptyBody
751
+ @error_status_code = 413
752
+ @env[HTTP_CONNECTION] = 'close'
753
+ raise HttpParserError, "Payload Too Large"
680
754
  end
681
755
  end
682
756
  end