puma 6.3.0 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/puma/cluster.rb CHANGED
@@ -85,9 +85,7 @@ module Puma
85
85
  @workers << WorkerHandle.new(idx, pid, @phase, @options)
86
86
  end
87
87
 
88
- if @options[:fork_worker] &&
89
- @workers.all? {|x| x.phase == @phase}
90
-
88
+ if @options[:fork_worker] && all_workers_in_phase?
91
89
  @fork_writer << "0\n"
92
90
  end
93
91
  end
@@ -148,10 +146,22 @@ module Puma
148
146
  idx
149
147
  end
150
148
 
149
+ def worker_at(idx)
150
+ @workers.find { |w| w.index == idx }
151
+ end
152
+
151
153
  def all_workers_booted?
152
154
  @workers.count { |w| !w.booted? } == 0
153
155
  end
154
156
 
157
+ def all_workers_in_phase?
158
+ @workers.all? { |w| w.phase == @phase }
159
+ end
160
+
161
+ def all_workers_idle_timed_out?
162
+ (@workers.map(&:pid) - idle_timed_out_worker_pids).empty?
163
+ end
164
+
155
165
  def check_workers
156
166
  return if @next_check >= Time.now
157
167
 
@@ -276,7 +286,7 @@ module Puma
276
286
 
277
287
  # @version 5.0.0
278
288
  def fork_worker!
279
- if (worker = @workers.find { |w| w.index == 0 })
289
+ if (worker = worker_at 0)
280
290
  worker.phase += 1
281
291
  end
282
292
  phased_restart(true)
@@ -338,6 +348,8 @@ module Puma
338
348
  def run
339
349
  @status = :run
340
350
 
351
+ @idle_workers = {}
352
+
341
353
  output_header "cluster"
342
354
 
343
355
  # This is aligned with the output from Runner, see Runner#output_header
@@ -411,6 +423,8 @@ module Puma
411
423
 
412
424
  @master_read, @worker_write = read, @wakeup
413
425
 
426
+ @options[:worker_write] = @worker_write
427
+
414
428
  @config.run_hooks(:before_fork, nil, @log_writer)
415
429
 
416
430
  spawn_workers
@@ -426,6 +440,11 @@ module Puma
426
440
 
427
441
  while @status == :run
428
442
  begin
443
+ if all_workers_idle_timed_out?
444
+ log "- All workers reached idle timeout"
445
+ break
446
+ end
447
+
429
448
  if @phased_restart
430
449
  start_phased_restart
431
450
  @phased_restart = false
@@ -446,7 +465,7 @@ module Puma
446
465
 
447
466
  if req == "b" || req == "f"
448
467
  pid, idx = result.split(':').map(&:to_i)
449
- w = @workers.find {|x| x.index == idx}
468
+ w = worker_at idx
450
469
  w.pid = pid if w.pid.nil?
451
470
  end
452
471
 
@@ -463,24 +482,37 @@ module Puma
463
482
  when "t"
464
483
  w.term unless w.term?
465
484
  when "p"
466
- w.ping!(result.sub(/^\d+/,'').chomp)
485
+ status = result.sub(/^\d+/,'').chomp
486
+ w.ping!(status)
467
487
  @events.fire(:ping!, w)
488
+
489
+ if in_phased_restart && workers_not_booted.positive? && w0 = worker_at(0)
490
+ w0.ping!(status)
491
+ @events.fire(:ping!, w0)
492
+ end
493
+
468
494
  if !booted && @workers.none? {|worker| worker.last_status.empty?}
469
495
  @events.fire_on_booted!
470
496
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
471
497
  booted = true
472
498
  end
499
+ when "i"
500
+ if @idle_workers[pid]
501
+ @idle_workers.delete pid
502
+ else
503
+ @idle_workers[pid] = true
504
+ end
473
505
  end
474
506
  else
475
507
  log "! Out-of-sync worker list, no #{pid} worker"
476
508
  end
