puma 5.6.4 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +154 -3
  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 +18 -10
  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 +63 -24
  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 +166 -65
  20. data/ext/puma_http11/puma_http11.c +17 -9
  21. data/lib/puma/app/status.rb +6 -3
  22. data/lib/puma/binder.rb +37 -43
  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 +21 -18
  31. data/lib/puma/detect.rb +2 -0
  32. data/lib/puma/dsl.rb +97 -49
  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 +107 -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 +91 -15
  42. data/lib/puma/null_io.rb +5 -0
  43. data/lib/puma/plugin/tmp_restart.rb +1 -1
  44. data/lib/puma/rack/builder.rb +4 -4
  45. data/lib/puma/rack_default.rb +1 -1
  46. data/lib/puma/reactor.rb +4 -4
  47. data/lib/puma/request.rb +334 -162
  48. data/lib/puma/runner.rb +41 -20
  49. data/lib/puma/server.rb +55 -69
  50. data/lib/puma/single.rb +10 -10
  51. data/lib/puma/state_file.rb +2 -4
  52. data/lib/puma/systemd.rb +3 -2
  53. data/lib/puma/thread_pool.rb +16 -16
  54. data/lib/puma/util.rb +12 -14
  55. data/lib/puma.rb +12 -9
  56. data/lib/rack/handler/puma.rb +9 -9
  57. metadata +7 -3
  58. 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,119 +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
- after_reply.each { |o| o.call }
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
182
241
  end
183
242
 
184
- 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
185
246
  end
186
247
 
187
248
  # @param env [Hash] see Puma::Client#env, from request
@@ -195,45 +256,126 @@ module Puma
195
256
  end
196
257
  end
197
258
 
198
- # Writes to an io (normally Client#io) using #syswrite
199
- # @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
200
265
  # @param str [String] the string written to the io
201
266
  # @raise [ConnectionError]
202
267
  #
203
- def fast_write(io, str)
268
+ def fast_write_str(socket, str)
204
269
  n = 0
205
- while true
270
+ byte_size = str.bytesize
271
+ while n < byte_size
206
272
  begin
207
- n = io.syswrite str
273
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
208
274
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
209
- unless io.wait_writable WRITE_TIMEOUT
210
- raise ConnectionError, "Socket timeout writing data"
275
+ unless socket.wait_writable WRITE_TIMEOUT
276
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
211
277
  end
212
-
213
278
  retry
214
279
  rescue Errno::EPIPE, SystemCallError, IOError
215
- raise ConnectionError, "Socket timeout writing data"
280
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
216
281
  end
217
-
218
- return if n == str.bytesize
219
- str = str.byteslice(n..-1)
220
282
  end
221
283
  end
222
- private :fast_write
223
284
 
224
- # @param status [Integer] status from the app
225
- # @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]
226
294
  #
227
- def fetch_status_code(status)
228
- 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
229
371
  end
230
- private :fetch_status_code
372
+
373
+ private :fast_write_str, :fast_write_response
231
374
 
232
375
  # Given a Hash +env+ for the request read from +client+, add
233
376
  # and fixup keys to comply with Rack's env guidelines.
234
377
  # @param env [Hash] see Puma::Client#env, from request
235
378
  # @param client [Puma::Client] only needed for Client#peerip
236
- # @todo make private in 6.0.0
237
379
  #
238
380
  def normalize_env(env, client)
239
381
  if host = env[HTTP_HOST]
@@ -258,14 +400,12 @@ module Puma
258
400
  uri = URI.parse(env[REQUEST_URI])
259
401
  env[REQUEST_PATH] = uri.path
260
402
 
261
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
262
-
263
403
  # A nil env value will cause a LintError (and fatal errors elsewhere),
264
404
  # so only set the env value if there actually is a value.
265
405
  env[QUERY_STRING] = uri.query if uri.query
266
406
  end
267
407
 
268
- env[PATH_INFO] = env[REQUEST_PATH]
408
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
269
409
 
270
410
  # From https://www.ietf.org/rfc/rfc3875 :
271
411
  # "Script authors should be aware that the REMOTE_ADDR and
@@ -281,17 +421,31 @@ module Puma
281
421
  addr = client.peerip
282
422
  rescue Errno::ENOTCONN
283
423
  # Client disconnects can result in an inability to get the
284
- # peeraddr from the socket; default to localhost.
285
- 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
286
430
  end
287
431
 
288
432
  # Set unix socket addrs to localhost
289
- 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
290
440
 
291
441
  env[REMOTE_ADDR] = addr
292
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]
293
447
  end
294
- # private :normalize_env
448
+ private :normalize_env
295
449
 
296
450
  # @param header_key [#to_s]
297
451
  # @return [Boolean]
@@ -350,7 +504,7 @@ module Puma
350
504
  # @version 5.0.3
