puma 5.3.2 → 6.0.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.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +284 -11
  3. data/LICENSE +0 -0
  4. data/README.md +61 -16
  5. data/bin/puma-wild +1 -1
  6. data/docs/architecture.md +49 -16
  7. data/docs/compile_options.md +38 -2
  8. data/docs/deployment.md +53 -67
  9. data/docs/fork_worker.md +1 -3
  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/jungle/README.md +0 -0
  14. data/docs/jungle/rc.d/README.md +0 -0
  15. data/docs/jungle/rc.d/puma.conf +0 -0
  16. data/docs/kubernetes.md +0 -0
  17. data/docs/nginx.md +0 -0
  18. data/docs/plugins.md +15 -15
  19. data/docs/rails_dev_mode.md +2 -3
  20. data/docs/restart.md +6 -6
  21. data/docs/signals.md +11 -10
  22. data/docs/stats.md +8 -8
  23. data/docs/systemd.md +64 -67
  24. data/docs/testing_benchmarks_local_files.md +150 -0
  25. data/docs/testing_test_rackup_ci_files.md +36 -0
  26. data/ext/puma_http11/PumaHttp11Service.java +0 -0
  27. data/ext/puma_http11/ext_help.h +0 -0
  28. data/ext/puma_http11/extconf.rb +44 -13
  29. data/ext/puma_http11/http11_parser.c +24 -11
  30. data/ext/puma_http11/http11_parser.h +1 -1
  31. data/ext/puma_http11/http11_parser.java.rl +2 -2
  32. data/ext/puma_http11/http11_parser.rl +2 -2
  33. data/ext/puma_http11/http11_parser_common.rl +3 -3
  34. data/ext/puma_http11/mini_ssl.c +122 -23
  35. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +0 -0
  36. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  37. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +50 -48
  38. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +188 -102
  39. data/ext/puma_http11/puma_http11.c +18 -10
  40. data/lib/puma/app/status.rb +9 -6
  41. data/lib/puma/binder.rb +81 -42
  42. data/lib/puma/cli.rb +23 -19
  43. data/lib/puma/client.rb +124 -30
  44. data/lib/puma/cluster/worker.rb +21 -29
  45. data/lib/puma/cluster/worker_handle.rb +8 -1
  46. data/lib/puma/cluster.rb +57 -48
  47. data/lib/puma/commonlogger.rb +0 -0
  48. data/lib/puma/configuration.rb +74 -55
  49. data/lib/puma/const.rb +21 -24
  50. data/lib/puma/control_cli.rb +22 -19
  51. data/lib/puma/detect.rb +10 -2
  52. data/lib/puma/dsl.rb +196 -57
  53. data/lib/puma/error_logger.rb +17 -9
  54. data/lib/puma/events.rb +6 -126
  55. data/lib/puma/io_buffer.rb +29 -4
  56. data/lib/puma/jruby_restart.rb +2 -1
  57. data/lib/puma/{json.rb → json_serialization.rb} +1 -1
  58. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  59. data/lib/puma/launcher.rb +108 -154
  60. data/lib/puma/log_writer.rb +137 -0
  61. data/lib/puma/minissl/context_builder.rb +29 -16
  62. data/lib/puma/minissl.rb +115 -38
  63. data/lib/puma/null_io.rb +5 -0
  64. data/lib/puma/plugin/tmp_restart.rb +1 -1
  65. data/lib/puma/plugin.rb +2 -2
  66. data/lib/puma/rack/builder.rb +5 -5
  67. data/lib/puma/rack/urlmap.rb +0 -0
  68. data/lib/puma/rack_default.rb +1 -1
  69. data/lib/puma/reactor.rb +3 -3
  70. data/lib/puma/request.rb +293 -153
  71. data/lib/puma/runner.rb +63 -28
  72. data/lib/puma/server.rb +83 -88
  73. data/lib/puma/single.rb +10 -10
  74. data/lib/puma/state_file.rb +39 -7
  75. data/lib/puma/systemd.rb +3 -2
  76. data/lib/puma/thread_pool.rb +22 -17
  77. data/lib/puma/util.rb +20 -15
  78. data/lib/puma.rb +12 -9
  79. data/lib/rack/handler/puma.rb +9 -9
  80. data/tools/Dockerfile +1 -1
  81. data/tools/trickletest.rb +0 -0
  82. metadata +13 -9
  83. data/lib/puma/queue_close.rb +0 -26