477
509
  end
510
+
478
511
  if in_phased_restart && workers_not_booted.zero?
479
512
  @events.fire_on_booted!
480
513
  debug_loaded_extensions("Loaded Extensions - master:") if @log_writer.debug?
481
514
  in_phased_restart = false
482
515
  end
483
-
484
516
  rescue Interrupt
485
517
  @status = :stop
486
518
  end
@@ -509,10 +541,28 @@ module Puma
509
541
  # loops thru @workers, removing workers that exited, and calling
510
542
  # `#term` if needed
511
543
  def wait_workers
544
+ # Reap all children, known workers or otherwise.
545
+ # If puma has PID 1, as it's common in containerized environments,
546
+ # then it's responsible for reaping orphaned processes, so we must reap
547
+ # all our dead children, regardless of whether they are workers we spawned
548
+ # or some reattached processes.
549
+ reaped_children = {}
550
+ loop do
551
+ begin
552
+ pid, status = Process.wait2(-1, Process::WNOHANG)
553
+ break unless pid
554
+ reaped_children[pid] = status
555
+ rescue Errno::ECHILD
556
+ break
557
+ end
558
+ end
559
+
512
560
  @workers.reject! do |w|
513
561
  next false if w.pid.nil?
514
562
  begin
515
- if Process.wait(w.pid, Process::WNOHANG)
563
+ # When `fork_worker` is enabled, some worker may not be direct children, but grand children.
564
+ # Because of this they won't be reaped by `Process.wait2(-1)`, so we need to check them individually)
565
+ if reaped_children.delete(w.pid) || (@options[:fork_worker] && Process.wait(w.pid, Process::WNOHANG))
516
566
  true
517
567
  else
518
568
  w.term if w.term?
@@ -529,6 +579,11 @@ module Puma
529
579
  end
530
580
  end
531
581
  end
582
+
583
+ # Log unknown children
584
+ reaped_children.each do |pid, status|
585
+ log "! reaped unknown child process pid=#{pid} status=#{status}"
586
+ end
532
587
  end
533
588
 
534
589
  # @version 5.0.0
@@ -536,14 +591,18 @@ module Puma
536
591
  @workers.each do |w|
537
592
  if !w.term? && w.ping_timeout <= Time.now
538
593
  details = if w.booted?
539
- "(worker failed to check in within #{@options[:worker_timeout]} seconds)"
594
+ "(Worker #{w.index} failed to check in within #{@options[:worker_timeout]} seconds)"
540
595
  else
541
- "(worker failed to boot within #{@options[:worker_boot_timeout]} seconds)"
596
+ "(Worker #{w.index} failed to boot within #{@options[:worker_boot_timeout]} seconds)"
542
597
  end
543
598
  log "! Terminating timed out worker #{details}: #{w.pid}"
544
599
  w.kill
545
600
  end
546
601
  end
547
602
  end
603
+
604
+ def idle_timed_out_worker_pids
605
+ @idle_workers.keys
606
+ end
548
607
  end
549
608
  end
@@ -3,7 +3,7 @@
3
3
  require_relative 'rack/builder'
4
4
  require_relative 'plugin'
5
5
  require_relative 'const'
6
- # note that dsl is loaded at end of file, requires ConfigDefault constants
6
+ require_relative 'dsl'
7
7
 
8
8
  module Puma
9
9
  # A class used for storing "leveled" configuration options.
@@ -133,8 +133,10 @@ module Puma
133
133
  debug: false,
134
134
  early_hints: nil,
135
135
  environment: 'development'.freeze,
136
- # Number of seconds to wait until we get the first data for the request
136
+ # Number of seconds to wait until we get the first data for the request.
137
137
  first_data_timeout: 30,
138
+ # Number of seconds to wait until the next request before shutting down.
139
+ idle_timeout: nil,
138
140
  io_selector_backend: :auto,
139
141
  log_requests: false,
140
142
  logger: STDOUT,