351
505
  #
352
506
  def str_early_hints(headers)
353
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
507
+ eh_str = +"HTTP/1.1 103 Early Hints\r\n"
354
508
  headers.each_pair do |k, vs|
355
509
  next if illegal_header_key?(k)
356
510
 
@@ -367,66 +521,74 @@ module Puma
367
521
  end
368
522
  private :str_early_hints
369
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
+
370
532
  # Processes and write headers to the IOBuffer.
371
533
  # @param env [Hash] see Puma::Client#env, from request
372
534
  # @param status [Integer] the status returned by the Rack application
373
535
  # @param headers [Hash] the headers returned by the Rack application
374
- # @param res_info [Hash] used to pass info between this method and #handle_request
375
- # @param lines [Puma::IOBuffer] modified inn place
376
- # @param requests [Integer] number of inline requests handled
377
- # @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
378
542
  # @version 5.0.3
379
543
  #
380
- 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
+
381
546
  line_ending = LINE_END
382
547
  colon = COLON
383
548
 
384
- 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
385
553
  if http_11
386
- res_info[:allow_chunked] = true
387
- 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
388
556
 
389
557
  # An optimization. The most common response is 200, so we can
390
558
  # reply with the proper 200 status without having to compute
391
559
  # the response header.
392
560
  #
393
561
  if status == 200
394
- lines << HTTP_11_200
562
+ io_buffer << HTTP_11_200
395
563
  else
396
- lines.append "HTTP/1.1 ", status.to_s, " ",
397
- fetch_status_code(status), line_ending
564
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
398
565
 
399
- 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]
400
567
  end
401
568
  else
402
- res_info[:allow_chunked] = false
403
- 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
404
571
 
405
572
  # Same optimization as above for HTTP/1.1
406
573
  #
407
574
  if status == 200
408
- lines << HTTP_10_200
575
+ io_buffer << HTTP_10_200
409
576
  else
410
- lines.append "HTTP/1.0 ", status.to_s, " ",
577
+ io_buffer.append "HTTP/1.0 #{status} ",
411
578
  fetch_status_code(status), line_ending
412
579
 
413
- 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]
414
581
  end
415
582
  end
416
583
 
417
584
  # regardless of what the client wants, we always close the connection
418
585
  # if running without request queueing
419
- res_info[:keep_alive] &&= @queue_requests
586
+ resp_info[:keep_alive] &&= @queue_requests
420
587
 
421
- # Close the connection after a reasonable number of inline requests
422
- # if the server is at capacity and the listener has a new connection ready.
423
- # This allows Puma to service connections fairly when the number
424
- # of concurrent connections exceeds the size of the threadpool.
425
- res_info[:keep_alive] &&= requests < @max_fast_inline ||
426
- @thread_pool.busy_threads < @max_threads ||
427
- !client.listener.to_io.wait_readable(0)
588
+ # see prepare_response
589
+ resp_info[:keep_alive] &&= force_keep_alive
428
590
 
429
- res_info[:response_hijack] = nil
591
+ resp_info[:response_hijack] = nil
430
592
 
431
593
  headers.each do |k, vs|
432
594
  next if illegal_header_key?(k)
@@ -434,25 +596,34 @@ module Puma
434
596
  case k.downcase
435
597
  when CONTENT_LENGTH2
436
598
  next if illegal_header_value?(vs)
437
- res_info[:content_length] = vs
599
+ # nil.to_i is 0, nil&.to_i is nil
600
+ resp_info[:content_length] = vs&.to_i
438
601
  next
439
602
  when TRANSFER_ENCODING
440
- res_info[:allow_chunked] = false
441
- res_info[:content_length] = nil
603
+ resp_info[:allow_chunked] = false
604
+ resp_info[:content_length] = nil
605
+ resp_info[:transfer_encoding] = vs
442
606
  when HIJACK
443
- res_info[:response_hijack] = vs
607
+ resp_info[:response_hijack] = vs
444
608
  next
445
609
  when BANNED_HEADER_KEY
446
610
  next
447
611
  end
448
612
 
449
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
450
- 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|
451
622
  next if illegal_header_value?(v)
452
- lines.append k, colon, v, line_ending
623
+ io_buffer.append k, colon, v, line_ending
453
624
  end
454
625
  else
455
- lines.append k, colon, line_ending
626
+ io_buffer.append k, colon, line_ending
456
627
  end
457
628
  end
458
629
 
@@ -462,10 +633,11 @@ module Puma
462
633
  # Only set the header if we're doing something which is not the default
463
634
  # for this protocol version
464
635
  if http_11
465
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
636
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
466
637
  else
467
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
638
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
468
639
  end
640
+ resp_info
469
641
  end
470
642
  private :str_headers
471
643
  end