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