@@ -385,5 +387,3 @@ module Puma
385
387
  end
386
388
  end
387
389
  end
388
-
389
- require_relative 'dsl'
data/lib/puma/const.rb CHANGED
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "6.3.0"
104
- CODE_NAME = "Mugi No Toki Itaru"
103
+ PUMA_VERSION = VERSION = "6.4.2"
104
+ CODE_NAME = "The Eagle of Durango"
105
105
 
106
106
  PUMA_SERVER_STRING = ["puma", PUMA_VERSION, CODE_NAME].join(" ").freeze
107
107
 
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'optparse'
4
- require_relative 'state_file'
5
4
  require_relative 'const'
6
5
  require_relative 'detect'
7
- require_relative 'configuration'
8
6
  require 'uri'
9
7
  require 'socket'
10
8
 
@@ -126,6 +124,9 @@ module Puma
126
124
  end
127
125
 
128
126
  if @config_file
127
+ require_relative 'configuration'
128
+ require_relative 'log_writer'
129
+
129
130
  config = Puma::Configuration.new({ config_files: [@config_file] }, {})
130
131
  config.load
131
132
  @state ||= config.options[:state]
@@ -149,6 +150,8 @@ module Puma
149
150
  raise "State file not found: #{@state}"
150
151
  end
151
152
 
153
+ require_relative 'state_file'
154
+
152
155
  sf = Puma::StateFile.new
153
156
  sf.load @state
154
157
 
@@ -164,22 +167,26 @@ module Puma
164
167
  def send_request
165
168
  uri = URI.parse @control_url
166
169
 
170
+ host = uri.host
171
+
167
172
  # create server object by scheme
168
173
  server =
169
174
  case uri.scheme
170
175
  when 'ssl'
171
176
  require 'openssl'
177
+ host = host[1..-2] if host&.start_with? '['
172
178
  OpenSSL::SSL::SSLSocket.new(
173
- TCPSocket.new(uri.host, uri.port),
179
+ TCPSocket.new(host, uri.port),
174
180
  OpenSSL::SSL::SSLContext.new)
175
181
  .tap { |ssl| ssl.sync_close = true } # default is false
176
182
  .tap(&:connect)
177
183
  when 'tcp'
178
- TCPSocket.new uri.host, uri.port
184
+ host = host[1..-2] if host&.start_with? '['
185
+ TCPSocket.new host, uri.port
179
186
  when 'unix'
180
187
  # check for abstract UNIXSocket
181
188
  UNIXSocket.new(@control_url.start_with?('unix://@') ?
182
- "\0#{uri.host}#{uri.path}" : "#{uri.host}#{uri.path}")
189
+ "\0#{host}#{uri.path}" : "#{host}#{uri.path}")
183
190
  else
184
191
  raise "Invalid scheme: #{uri.scheme}"
185
192
  end
data/lib/puma/detect.rb CHANGED
@@ -12,15 +12,14 @@ module Puma
12
12
 
13
13
  IS_JRUBY = Object.const_defined? :JRUBY_VERSION
14
14
 
15
- IS_OSX = RUBY_PLATFORM.include? 'darwin'
15
+ IS_OSX = RUBY_DESCRIPTION.include? 'darwin'
16
16
 
17
- IS_WINDOWS = !!(RUBY_PLATFORM =~ /mswin|ming|cygwin/) ||
18
- IS_JRUBY && RUBY_DESCRIPTION.include?('mswin')
17
+ IS_WINDOWS = RUBY_DESCRIPTION.match?(/mswin|ming|cygwin/)
19
18
 
20
19
  IS_LINUX = !(IS_OSX || IS_WINDOWS)
21
20
 
22
21
  # @version 5.2.0
23
- IS_MRI = (RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?)
22
+ IS_MRI = RUBY_ENGINE == 'ruby'
24
23
 
25
24
  def self.jruby?
26
25
  IS_JRUBY
