puma 6.0.0 → 6.6.0

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +392 -13
  3. data/LICENSE +0 -0
  4. data/README.md +135 -29
  5. data/bin/puma-wild +0 -0
  6. data/docs/architecture.md +0 -0
  7. data/docs/compile_options.md +0 -0
  8. data/docs/deployment.md +0 -0
  9. data/docs/fork_worker.md +11 -1
  10. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  11. data/docs/images/puma-connection-flow.png +0 -0
  12. data/docs/images/puma-general-arch.png +0 -0
  13. data/docs/java_options.md +54 -0
  14. data/docs/jungle/README.md +0 -0
  15. data/docs/jungle/rc.d/README.md +0 -0
  16. data/docs/jungle/rc.d/puma.conf +0 -0
  17. data/docs/kubernetes.md +12 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +4 -0
  20. data/docs/rails_dev_mode.md +0 -0
  21. data/docs/restart.md +1 -0
  22. data/docs/signals.md +2 -2
  23. data/docs/stats.md +8 -3
  24. data/docs/systemd.md +13 -7
  25. data/docs/testing_benchmarks_local_files.md +0 -0
  26. data/docs/testing_test_rackup_ci_files.md +0 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  28. data/ext/puma_http11/ext_help.h +0 -0
  29. data/ext/puma_http11/extconf.rb +21 -14
  30. data/ext/puma_http11/http11_parser.c +0 -0
  31. data/ext/puma_http11/http11_parser.h +0 -0
  32. data/ext/puma_http11/http11_parser.java.rl +0 -0
  33. data/ext/puma_http11/http11_parser.rl +0 -0
  34. data/ext/puma_http11/http11_parser_common.rl +0 -0
  35. data/ext/puma_http11/mini_ssl.c +107 -10
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +30 -7
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +0 -0
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +2 -1
  40. data/ext/puma_http11/puma_http11.c +4 -1
  41. data/lib/puma/app/status.rb +1 -1
  42. data/lib/puma/binder.rb +26 -15
  43. data/lib/puma/cli.rb +13 -5
  44. data/lib/puma/client.rb +113 -26
  45. data/lib/puma/cluster/worker.rb +14 -6
  46. data/lib/puma/cluster/worker_handle.rb +4 -5
  47. data/lib/puma/cluster.rb +93 -22
  48. data/lib/puma/commonlogger.rb +21 -14
  49. data/lib/puma/configuration.rb +42 -22
  50. data/lib/puma/const.rb +149 -89
  51. data/lib/puma/control_cli.rb +16 -9
  52. data/lib/puma/detect.rb +5 -4
  53. data/lib/puma/dsl.rb +432 -40
  54. data/lib/puma/error_logger.rb +6 -5
  55. data/lib/puma/events.rb +0 -0
  56. data/lib/puma/io_buffer.rb +10 -0
  57. data/lib/puma/jruby_restart.rb +0 -16
  58. data/lib/puma/json_serialization.rb +0 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +0 -0
  60. data/lib/puma/launcher.rb +29 -29
  61. data/lib/puma/log_writer.rb +23 -13
  62. data/lib/puma/minissl/context_builder.rb +4 -0
  63. data/lib/puma/minissl.rb +23 -0
  64. data/lib/puma/null_io.rb +42 -2
  65. data/lib/puma/plugin/systemd.rb +90 -0
  66. data/lib/puma/plugin/tmp_restart.rb +0 -0
  67. data/lib/puma/plugin.rb +0 -0
  68. data/lib/puma/rack/builder.rb +2 -2
  69. data/lib/puma/rack/urlmap.rb +1 -1
  70. data/lib/puma/rack_default.rb +18 -3
  71. data/lib/puma/reactor.rb +17 -8
  72. data/lib/puma/request.rb +207 -126
  73. data/lib/puma/runner.rb +26 -4
  74. data/lib/puma/sd_notify.rb +146 -0
  75. data/lib/puma/server.rb +121 -49
  76. data/lib/puma/single.rb +3 -1
  77. data/lib/puma/state_file.rb +2 -2
  78. data/lib/puma/thread_pool.rb +56 -9
  79. data/lib/puma/util.rb +1 -1
  80. data/lib/puma.rb +1 -3
  81. data/lib/rack/handler/puma.rb +116 -86
  82. data/tools/Dockerfile +2 -2
  83. data/tools/trickletest.rb +0 -0
  84. metadata +12 -13
  85. data/lib/puma/systemd.rb +0 -47