data/lib/puma/request.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Puma
4
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
5
+
4
6
 
5
7
  # The methods here are included in Server, but are separated into this file.
6
8
  # All the methods here pertain to passing the request to the app, then
@@ -10,7 +12,21 @@ module Puma
10
12
  # #handle_request, which is called in Server#process_client.
11
13
  # @version 5.0.3
12
14
  #
13
- module Request
15
+ module Request # :nodoc:
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
20
+
21
+ # size divide for using copy_stream on body
22
+ IO_BODY_MAX = 1_024 * 64
23
+
24
+ # max size for io_buffer, force write when exceeded
25
+ IO_BUFFER_LEN_MAX = 1_024 * 1_024 * 4
26
+
27
+ SOCKET_WRITE_ERR_MSG = "Socket timeout writing data"
28
+
29
+ CUSTOM_STAT = 'CUSTOM'
14
30
 
15
31
  include Puma::Const
16
32
 
@@ -25,40 +41,36 @@ module Puma
25
41
  #
26
42
  # Finally, it'll return +true+ on keep-alive connections.
27
43
  # @param client [Puma::Client]
28
- # @param lines [Puma::IOBuffer]
44
+ # @param io_buffer [Puma::IOBuffer]
29
45
  # @param requests [Integer]
30
46
  # @return [Boolean,:async]
31
47
  #
32
- def handle_request(client, lines, requests)
48
+ def handle_request(client, io_buffer, requests)
33
49
  env = client.env
34
- io = client.io # io may be a MiniSSL::Socket
50
+ socket = client.io # io may be a MiniSSL::Socket
35
51
 
36
- return false if closed_socket?(io)
52
+ return false if closed_socket?(socket)
37
53
 
38
54
  normalize_env env, client
39
55
 
40
- env[PUMA_SOCKET] = io
56
+ env[PUMA_SOCKET] = socket
41
57
 
42
- if env[HTTPS_KEY] && io.peercert
43
- env[PUMA_PEERCERT] = io.peercert
58
+ if env[HTTPS_KEY] && socket.peercert
59
+ env[PUMA_PEERCERT] = socket.peercert
44
60
  end
45
61
 
46
62
  env[HIJACK_P] = true
47
63
  env[HIJACK] = client
48
64
 
49
- body = client.body
50
-
51
- head = env[REQUEST_METHOD] == HEAD
52
-
53
- env[RACK_INPUT] = body
54
- env[RACK_URL_SCHEME] = default_server_port(env) == PORT_443 ? HTTPS : HTTP
65
+ env[RACK_INPUT] = client.body
66
+ env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
55
67
 
56
68
  if @early_hints
57
69
  env[EARLY_HINTS] = lambda { |headers|
58
70
  begin
59
- fast_write io, str_early_hints(headers)
71
+ fast_write_str socket, str_early_hints(headers)
60
72
  rescue ConnectionError => e
61
- @events.debug_error e
73
+ @log_writer.debug_error e
62
74
  # noop, if we lost the socket we just won't send the early hints
63
75
  end
64
76
  }
@@ -69,114 +81,147 @@ module Puma
69
81
  # A rack extension. If the app writes #call'ables to this
70
82
  # array, we will invoke them when the request is done.
71
83
  #
72
- after_reply = env[RACK_AFTER_REPLY] = []
84
+ env[RACK_AFTER_REPLY] ||= []
73
85
 
74
86
  begin
75
- begin
87
+ if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
76
88
  status, headers, res_body = @thread_pool.with_force_shutdown do
77
89
  @app.call(env)
78
90
  end
91
+ else
92
+ @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
93
+ status, headers, res_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
94
+ end
79
95
 
80
- return :async if client.hijacked
81
-
82
- status = status.to_i
96
+ return :async if client.hijacked
83
97
 
84
- if status == -1
85
- unless headers.empty? and res_body == []
86
- raise "async response must have empty headers and body"
87
- end
98
+ status = status.to_i
88
99
 
89
- return :async
100
+ if status == -1
101
+ unless headers.empty? and res_body == []
102
+ raise "async response must have empty headers and body"
90
103
  end
91
- rescue ThreadPool::ForceShutdown => e
92
- @events.unknown_error e, client, "Rack app"
93
- @events.log "Detected force shutdown of a thread"
94
104
 