data/lib/puma/dsl.rb CHANGED
@@ -315,16 +315,22 @@ module Puma
315
315
  bind URI::Generic.build(scheme: 'tcp', host: host, port: Integer(port)).to_s
316
316
  end
317
317
 
318
+ # Define how long the tcp socket stays open, if no data has been received.
319
+ # @see Puma::Server.new
320
+ def first_data_timeout(seconds)
321
+ @options[:first_data_timeout] = Integer(seconds)
322
+ end
323
+
318
324
  # Define how long persistent connections can be idle before Puma closes them.
319
325
  # @see Puma::Server.new
320
326
  def persistent_timeout(seconds)
321
327
  @options[:persistent_timeout] = Integer(seconds)
322
328
  end
323
329
 
324
- # Define how long the tcp socket stays open, if no data has been received.
330
+ # If a new request is not received within this number of seconds, begin shutting down.
325
331
  # @see Puma::Server.new
326
- def first_data_timeout(seconds)
327
- @options[:first_data_timeout] = Integer(seconds)
332
+ def idle_timeout(seconds)
333
+ @options[:idle_timeout] = Integer(seconds)
328
334
  end
329
335
 
330
336
  # Work around leaky apps that leave garbage in Thread locals
@@ -510,6 +516,12 @@ module Puma
510
516
  # `true`, which sets reuse 'on' with default values, or a hash, with `:size`
511
517
  # and/or `:timeout` keys, each with integer values.
512
518
  #
519
+ # The `cert:` options hash parameter can be the path to a certificate
520
+ # file including all intermediate certificates in PEM format.
521
+ #
522
+ # The `cert_pem:` options hash parameter can be String containing the
523
+ # cerificate and all intermediate certificates in PEM format.
524
+ #
513
525
  # @example
514
526
  # ssl_bind '127.0.0.1', '9292', {
515
527
  # cert: path_to_cert,
@@ -717,6 +729,51 @@ module Puma
717
729
  process_hook :before_refork, key, block, 'on_refork'
718
730
  end
719
731
 
732
+ # Provide a block to be executed just before a thread is added to the thread
733
+ # pool. Be careful: while the block executes, thread creation is delayed, and
734
+ # probably a request will have to wait too! The new thread will not be added to
735
+ # the threadpool until the provided block returns.
736
+ #
737
+ # Return values are ignored.
738
+ # Raising an exception will log a warning.
739
+ #
740
+ # This hook is useful for doing something when the thread pool grows.
741
+ #
742
+ # This can be called multiple times to add several hooks.
743
+ #
744
+ # @example
745
+ # on_thread_start do
746
+ # puts 'On thread start...'
747
+ # end
748
+ def on_thread_start(&block)
749
+ @options[:before_thread_start] ||= []
750
+ @options[:before_thread_start] << block
751
+ end
752
+
753
+ # Provide a block to be executed after a thread is trimmed from the thread
754
+ # pool. Be careful: while this block executes, Puma's main loop is
755
+ # blocked, so no new requests will be picked up.
756
+ #
757
+ # This hook only runs when a thread in the threadpool is trimmed by Puma.
758
+ # It does not run when a thread dies due to exceptions or any other cause.
759
+ #
760
+ # Return values are ignored.
761
+ # Raising an exception will log a warning.
762
+ #
763
+ # This hook is useful for cleaning up thread local resources when a thread
764
+ # is trimmed.
765
+ #
766
+ # This can be called multiple times to add several hooks.
767
+ #
768
+ # @example
769
+ # on_thread_exit do
770
+ # puts 'On thread exit...'
771
+ # end
772
+ def on_thread_exit(&block)
773
+ @options[:before_thread_exit] ||= []
774
+ @options[:before_thread_exit] << block
775
+ end
776
+
720
777
  # Code to run out-of-band when the worker is idle.
721
778
  # These hooks run immediately after a request has finished
722
779
  # processing and there are no busy threads on the worker.