data/lib/puma/request.rb CHANGED
@@ -14,15 +14,18 @@ module Puma
14
14
  #
15
15
  module Request # :nodoc:
16
16
 
17
- # determines whether to write body to io_buffer first, or straight to socket
18
- # also fixes max size of chunked body read when bosy is an IO.
19
- BODY_LEN_MAX = 1_024 * 256
17
+ # Single element array body: smaller bodies are written to io_buffer first,
18
+ # then a single write from io_buffer. Larger sizes are written separately.
19
+ # Also fixes max size of chunked file body read.
20
+ BODY_LEN_MAX = 1_024 * 256
20
21
 
21
- # size divide for using copy_stream on body
22
+ # File body: smaller bodies are combined with io_buffer, then written to
23
+ # socket. Larger bodies are written separately using `copy_stream`
22
24
  IO_BODY_MAX = 1_024 * 64
23
25
 
24
- # max size for io_buffer, force write when exceeded
25
- IO_BUFFER_LEN_MAX = 1_024 * 1_024 * 4
26
+ # Array body: elements are collected in io_buffer. When io_buffer's size
27
+ # exceeds value, they are written to the socket.
28
+ IO_BUFFER_LEN_MAX = 1_024 * 512
26
29
 
27
30
  SOCKET_WRITE_ERR_MSG = "Socket timeout writing data"
28
31
 
@@ -41,16 +44,21 @@ module Puma
41
44
  #
42
45
  # Finally, it'll return +true+ on keep-alive connections.
43
46
  # @param client [Puma::Client]
44
- # @param io_buffer [Puma::IOBuffer]
45
47
  # @param requests [Integer]
46
48
  # @return [Boolean,:async]
47
49
  #
48
- def handle_request(client, io_buffer, requests)
50
+ def handle_request(client, requests)
49
51
  env = client.env
52
+ io_buffer = client.io_buffer
50
53
  socket = client.io # io may be a MiniSSL::Socket
54
+ app_body = nil
51
55
 
52
56
  return false if closed_socket?(socket)
53
57
 
58
+ if client.http_content_length_limit_exceeded
59
+ return prepare_response(413, {}, ["Payload Too Large"], requests, client)
60
+ end
61
+
54
62
  normalize_env env, client
55
63
 
56
64
  env[PUMA_SOCKET] = socket
@@ -68,7 +76,9 @@ module Puma
68
76
  if @early_hints