95
- status, headers, res_body = lowlevel_error(e, env, 503)
96
- rescue Exception => e
97
- @events.unknown_error e, client, "Rack app"
98
-
99
- status, headers, res_body = lowlevel_error(e, env, 500)
100
- end
101
-
102
- res_info = {}
103
- res_info[:content_length] = nil
104
- res_info[:no_body] = head
105
-
106
- res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
107
- res_body[0].bytesize
108
- else
109
- nil
105
+ return :async
110
106
  end
107
+ rescue ThreadPool::ForceShutdown => e
108
+ @log_writer.unknown_error e, client, "Rack app"
109
+ @log_writer.log "Detected force shutdown of a thread"
111
110
 
112
- cork_socket io
113
-
114
- str_headers(env, status, headers, res_info, lines, requests, client)
115
-
116
- line_ending = LINE_END
111
+ status, headers, res_body = lowlevel_error(e, env, 503)
112
+ rescue Exception => e
113
+ @log_writer.unknown_error e, client, "Rack app"
117
114
 
118
- content_length = res_info[:content_length]
119
- response_hijack = res_info[:response_hijack]
115
+ status, headers, res_body = lowlevel_error(e, env, 500)
116
+ end
117
+ prepare_response(status, headers, res_body, io_buffer, requests, client)
118
+ end
120
119
 
121
- if res_info[:no_body]
122
- if content_length and status != 204
123
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
120
+ # Assembles the headers and prepares the body for actually sending the
121
+ # response via #fast_write_response.
122
+ #
123
+ # @param status [Integer] the status returned by the Rack application
124
+ # @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
128
+ # @param requests [Integer] number of inline requests handled
129
+ # @param client [Puma::Client]
130
+ # @return [Boolean,:async]
131
+ def prepare_response(status, headers, app_body, io_buffer, requests, client)
132
+ env = client.env
133
+ socket = client.io
134
+
135
+ after_reply = env[RACK_AFTER_REPLY] || []
136
+
137
+ return false if closed_socket?(socket)
138
+
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 }
124
153
  end
125
-
126
- lines << LINE_END
127
- fast_write io, lines.to_s
128
- return res_info[:keep_alive]
129
- end
130
-
131
- if content_length
132
- lines.append CONTENT_LENGTH_S, content_length.to_s, line_ending
133
- chunked = false
134
- elsif !response_hijack and res_info[:allow_chunked]
135
- lines << TRANSFER_ENCODING_CHUNKED
136
- chunked = true
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
137
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
169
+ else
170
+ body = app_body
171
+ end
138
172
 
139
- lines << line_ending
173
+ line_ending = LINE_END
140
174
 
141
- fast_write io, lines.to_s
175
+ content_length = resp_info[:content_length]
176
+ keep_alive = resp_info[:keep_alive]
142
177
 
143
- if response_hijack
144
- response_hijack.call io
145
- return :async
146
- end
178
+ if app_body && !app_body.respond_to?(:each)
179
+ response_hijack = app_body
180
+ else
181
+ response_hijack = resp_info[:response_hijack]
182
+ end
147
183
 
148
- begin
149
- res_body.each do |part|
150
- next if part.bytesize.zero?
151
- if chunked
152
- fast_write io, (part.bytesize.to_s(16) << line_ending)
153
- fast_write io, part # part may have different encoding
154
- fast_write io, line_ending
155
- else
156
- fast_write io, part
157
- end
158
- io.flush
159
- end
184
+ cork_socket socket
160
185
 
161
- if chunked
162
- fast_write io, CLOSE_CHUNKED
163
- io.flush
164
- end
165
- rescue SystemCallError, IOError
166
- raise ConnectionError, "Connection error detected during write"
186
+ if resp_info[:no_body]
187
+ if content_length and status != 204
188
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
167
189
  end
168
190
 
169
- ensure
170
- uncork_socket io
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
+ end
171
202
 
172
- body.close
173
- client.tempfile.unlink if client.tempfile
174
- res_body.close if res_body.respond_to? :close
203
+ io_buffer << line_ending
175
204
 
176
- after_reply.each { |o| o.call }
205
+ if response_hijack
206
+ fast_write_str socket, io_buffer.to_s
207
+ response_hijack.call socket
208
+ return :async
177
209
  end
178
210
 
179
- return res_info[:keep_alive]
211
+ fast_write_response socket, body, io_buffer, chunked, content_length.to_i
212
+ 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?
180
225
  end