@@ -848,7 +905,8 @@ module Puma
848
905
  # not a request timeout, it is to protect against a hung or dead process.
849
906
  # Setting this value will not protect against slow requests.
850
907
  #
851
- # The minimum value is 6 seconds, the default value is 60 seconds.
908
+ # This value must be greater than worker_check_interval.
909
+ # The default value is 60 seconds.
852
910
  #
853
911
  # @note Cluster mode only.
854
912
  # @example
@@ -1132,8 +1190,10 @@ module Puma
1132
1190
 
1133
1191
  def warn_if_in_single_mode(hook_name)
1134
1192
  return if @options[:silence_fork_callback_warning]
1135
-
1136
- if (@options[:workers] || 0) == 0
1193
+ # user_options (CLI) have precedence over config file
1194
+ workers_val = @config.options.user_options[:workers] || @options[:workers] ||
1195
+ @config.puma_default_options[:workers] || 0
1196
+ if workers_val == 0
1137
1197
  log_string =
1138
1198
  "Warning: You specified code to run in a `#{hook_name}` block, " \
1139
1199
  "but Puma is not configured to run in cluster mode (worker count > 0 ), " \
@@ -51,6 +51,8 @@ module Puma
51
51
  unless params['ca']
52
52
  log_writer.error "Please specify the SSL ca via 'ca='"
53
53
  end
54
+ # needed for Puma::MiniSSL::Socket#peercert, env['puma.peercert']
55
+ require 'openssl'
54
56
  end
55
57
 
56
58
  ctx.ca = params['ca'] if params['ca']
data/lib/puma/minissl.rb CHANGED
@@ -184,6 +184,11 @@ module Puma
184
184
  @socket.peeraddr
185
185
  end
186
186
 
187
+ # OpenSSL is loaded in `MiniSSL::ContextBuilder` when
188
+ # `MiniSSL::Context#verify_mode` is not `VERIFY_NONE`.
189
+ # When `VERIFY_NONE`, `MiniSSL::Engine#peercert` is nil, regardless of
190
+ # whether the client sends a cert.
191
+ # @return [OpenSSL::X509::Certificate, nil]
187
192
  # @!attribute [r] peercert
188
193
  def peercert
189
194
  return @peercert if @peercert
data/lib/puma/null_io.rb CHANGED
@@ -18,8 +18,22 @@ module Puma
18
18
 
19
19
  # Mimics IO#read with no data.
20
20
  #
21
- def read(count = nil, _buffer = nil)
22
- count && count > 0 ? nil : ""
21
+ def read(length = nil, buffer = nil)
22
+ if length.to_i < 0
23
+ raise ArgumentError, "(negative length #{length} given)"
24
+ end
25
+
26
+ buffer = if buffer.nil?
27
+ "".b
28
+ else
29
+ String.try_convert(buffer) or raise TypeError, "no implicit conversion of #{buffer.class} into String"
30
+ end
31
+ buffer.clear
32
+ if length.to_i > 0
33
+ nil
34
+ else
35
+ buffer
36
+ end
23
37
  end
24
38
 
25
39
  def rewind
@@ -34,7 +34,7 @@ module Puma::Rack
34
34
  end
35
35
 
36
36
  location = location.chomp('/')
37
- match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n')
37
+ match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
38
38
 
39
39
  [host, location, match, app]
40
40
  }.sort_by do |(host, location, _, _)|
data/lib/puma/runner.rb CHANGED
@@ -70,12 +70,16 @@ module Puma
70
70
 
71
71
  app = Puma::App::Status.new @launcher, token
72
72
 
73
- # A Reactor is not created aand nio4r is not loaded when 'queue_requests: false'
73
+ # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
74
74
  # Use `nil` for events, no hooks in control server
75
75
  control = Puma::Server.new app, nil,
76
76
  { min_threads: 0, max_threads: 1, queue_requests: false, log_writer: @log_writer }
77
77
 
