puma 5.6.5 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

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