181
226
 
182
227
  # @param env [Hash] see Puma::Client#env, from request
@@ -190,45 +235,107 @@ module Puma
190
235
  end
191
236
  end
192
237
 
193
- # Writes to an io (normally Client#io) using #syswrite
194
- # @param io [#syswrite] the io to write to
238
+ # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
239
+ # and body segments (called by `fast_write_response`).
240
+ # Writes a string to an io (normally `Client#io`) using `write_nonblock`.
241
+ # Large strings may not be written in one pass, especially if `io` is a
242
+ # `MiniSSL::Socket`.
243
+ # @param io [#write_nonblock] the io to write to
195
244
  # @param str [String] the string written to the io
196
245
  # @raise [ConnectionError]
197
246
  #
198
- def fast_write(io, str)
247
+ def fast_write_str(socket, str)
199
248
  n = 0
200
- while true
249
+ byte_size = str.bytesize
250
+ while n < byte_size
201
251
  begin
202
- n = io.syswrite str
252
+ n += socket.syswrite(n.zero? ? str : str.byteslice(n..-1))
203
253
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
204
- if !IO.select(nil, [io], nil, WRITE_TIMEOUT)
205
- raise ConnectionError, "Socket timeout writing data"
254
+ unless socket.wait_writable WRITE_TIMEOUT
255
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
206
256
  end
207
-
208
257
  retry
209
258
  rescue Errno::EPIPE, SystemCallError, IOError
210
- raise ConnectionError, "Socket timeout writing data"
259
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
211
260
  end
212
-
213
- return if n == str.bytesize
214
- str = str.byteslice(n..-1)
215
261
  end
216
262
  end
217
- private :fast_write
218
263
 
219
- # @param status [Integer] status from the app
220
- # @return [String] the text description from Puma::HTTP_STATUS_CODES
264
+ # Used to write headers and body.
265
+ # Writes to a socket (normally `Client#io`) using `#fast_write_str`.
266
+ # Accumulates `body` items into `io_buffer`, then writes to socket.
267
+ # @param socket [#write] the response socket
268
+ # @param body [Enumerable, File] the body object
269
+ # @param io_buffer [Puma::IOBuffer] contains headers
270
+ # @param chunk [Boolean]
271
+ # @raise [ConnectionError]
221
272
  #
222
- def fetch_status_code(status)
223
- HTTP_STATUS_CODES.fetch(status) { 'CUSTOM' }
273
+ 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)
275
+ 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
278
+ end
279
+ io_buffer << CLOSE_CHUNKED
280
+ fast_write_str socket, io_buffer.to_s
281
+ else
282
+ if content_length <= IO_BODY_MAX
283
+ io_buffer.write body.sysread(content_length)
284
+ fast_write_str socket, io_buffer.to_s
285
+ else
286
+ fast_write_str socket, io_buffer.to_s
287
+ IO.copy_stream body, socket
288
+ end
289
+ end
290
+ body.close
291
+ 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
294
+ # large body, write both header & body to socket
295
+ fast_write_str socket, io_buffer.to_s
296
+ 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
+ end
302
+ else
303
+ # for array bodies, flush io_buffer to socket when size is greater than
304
+ # IO_BUFFER_LEN_MAX
305
+ if chunked
306
+ body.each do |part|
307
+ next if (byte_size = part.bytesize).zero?
308
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
309
+ if io_buffer.length > IO_BUFFER_LEN_MAX
310
+ fast_write_str socket, io_buffer.to_s
311
+ io_buffer.reset
312
+ end
313
+ end
314
+ io_buffer.write CLOSE_CHUNKED
315
+ else
316
+ body.each do |part|
317
+ next if part.bytesize.zero?
318
+ io_buffer.write part
319
+ if io_buffer.length > IO_BUFFER_LEN_MAX
320
+ fast_write_str socket, io_buffer.to_s
321
+ io_buffer.reset
322
+ end
323
+ end
324
+ end
325
+ fast_write_str(socket, io_buffer.to_s) unless io_buffer.length.zero?
326
+ end
327
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
328
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
329
+ rescue Errno::EPIPE, SystemCallError, IOError
330
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
224
331
  end
225
- private :fetch_status_code
332
+
333
+ private :fast_write_str, :fast_write_response
226
334
 