78
- control.binder.parse [str], nil, 'Starting control server'
78
+ begin
79
+ control.binder.parse [str], nil, 'Starting control server'
80
+ rescue Errno::EADDRINUSE, Errno::EACCES => e
81
+ raise e, "Error: Control server address '#{str}' is already in use. Original error: #{e.message}"
82
+ end
79
83
 
80
84
  control.run thread_name: 'ctl'
81
85
  @control = control
data/lib/puma/server.rb CHANGED
@@ -15,7 +15,6 @@ require_relative 'request'
15
15
 
16
16
  require 'socket'
17
17
  require 'io/wait' unless Puma::HAS_NATIVE_IO_WAIT
18
- require 'forwardable'
19
18
 
20
19
  module Puma
21
20
 
@@ -32,7 +31,6 @@ module Puma
32
31
  class Server
33
32
  include Puma::Const
34
33
  include Request
35
- extend Forwardable
36
34
 
37
35
  attr_reader :thread
38
36
  attr_reader :log_writer
@@ -48,9 +46,6 @@ module Puma
48
46
  attr_accessor :app
49
47
  attr_accessor :binder
50
48
 
51
- def_delegators :@binder, :add_tcp_listener, :add_ssl_listener,
52
- :add_unix_listener, :connected_ports
53
-
54
49
  THREAD_LOCAL_KEY = :puma_server
55
50
 
56
51
  # Create a server for the rack app +app+.
@@ -86,15 +81,18 @@ module Puma
86
81
  UserFileDefaultOptions.new(options, Configuration::DEFAULTS)
87
82
  end
88
83
 
89
- @log_writer = @options.fetch :log_writer, LogWriter.stdio
90
- @early_hints = @options[:early_hints]
91
- @first_data_timeout = @options[:first_data_timeout]
92
- @min_threads = @options[:min_threads]
93
- @max_threads = @options[:max_threads]
94
- @persistent_timeout = @options[:persistent_timeout]
95
- @queue_requests = @options[:queue_requests]
96
- @max_fast_inline = @options[:max_fast_inline]
97
- @io_selector_backend = @options[:io_selector_backend]
84
+ @clustered = (@options.fetch :workers, 0) > 0
85
+ @worker_write = @options[:worker_write]
86
+ @log_writer = @options.fetch :log_writer, LogWriter.stdio
87
+ @early_hints = @options[:early_hints]
88
+ @first_data_timeout = @options[:first_data_timeout]
89
+ @persistent_timeout = @options[:persistent_timeout]
90
+ @idle_timeout = @options[:idle_timeout]
91
+ @min_threads = @options[:min_threads]
92
+ @max_threads = @options[:max_threads]
93
+ @queue_requests = @options[:queue_requests]
94
+ @max_fast_inline = @options[:max_fast_inline]
95
+ @io_selector_backend = @options[:io_selector_backend]
98
96
  @http_content_length_limit = @options[:http_content_length_limit]
99
97
 
100
98
  # make this a hash, since we prefer `key?` over `include?`
@@ -121,6 +119,8 @@ module Puma
121
119
  @precheck_closing = true
122
120
 
123
121
  @requests_count = 0
122
+
123
+ @idle_timeout_reached = false
124
124
  end
125
125
 
126
126
  def inherit_binder(bind)
@@ -330,8 +330,28 @@ module Puma
330
330
 
331
331
  while @status == :run || (drain && shutting_down?)
332
332
  begin
333
- ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : nil)
334
- break unless ios
333
+ ios = IO.select sockets, nil, nil, (shutting_down? ? 0 : @idle_timeout)
334
+ unless ios
335
+ unless shutting_down?
336
+ @idle_timeout_reached = true
337
+
338
+ if @clustered
339
+ @worker_write << "i#{Process.pid}\n" rescue nil
340
+ next
341
+ else
342
+ @log_writer.log "- Idle timeout reached"
343
+ @status = :stop
344
+ end
345
+ end
346
+
347
+ break
348
+ end
349
+
350
+ if @idle_timeout_reached && @clustered
351
+ @idle_timeout_reached = false
352
+ @worker_write << "i#{Process.pid}\n" rescue nil
353
+ end
354
+
335
355
  ios.first.each do |sock|
