puma 5.5.2 → 6.3.0

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