227
335
  # Given a Hash +env+ for the request read from +client+, add
228
336
  # and fixup keys to comply with Rack's env guidelines.
229
337
  # @param env [Hash] see Puma::Client#env, from request
230
338
  # @param client [Puma::Client] only needed for Client#peerip
231
- # @todo make private in 6.0.0
232
339
  #
233
340
  def normalize_env(env, client)
234
341
  if host = env[HTTP_HOST]
@@ -253,14 +360,12 @@ module Puma
253
360
  uri = URI.parse(env[REQUEST_URI])
254
361
  env[REQUEST_PATH] = uri.path
255
362
 
256
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
257
-
258
363
  # A nil env value will cause a LintError (and fatal errors elsewhere),
259
364
  # so only set the env value if there actually is a value.
260
365
  env[QUERY_STRING] = uri.query if uri.query
261
366
  end
262
367
 
263
- env[PATH_INFO] = env[REQUEST_PATH]
368
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
264
369
 
265
370
  # From https://www.ietf.org/rfc/rfc3875 :
266
371
  # "Script authors should be aware that the REMOTE_ADDR and
@@ -276,17 +381,31 @@ module Puma
276
381
  addr = client.peerip
277
382
  rescue Errno::ENOTCONN
278
383
  # Client disconnects can result in an inability to get the
279
- # peeraddr from the socket; default to localhost.
280
- addr = LOCALHOST_IP
384
+ # peeraddr from the socket; default to unspec.
385
+ if client.peer_family == Socket::AF_INET6
386
+ addr = UNSPECIFIED_IPV6
387
+ else
388
+ addr = UNSPECIFIED_IPV4
389
+ end
281
390
  end
282
391
 
283
392
  # Set unix socket addrs to localhost
284
- addr = LOCALHOST_IP if addr.empty?
393
+ if addr.empty?
394
+ if client.peer_family == Socket::AF_INET6
395
+ addr = LOCALHOST_IPV6
396
+ else
397
+ addr = LOCALHOST_IPV4
398
+ end
399
+ end
285
400
 
286
401
  env[REMOTE_ADDR] = addr
287
402
  end
403
+
404
+ # The legacy HTTP_VERSION header can be sent as a client header.
405
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
406
+ env[HTTP_VERSION] = env[SERVER_PROTOCOL]
288
407
  end
289
- # private :normalize_env
408
+ private :normalize_env
290
409
 
291
410
  # @param header_key [#to_s]
292
411
  # @return [Boolean]
@@ -345,7 +464,7 @@ module Puma
345
464
  # @version 5.0.3
346
465
  #
347
466
  def str_early_hints(headers)
348
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
467
+ eh_str = +"HTTP/1.1 103 Early Hints\r\n"
349
468
  headers.each_pair do |k, vs|
350
469
  next if illegal_header_key?(k)
351
470
 
@@ -362,66 +481,78 @@ module Puma
362
481
  end
363
482
  private :str_early_hints
364
483
 
484
+ # @param status [Integer] status from the app
485
+ # @return [String] the text description from Puma::HTTP_STATUS_CODES
486
+ #
487
+ def fetch_status_code(status)
488
+ HTTP_STATUS_CODES.fetch(status) { CUSTOM_STAT }
489
+ end
490
+ private :fetch_status_code
491
+
365
492
  # Processes and write headers to the IOBuffer.
366
493
  # @param env [Hash] see Puma::Client#env, from request
367
494
  # @param status [Integer] the status returned by the Rack application
368
495
  # @param headers [Hash] the headers returned by the Rack application
369
- # @param res_info [Hash] used to pass info between this method and #handle_request
370
- # @param lines [Puma::IOBuffer] modified inn place
496
+ # @param content_length [Integer,nil] content length if it can be determined from the
497
+ # response body
498
+ # @param io_buffer [Puma::IOBuffer] modified inn place
371
499
  # @param requests [Integer] number of inline requests handled
372
500
  # @param client [Puma::Client]
501
+ # @return [Hash] resp_info
373
502
  # @version 5.0.3
374
503
  #
375
- def str_headers(env, status, headers, res_info, lines, requests, client)
504
+ def str_headers(env, status, headers, res_body, io_buffer, requests, client)
376
505
  line_ending = LINE_END
377
506
  colon = COLON
378
507
 