336
356
  if sock == check
337
357
  break if handle_check
@@ -367,6 +387,7 @@ module Puma
367
387
  @queue_requests = false
368
388
  @reactor.shutdown
369
389
  end
390
+
370
391
  graceful_shutdown if @status == :stop || @status == :restart
371
392
  rescue Exception => e
372
393
  @log_writer.unknown_error e, nil, "Exception handling servers"
@@ -476,7 +497,7 @@ module Puma
476
497
  end
477
498
  true
478
499
  rescue StandardError => e
479
- client_error(e, client)
500
+ client_error(e, client, requests)
480
501
  # The ensure tries to close +client+ down
481
502
  requests > 0
482
503
  ensure
@@ -504,22 +525,22 @@ module Puma
504
525
  # :nocov:
505
526
 
506
527
  # Handle various error types thrown by Client I/O operations.
507
- def client_error(e, client)
528
+ def client_error(e, client, requests = 1)
508
529
  # Swallow, do not log
509
530
  return if [ConnectionError, EOFError].include?(e.class)
510
531
 
511
- lowlevel_error(e, client.env)
512
532
  case e
513
533
  when MiniSSL::SSLError
534
+ lowlevel_error(e, client.env)
514
535
  @log_writer.ssl_error e, client.io
515
536
  when HttpParserError
516
- client.write_error(400)
537
+ response_to_error(client, requests, e, 400)
517
538
  @log_writer.parse_error e, client
518
539
  when HttpParserError501
519
- client.write_error(501)
540
+ response_to_error(client, requests, e, 501)
520
541
  @log_writer.parse_error e, client
521
542
  else
522
- client.write_error(500)
543
+ response_to_error(client, requests, e, 500)
523
544
  @log_writer.unknown_error e, nil, "Read"
524
545
  end
525
546
  end
@@ -541,10 +562,17 @@ module Puma
541
562
  backtrace = e.backtrace.nil? ? '<no backtrace available>' : e.backtrace.join("\n")
542
563
  [status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{backtrace}"]]
543
564
  else
544
- [status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
565
+ [status, {}, [""]]
545
566
  end
546
567
  end
547
568
 
569
+ def response_to_error(client, requests, err, status_code)
570
+ status, headers, res_body = lowlevel_error(err, client.env, status_code)
571
+ prepare_response(status, headers, res_body, requests, client)
572
+ client.write_error(status_code)
573
+ end
574
+ private :response_to_error
575
+
548
576
  # Wait for all outstanding requests to finish.
549
577
  #
550
578
  def graceful_shutdown
@@ -623,5 +651,27 @@ module Puma
623
651
  def stats
624
652
  STAT_METHODS.map {|name| [name, send(name) || 0]}.to_h
625
653
  end
654
+
655
+ # below are 'delegations' to binder
656
+ # remove in Puma 7?
657
+
658
+
659
+ def add_tcp_listener(host, port, optimize_for_latency = true, backlog = 1024)
660
+ @binder.add_tcp_listener host, port, optimize_for_latency, backlog
661
+ end
662
+
663
+ def add_ssl_listener(host, port, ctx, optimize_for_latency = true,
664
+ backlog = 1024)
665
+ @binder.add_ssl_listener host, port, ctx, optimize_for_latency, backlog
666
+ end
667
+
668
+ def add_unix_listener(path, umask = nil, mode = nil, backlog = 1024)
669
+ @binder.add_unix_listener path, umask, mode, backlog
670
+ end
671
+
672
+ # @!attribute [r] connected_ports
673
+ def connected_ports
674
+ @binder.connected_ports
675
+ end
626
676
  end
627
677
  end