69
77
  env[EARLY_HINTS] = lambda { |headers|
70
78
  begin
71
- fast_write_str socket, str_early_hints(headers)
79
+ unless (str = str_early_hints headers).empty?
80
+ fast_write_str socket, "HTTP/1.1 103 Early Hints\r\n#{str}\r\n"
81
+ end
72
82
  rescue ConnectionError => e
73
83
  @log_writer.debug_error e
74
84
  # noop, if we lost the socket we just won't send the early hints
@@ -84,15 +94,20 @@ module Puma
84
94
  env[RACK_AFTER_REPLY] ||= []
85
95
 
86
96
  begin
87
- if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
88
- status, headers, res_body = @thread_pool.with_force_shutdown do
97
+ if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
98
+ status, headers, app_body = @thread_pool.with_force_shutdown do
89
99
  @app.call(env)
90
100
  end
91
101
  else
92
102
  @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
93
- status, headers, res_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
103
+ status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
94
104
  end
95
105
 
106
+ # app_body needs to always be closed, hold value in case lowlevel_error
107
+ # is called
108
+ res_body = app_body
109
+
110
+ # full hijack, app called env['rack.hijack']
96
111
  return :async if client.hijacked
97
112
 
98
113
  status = status.to_i
@@ -114,114 +129,141 @@ module Puma
114
129
 
115
130
  status, headers, res_body = lowlevel_error(e, env, 500)
116
131
  end
117
- prepare_response(status, headers, res_body, io_buffer, requests, client)
132
+ prepare_response(status, headers, res_body, requests, client)
133
+ ensure
134
+ io_buffer.reset
135
+ uncork_socket client.io
136
+ app_body.close if app_body.respond_to? :close
137
+ client&.tempfile_close
138
+ after_reply = env[RACK_AFTER_REPLY] || []
139
+ begin
140
+ after_reply.each { |o| o.call }
141
+ rescue StandardError => e
142
+ @log_writer.debug_error e
143
+ end unless after_reply.empty?
118
144
  end
119
145
 
120
146
  # Assembles the headers and prepares the body for actually sending the
121
- # response via #fast_write_response.
147
+ # response via `#fast_write_response`.
122
148
  #
123
149
  # @param status [Integer] the status returned by the Rack application
124
150
  # @param headers [Hash] the headers returned by the Rack application
125
- # @param app_body [Array] the body returned by the Rack application or
126
- # a call to `lowlevel_error`
127
- # @param io_buffer [Puma::IOBuffer] modified in place
151
+ # @param res_body [Array] the body returned by the Rack application or
152
+ # a call to `Server#lowlevel_error`
128
153
  # @param requests [Integer] number of inline requests handled
129
154
  # @param client [Puma::Client]
130
- # @return [Boolean,:async]
131
- def prepare_response(status, headers, app_body, io_buffer, requests, client)
155
+ # @return [Boolean,:async] keep-alive status or `:async`
156
+ def prepare_response(status, headers, res_body, requests, client)
132
157
  env = client.env
133
- socket = client.io
134
-
135
- after_reply = env[RACK_AFTER_REPLY] || []
158
+ socket = client.io
159
+ io_buffer = client.io_buffer
136
160
 
137
161
  return false if closed_socket?(socket)
138
162
 
139
- resp_info = str_headers(env, status, headers, app_body, io_buffer, requests, client)
140
-
141
- # below converts app_body into body, dependent on app_body's characteristics, and
142
- # resp_info[:content_length] will be set if it can be determined
143
- if !resp_info[:content_length] && !resp_info[:transfer_encoding] && status != 204
144
- if app_body.respond_to?(:to_ary)
145
- length = 0
146
- if array_body = app_body.to_ary
147
- body = array_body.map { |part| length += part.bytesize; part }
148
- elsif app_body.is_a?(::File) && app_body.respond_to?(:size)
149
- length = app_body.size
150
- elsif app_body.respond_to?(:each)
151
- body = []
152
- app_body.each { |part| length += part.bytesize; body << part }
153
- end
154
- resp_info[:content_length] = length
155
- elsif app_body.is_a?(File) && app_body.respond_to?(:size)
156
- resp_info[:content_length] = app_body.size
157
- body = app_body
158
- elsif app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
159
- File.readable?(fn = app_body.to_path)
160
- body = File.open fn, 'rb'
161
- resp_info[:content_length] = body.size
162
- else
163
- body = app_body
164
- end
165
- elsif !app_body.is_a?(::File) && app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
166
- File.readable?(fn = app_body.to_path)
167
- body = File.open fn, 'rb'
168
- resp_info[:content_length] = body.size
163
+ # Close the connection after a reasonable number of inline requests
164
+ # if the server is at capacity and the listener has a new connection ready.
165
+ # This allows Puma to service connections fairly when the number
166
+ # of concurrent connections exceeds the size of the threadpool.
167
+ force_keep_alive = if @enable_keep_alives
168
+ requests < @max_fast_inline ||
169
+ @thread_pool.busy_threads < @max_threads ||
170
+ !client.listener.to_io.wait_readable(0)
169
171
  else
170
- body = app_body
172
+ # Always set force_keep_alive to false if the server has keep-alives not enabled.
173
+ false
171
174
  end
172
175
 
173
- line_ending = LINE_END
176
+ resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
174
177
 
178
+ close_body = false
179
+ response_hijack = nil
175
180
  content_length = resp_info[:content_length]
176
181
  keep_alive = resp_info[:keep_alive]
177
182
 
178
- if app_body && !app_body.respond_to?(:each)
179
- response_hijack = app_body
183
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
184
+ # below converts app_body into body, dependent on app_body's characteristics, and
185
+ # content_length will be set if it can be determined
186
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
187
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
188
+ array_body.is_a?(Array)
189
+ body = array_body.compact
190
+ content_length = body.sum(&:bytesize)
191
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
192
+ body = res_body
193
+ content_length = body.size
194
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
195
+ body = File.open fn, 'rb'
196
+ content_length = body.size
197
+ close_body = true
198
+ else
199
+ body = res_body
200
+ end
201
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
202
+ File.readable?(fn = res_body.to_path)
203
+ body = File.open fn, 'rb'
204
+ content_length = body.size
205
+ close_body = true
206
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
207
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
208
+ # Sprockets::Asset
209
+ content_length = res_body.bytesize unless content_length
210
+ if (body_str = res_body.to_hash[:source])
211
+ body = [body_str]
212
+ else # avoid each and use a File object
213
+ body = File.open fn, 'rb'
214
+ close_body = true
215
+ end
216
+ else
217
+ body = res_body
218
+ end
180
219
  else
181
- response_hijack = resp_info[:response_hijack]
220
+ # partial hijack, from Rack spec:
221
+ # Servers must ignore the body part of the response tuple when the
222
+ # rack.hijack response header is present.
223
+ response_hijack = resp_info[:response_hijack] || res_body
182
224
  end
183
225
 
226
+ line_ending = LINE_END
227
+
184
228
  cork_socket socket
185
229
 
186
230
  if resp_info[:no_body]
187
- if content_length and status != 204
231
+ # 101 (Switching Protocols) doesn't return here or have content_length,
232
+ # it should be using `response_hijack`
233
+ unless status == 101
234
+ if content_length && status != 204
235
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
236
+ end
237
+
238
+ io_buffer << LINE_END
239
+ fast_write_str socket, io_buffer.read_and_reset
240
+ socket.flush
241
+ return keep_alive
242
+ end
243
+ else
244
+ if content_length
188
245
  io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
246
+ chunked = false
247
+ elsif !response_hijack && resp_info[:allow_chunked]
248
+ io_buffer << TRANSFER_ENCODING_CHUNKED
249
+ chunked = true
189
250
  end
190
-
191
- io_buffer << LINE_END
192
- fast_write_str socket, io_buffer.to_s
193
- return keep_alive
194
- end
195
- if content_length
196
- io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
197
- chunked = false
198
- elsif !response_hijack and resp_info[:allow_chunked]
199
- io_buffer << TRANSFER_ENCODING_CHUNKED
200
- chunked = true
201
251
  end
202
252
 
203
253
  io_buffer << line_ending
204
254
 
255
+ # partial hijack, we write headers, then hand the socket to the app via
256
+ # response_hijack.call
205
257
  if response_hijack
206
- fast_write_str socket, io_buffer.to_s
258
+ fast_write_str socket, io_buffer.read_and_reset
259
+ uncork_socket socket
207
260
  response_hijack.call socket
208
261
  return :async
209
262
  end
210
263
 
211
264
  fast_write_response socket, body, io_buffer, chunked, content_length.to_i
265
+ body.close if close_body
212
266
  keep_alive
213
- ensure
214
- io_buffer.reset
215
- resp_info = nil
216
- uncork_socket socket
217
- app_body.close if app_body.respond_to? :close
218
- client.tempfile&.unlink
219
-
220
- begin
221
- after_reply.each { |o| o.call }
222
- rescue StandardError => e
223
- @log_writer.debug_error e
224
- end unless after_reply.empty?
225
267
  end
226
268
 
227
269
  # @param env [Hash] see Puma::Client#env, from request
@@ -237,10 +279,10 @@ module Puma
237
279
 
238
280
  # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
239
281
  # and body segments (called by `fast_write_response`).
240
- # Writes a string to an io (normally `Client#io`) using `write_nonblock`.
282
+ # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
241
283
  # Large strings may not be written in one pass, especially if `io` is a
242
284
  # `MiniSSL::Socket`.
243
- # @param io [#write_nonblock] the io to write to
285
+ # @param socket [#write_nonblock] the request/response socket
244
286
  # @param str [String] the string written to the io
245
287
  # @raise [ConnectionError]
246
288
  #
@@ -249,7 +291,7 @@ module Puma
249
291
  byte_size = str.bytesize
250
292
  while n < byte_size
251
293
  begin
252
- n += socket.syswrite(n.zero? ? str : str.byteslice(n..-1))
294
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
253
295
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
254
296
  unless socket.wait_writable WRITE_TIMEOUT
255
297
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
@@ -267,39 +309,41 @@ module Puma
267
309
  # @param socket [#write] the response socket
268
310
  # @param body [Enumerable, File] the body object
269
311
  # @param io_buffer [Puma::IOBuffer] contains headers
270
- # @param chunk [Boolean]
312
+ # @param chunked [Boolean]
313
+ # @paramn content_length [Integer
271
314
  # @raise [ConnectionError]
272
315
  #
273
316
  def fast_write_response(socket, body, io_buffer, chunked, content_length)
274
- if body.is_a?(::File) || body.respond_to?(:read) || body.respond_to?(:readpartial)
317
+ if body.is_a?(::File) && body.respond_to?(:read)
275
318
  if chunked # would this ever happen?
276
- while part = body.read(BODY_LEN_MAX)
277
- io_buffer.append part.bytesize.to_s(16), LINE_END, part, LINE_END
319
+ while chunk = body.read(BODY_LEN_MAX)
320
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
278
321
  end
279
- io_buffer << CLOSE_CHUNKED
280
- fast_write_str socket, io_buffer.to_s
322
+ fast_write_str socket, CLOSE_CHUNKED
281
323
  else
282
324
  if content_length <= IO_BODY_MAX
283
- io_buffer.write body.sysread(content_length)
284
- fast_write_str socket, io_buffer.to_s
325
+ io_buffer.write body.read(content_length)
326
+ fast_write_str socket, io_buffer.read_and_reset
285
327
  else
286
- fast_write_str socket, io_buffer.to_s
328
+ fast_write_str socket, io_buffer.read_and_reset
287
329
  IO.copy_stream body, socket
288
330
  end
289
331
  end
290
- body.close
291
332
  elsif body.is_a?(::Array) && body.length == 1
292
- body_first = body.first
293
- if body_first.is_a?(::String) && body_first.bytesize >= BODY_LEN_MAX
333
+ body_first = nil
334
+ # using body_first = body.first causes issues?
335
+ body.each { |str| body_first ||= str }
336
+
337
+ if body_first.is_a?(::String) && body_first.bytesize < BODY_LEN_MAX
338
+ # smaller body, write to io_buffer first
339
+ io_buffer.write body_first
340
+ fast_write_str socket, io_buffer.read_and_reset
341
+ else
294
342
  # large body, write both header & body to socket
295
- fast_write_str socket, io_buffer.to_s
343
+ fast_write_str socket, io_buffer.read_and_reset
296
344
  fast_write_str socket, body_first
297
- else
298
- # smaller body, write to stream first
299
- io_buffer.write body_first
300
- fast_write_str socket, io_buffer.to_s
301
345
  end
302
- else
346
+ elsif body.is_a?(::Array)
303
347
  # for array bodies, flush io_buffer to socket when size is greater than
304
348
  # IO_BUFFER_LEN_MAX
305
349
  if chunked
@@ -307,8 +351,7 @@ module Puma
307
351
  next if (byte_size = part.bytesize).zero?
308
352
  io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
309
353
  if io_buffer.length > IO_BUFFER_LEN_MAX
310
- fast_write_str socket, io_buffer.to_s
311
- io_buffer.reset
354
+ fast_write_str socket, io_buffer.read_and_reset
312
355
  end
313
356
  end
314
357
  io_buffer.write CLOSE_CHUNKED
@@ -317,13 +360,37 @@ module Puma
317
360
  next if part.bytesize.zero?
318
361
  io_buffer.write part
319
362
  if io_buffer.length > IO_BUFFER_LEN_MAX
320
- fast_write_str socket, io_buffer.to_s
321
- io_buffer.reset
363
+ fast_write_str socket, io_buffer.read_and_reset
322
364
  end
323
365
  end
324
366
  end
325
- fast_write_str(socket, io_buffer.to_s) unless io_buffer.length.zero?
367
+ # may write last body part for non-chunked, also headers if array is empty
368
+ fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
369
+ else
370
+ # for enum bodies
371
+ if chunked
372
+ empty_body = true
373
+ body.each do |part|
374
+ next if part.nil? || (byte_size = part.bytesize).zero?
375
+ empty_body = false
376
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
377
+ fast_write_str socket, io_buffer.read_and_reset
378
+ end
379
+ if empty_body
380
+ io_buffer << CLOSE_CHUNKED
381
+ fast_write_str socket, io_buffer.read_and_reset
382
+ else
383
+ fast_write_str socket, CLOSE_CHUNKED
384
+ end
385
+ else
386
+ fast_write_str socket, io_buffer.read_and_reset
387
+ body.each do |part|
388
+ next if part.bytesize.zero?
389
+ fast_write_str socket, part
390
+ end
391
+ end
326
392
  end
393
+ socket.flush
327
394
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
328
395
  raise ConnectionError, SOCKET_WRITE_ERR_MSG
329
396
  rescue Errno::EPIPE, SystemCallError, IOError
@@ -357,7 +424,11 @@ module Puma
357
424
 
358
425
  unless env[REQUEST_PATH]
359
426
  # it might be a dumbass full host request header
360
- uri = URI.parse(env[REQUEST_URI])
427
+ uri = begin
428
+ URI.parse(env[REQUEST_URI])
429
+ rescue URI::InvalidURIError
430
+ raise Puma::HttpParserError
431
+ end
361
432
  env[REQUEST_PATH] = uri.path
362
433
 
363
434
  # A nil env value will cause a LintError (and fatal errors elsewhere),
@@ -428,6 +499,11 @@ module Puma
428
499
  # compatibility, we'll convert them back. This code is written to
429
500
  # avoid allocation in the common case (ie there are no headers
430
501
  # with `,` in their names), that's why it has the extra conditionals.
502
+ #
503
+ # @note If a normalized version of a `,` header already exists, we ignore
504
+ # the `,` version. This prevents clobbering headers managed by proxies
505
+ # but not by clients (Like X-Forwarded-For).
506
+ #
431
507
  # @param env [Hash] see Puma::Client#env, from request, modifies in place
432
508
  # @version 5.0.3
433
509
  #
@@ -436,23 +512,31 @@ module Puma
436
512
  to_add = nil
437
513
 
438
514
  env.each do |k,v|
439
- if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
515
+ if k.start_with?("HTTP_") && k.include?(",") && !UNMASKABLE_HEADERS.key?(k)
440
516
  if to_delete
441
517
  to_delete << k
442
518
  else
443
519
  to_delete = [k]
444
520
  end
445
521
 
522
+ new_k = k.tr(",", "_")
523
+ if env.key?(new_k)
524
+ next
525
+ end
526
+
446
527
  unless to_add
447
528
  to_add = {}
448
529
  end
449
530
 
450
- to_add[k.tr(",", "_")] = v
531
+ to_add[new_k] = v
451
532
  end
452
533
  end
453
534
 
454
- if to_delete
535
+ if to_delete # rubocop:disable Style/SafeNavigation
455
536
  to_delete.each { |k| env.delete(k) }
537
+ end
538
+
539
+ if to_add
456
540
  env.merge! to_add
457
541
  end
458
542
  end
@@ -464,7 +548,7 @@ module Puma
464
548
  # @version 5.0.3
465
549
  #
466
550
  def str_early_hints(headers)
467
- eh_str = +"HTTP/1.1 103 Early Hints\r\n"
551
+ eh_str = +""
468
552
  headers.each_pair do |k, vs|
469
553
  next if illegal_header_key?(k)
470
554
 
@@ -473,11 +557,11 @@ module Puma
473
557
  next if illegal_header_value?(v)
474
558
  eh_str << "#{k}: #{v}\r\n"
475
559
  end
476
- else
560
+ elsif !(vs.to_s.empty? || !illegal_header_value?(vs))
477
561
  eh_str << "#{k}: #{vs}\r\n"
478
562
  end
479
563
  end
480
- "#{eh_str}\r\n".freeze
564
+ eh_str.freeze
481
565
  end
482
566
  private :str_early_hints
483
567
 
@@ -496,12 +580,13 @@ module Puma
496
580
  # @param content_length [Integer,nil] content length if it can be determined from the
497
581
  # response body
498
582
  # @param io_buffer [Puma::IOBuffer] modified inn place
499
- # @param requests [Integer] number of inline requests handled
500
- # @param client [Puma::Client]
583
+ # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
584
+ # status and `@max_fast_inline`
501
585
  # @return [Hash] resp_info
502
586
  # @version 5.0.3
503
587
  #
504
- def str_headers(env, status, headers, res_body, io_buffer, requests, client)
588
+ def str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
589
+
505
590
  line_ending = LINE_END
506
591
  colon = COLON
507
592
 
@@ -544,13 +629,8 @@ module Puma
544
629
  # if running without request queueing
545
630
  resp_info[:keep_alive] &&= @queue_requests
546
631
 
547
- # Close the connection after a reasonable number of inline requests
548
- # if the server is at capacity and the listener has a new connection ready.
549
- # This allows Puma to service connections fairly when the number
550
- # of concurrent connections exceeds the size of the threadpool.
551
- resp_info[:keep_alive] &&= requests < @max_fast_inline ||
552
- @thread_pool.busy_threads < @max_threads ||
553
- !client.listener.to_io.wait_readable(0)
632
+ # see prepare_response
633
+ resp_info[:keep_alive] &&= force_keep_alive
554
634
 
555
635
  resp_info[:response_hijack] = nil
556
636
 
@@ -560,7 +640,8 @@ module Puma
560
640
  case k.downcase
561
641
  when CONTENT_LENGTH2
562
642
  next if illegal_header_value?(vs)
563
- resp_info[:content_length] = vs
643
+ # nil.to_i is 0, nil&.to_i is nil
644
+ resp_info[:content_length] = vs&.to_i
564
645
  next
565
646
  when TRANSFER_ENCODING
566
647
  resp_info[:allow_chunked] = false
data/lib/puma/runner.rb CHANGED
@@ -8,6 +8,9 @@ module Puma
8
8
  # serve requests. This class spawns a new instance of `Puma::Server` via
9
9
  # a call to `start_server`.
10
10
  class Runner
11
+
12
+ include ::Puma::Const::PipeRequest
13
+
11
14
  def initialize(launcher)
12
15
  @launcher = launcher
13
16
  @log_writer = launcher.log_writer
@@ -27,7 +30,7 @@ module Puma
27
30
  def wakeup!
28
31
  return unless @wakeup
29
32
 
30
- @wakeup.write "!" unless @wakeup.closed?
33
+ @wakeup.write PIPE_WAKEUP unless @wakeup.closed?
31
34
 
32
35
  rescue SystemCallError, IOError
33
36
  Puma::Util.purge_interrupt_queue
@@ -70,12 +73,16 @@ module Puma
70
73
 
71
74
  app = Puma::App::Status.new @launcher, token
72
75
 
73
- # A Reactor is not created aand nio4r is not loaded when 'queue_requests: false'
76
+ # A Reactor is not created and nio4r is not loaded when 'queue_requests: false'
74
77
  # Use `nil` for events, no hooks in control server
75
78
  control = Puma::Server.new app, nil,
76
79
  { min_threads: 0, max_threads: 1, queue_requests: false, log_writer: @log_writer }
77
80
 
78
- control.binder.parse [str], nil, 'Starting control server'
81
+ begin
82
+ control.binder.parse [str], nil, 'Starting control server'
83
+ rescue Errno::EADDRINUSE, Errno::EACCES => e
84
+ raise e, "Error: Control server address '#{str}' is already in use. Original error: #{e.message}"
85
+ end
79
86
 
80
87
  control.run thread_name: 'ctl'
81
88
  @control = control
@@ -87,7 +94,10 @@ module Puma
87
94
  end
88
95
 
89
96
  # @!attribute [r] ruby_engine
97
+ # @deprecated Use `RUBY_DESCRIPTION` instead
90
98
  def ruby_engine
99
+ warn "Puma::Runner#ruby_engine is deprecated; use RUBY_DESCRIPTION instead. It will be removed in puma v7."
100
+
91
101
  if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
92
102
  "ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
93
103
  else
@@ -105,7 +115,8 @@ module Puma
105
115
  environment = @options[:environment]
106
116
 
107
117
  log "Puma starting in #{mode} mode..."
108
- log "* Puma version: #{Puma::Const::PUMA_VERSION} (#{ruby_engine}) (\"#{Puma::Const::CODE_NAME}\")"
118
+ log "* Puma version: #{Puma::Const::PUMA_VERSION} (\"#{Puma::Const::CODE_NAME}\")"
119
+ log "* Ruby version: #{RUBY_DESCRIPTION}"
109
120
  log "* Min threads: #{min_t}"
110
121
  log "* Max threads: #{max_t}"
111
122
  log "* Environment: #{environment}"
@@ -182,6 +193,10 @@ module Puma
182
193
  end
183
194
  end
184
195
 
196
+ def utc_iso8601(val)
197
+ "#{val.utc.strftime '%FT%T'}Z"
198
+ end
199
+
185
200
  def stats
186
201
  {
187
202
  versions: {
@@ -194,5 +209,12 @@ module Puma
194
209
  }
195
210
  }
196
211
  end
212
+
213
+ # this method call should always be guarded by `@log_writer.debug?`
214
+ def debug_loaded_extensions(str)
215
+ @log_writer.debug "────────────────────────────────── #{str}"
216
+ re_ext = /\.#{RbConfig::CONFIG['DLEXT']}\z/i
217
+ $LOADED_FEATURES.grep(re_ext).each { |f| @log_writer.debug(" #{f}") }
218
+ end
197
219
  end
198
220
  end