379
- http_11 = env[HTTP_VERSION] == HTTP_11
508
+ resp_info = {}
509
+ resp_info[:no_body] = env[REQUEST_METHOD] == HEAD
510
+
511
+ http_11 = env[SERVER_PROTOCOL] == HTTP_11
380
512
  if http_11
381
- res_info[:allow_chunked] = true
382
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
513
+ resp_info[:allow_chunked] = true
514
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
383
515
 
384
516
  # An optimization. The most common response is 200, so we can
385
517
  # reply with the proper 200 status without having to compute
386
518
  # the response header.
387
519
  #
388
520
  if status == 200
389
- lines << HTTP_11_200
521
+ io_buffer << HTTP_11_200
390
522
  else
391
- lines.append "HTTP/1.1 ", status.to_s, " ",
392
- fetch_status_code(status), line_ending
523
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
393
524
 
394
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
525
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
395
526
  end
396
527
  else
397
- res_info[:allow_chunked] = false
398
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
528
+ resp_info[:allow_chunked] = false
529
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
399
530
 
400
531
  # Same optimization as above for HTTP/1.1
401
532
  #
402
533
  if status == 200
403
- lines << HTTP_10_200
534
+ io_buffer << HTTP_10_200
404
535
  else
405
- lines.append "HTTP/1.0 ", status.to_s, " ",
536
+ io_buffer.append "HTTP/1.0 #{status} ",
406
537
  fetch_status_code(status), line_ending
407
538
 
408
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
539
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
409
540
  end
410
541
  end
411
542
 
412
543
  # regardless of what the client wants, we always close the connection
413
544
  # if running without request queueing
414
- res_info[:keep_alive] &&= @queue_requests
545
+ resp_info[:keep_alive] &&= @queue_requests
415
546
 
416
547
  # Close the connection after a reasonable number of inline requests
417
548
  # if the server is at capacity and the listener has a new connection ready.
418
549
  # This allows Puma to service connections fairly when the number
419
550
  # of concurrent connections exceeds the size of the threadpool.
420
- res_info[:keep_alive] &&= requests < @max_fast_inline ||
551
+ resp_info[:keep_alive] &&= requests < @max_fast_inline ||
421
552
  @thread_pool.busy_threads < @max_threads ||
422
- !IO.select([client.listener], nil, nil, 0)
553
+ !client.listener.to_io.wait_readable(0)
423
554
 
424
- res_info[:response_hijack] = nil
555
+ resp_info[:response_hijack] = nil
425
556
 
426
557
  headers.each do |k, vs|
427
558
  next if illegal_header_key?(k)
@@ -429,25 +560,33 @@ module Puma
429
560
  case k.downcase
430
561
  when CONTENT_LENGTH2
431
562
  next if illegal_header_value?(vs)
432
- res_info[:content_length] = vs
563
+ resp_info[:content_length] = vs
433
564
  next
434
565
  when TRANSFER_ENCODING
435
- res_info[:allow_chunked] = false
436
- res_info[:content_length] = nil
566
+ resp_info[:allow_chunked] = false
567
+ resp_info[:content_length] = nil
568
+ resp_info[:transfer_encoding] = vs
437
569
  when HIJACK
438
- res_info[:response_hijack] = vs
570
+ resp_info[:response_hijack] = vs
439
571
  next
440
572
  when BANNED_HEADER_KEY
441
573
  next
442
574
  end
443
575
 
444
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
445
- vs.to_s.split(NEWLINE).each do |v|
576
+ ary = if vs.is_a?(::Array) && !vs.empty?
577
+ vs
578
+ elsif vs.respond_to?(:to_s) && !vs.to_s.empty?
579
+ vs.to_s.split NEWLINE
580
+ else
581
+ nil
582
+ end
583
+ if ary
584
+ ary.each do |v|
446
585
  next if illegal_header_value?(v)
447
- lines.append k, colon, v, line_ending
586
+ io_buffer.append k, colon, v, line_ending
448
587
  end
449
588
  else
450
- lines.append k, colon, line_ending
589
+ io_buffer.append k, colon, line_ending
451
590
  end
452
591
  end
453
592
 
@@ -457,10 +596,11 @@ module Puma
457
596
  # Only set the header if we're doing something which is not the default
458
597
  # for this protocol version
459
598
  if http_11
460
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
599
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
461
600
  else
462
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
601
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
463
602
  end
603
+ resp_info
464
604
  end
465
605
  private :str_headers
466
606
  end