puma 5.6.7 → 6.4.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +269 -13
  3. data/README.md +78 -29
  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/kubernetes.md +12 -0
  8. data/docs/nginx.md +1 -1
  9. data/docs/systemd.md +1 -2
  10. data/docs/testing_benchmarks_local_files.md +150 -0
  11. data/docs/testing_test_rackup_ci_files.md +36 -0
  12. data/ext/puma_http11/extconf.rb +11 -8
  13. data/ext/puma_http11/http11_parser.c +1 -1
  14. data/ext/puma_http11/http11_parser.h +1 -1
  15. data/ext/puma_http11/http11_parser.java.rl +2 -2
  16. data/ext/puma_http11/http11_parser.rl +2 -2
  17. data/ext/puma_http11/http11_parser_common.rl +2 -2
  18. data/ext/puma_http11/mini_ssl.c +122 -18
  19. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  20. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  21. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +156 -53
  22. data/ext/puma_http11/puma_http11.c +17 -9
  23. data/lib/puma/app/status.rb +4 -4
  24. data/lib/puma/binder.rb +50 -53
  25. data/lib/puma/cli.rb +16 -18
  26. data/lib/puma/client.rb +59 -19
  27. data/lib/puma/cluster/worker.rb +18 -11
  28. data/lib/puma/cluster/worker_handle.rb +4 -1
  29. data/lib/puma/cluster.rb +33 -30
  30. data/lib/puma/commonlogger.rb +21 -14
  31. data/lib/puma/configuration.rb +78 -58
  32. data/lib/puma/const.rb +129 -92
  33. data/lib/puma/control_cli.rb +15 -11
  34. data/lib/puma/detect.rb +4 -0
  35. data/lib/puma/dsl.rb +237 -56
  36. data/lib/puma/error_logger.rb +18 -9
  37. data/lib/puma/events.rb +6 -126
  38. data/lib/puma/io_buffer.rb +39 -4
  39. data/lib/puma/jruby_restart.rb +2 -1
  40. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  41. data/lib/puma/launcher.rb +102 -175
  42. data/lib/puma/log_writer.rb +147 -0
  43. data/lib/puma/minissl/context_builder.rb +24 -12
  44. data/lib/puma/minissl.rb +99 -11
  45. data/lib/puma/plugin/systemd.rb +90 -0
  46. data/lib/puma/plugin/tmp_restart.rb +1 -1
  47. data/lib/puma/rack/builder.rb +6 -6
  48. data/lib/puma/rack/urlmap.rb +1 -1
  49. data/lib/puma/rack_default.rb +19 -4
  50. data/lib/puma/reactor.rb +19 -10
  51. data/lib/puma/request.rb +365 -170
  52. data/lib/puma/runner.rb +56 -20
  53. data/lib/puma/sd_notify.rb +149 -0
  54. data/lib/puma/server.rb +116 -89
  55. data/lib/puma/single.rb +13 -11
  56. data/lib/puma/state_file.rb +1 -4
  57. data/lib/puma/thread_pool.rb +57 -19
  58. data/lib/puma/util.rb +0 -11
  59. data/lib/puma.rb +9 -10
  60. data/lib/rack/handler/puma.rb +113 -86
  61. metadata +9 -5
  62. data/lib/puma/queue_close.rb +0 -26
  63. data/lib/puma/systemd.rb +0 -46
  64. data/lib/rack/version_restriction.rb +0 -15
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
+
35
56
 
36
- return false if closed_socket?(io)
57
+ return false if closed_socket?(socket)
58
+
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,123 +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
-
95
- status, headers, res_body = lowlevel_error(e, env, 503)
96
- rescue Exception => e
97
- @events.unknown_error e, client, "Rack app"
98
120
 
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
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
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
179
246
  end
247
+ end
180
248
 
181
- begin
182
- after_reply.each { |o| o.call }
183
- rescue StandardError => e
184
- @log_writer.debug_error e
185
- end
249
+ io_buffer << line_ending
250
+
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
186
258
  end
187
259
 
188
- 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
189
263
  end
190
264
 
191
265
  # @param env [Hash] see Puma::Client#env, from request
@@ -199,45 +273,132 @@ module Puma
199
273
  end
200
274
  end
201
275
 
202
- # Writes to an io (normally Client#io) using #syswrite
203
- # @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
204
282
  # @param str [String] the string written to the io
205
283
  # @raise [ConnectionError]
206
284
  #
207
- def fast_write(io, str)
285
+ def fast_write_str(socket, str)
208
286
  n = 0
209
- while true
287
+ byte_size = str.bytesize
288
+ while n < byte_size
210
289
  begin
211
- n = io.syswrite str
290
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
212
291
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
213
- unless io.wait_writable WRITE_TIMEOUT
214
- raise ConnectionError, "Socket timeout writing data"
292
+ unless socket.wait_writable WRITE_TIMEOUT
293
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
215
294
  end
216
-
217
295
  retry
218
296
  rescue Errno::EPIPE, SystemCallError, IOError
219
- raise ConnectionError, "Socket timeout writing data"
297
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
220
298
  end
221
-
222
- return if n == str.bytesize
223
- str = str.byteslice(n..-1)
224
299
  end
225
300
  end
226
- private :fast_write
227
301
 
228
- # @param status [Integer] status from the app
229
- # @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]
230
311
  #
231
- def fetch_status_code(status)
232
- 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
233
394
  end
234
- private :fetch_status_code
395
+
396
+ private :fast_write_str, :fast_write_response
235
397
 
236
398
  # Given a Hash +env+ for the request read from +client+, add
237
399
  # and fixup keys to comply with Rack's env guidelines.
238
400
  # @param env [Hash] see Puma::Client#env, from request
239
401
  # @param client [Puma::Client] only needed for Client#peerip
240
- # @todo make private in 6.0.0
241
402
  #
242
403
  def normalize_env(env, client)
243
404
  if host = env[HTTP_HOST]
@@ -259,17 +420,19 @@ module Puma
259
420
 
260
421
  unless env[REQUEST_PATH]
261
422
  # it might be a dumbass full host request header
262
- 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
263
428
  env[REQUEST_PATH] = uri.path
264
429
 
265
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
266
-
267
430
  # A nil env value will cause a LintError (and fatal errors elsewhere),
268
431
  # so only set the env value if there actually is a value.
269
432
  env[QUERY_STRING] = uri.query if uri.query
270
433
  end
271
434
 
272
- env[PATH_INFO] = env[REQUEST_PATH]
435
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
273
436
 
274
437
  # From https://www.ietf.org/rfc/rfc3875 :
275
438
  # "Script authors should be aware that the REMOTE_ADDR and
@@ -285,17 +448,31 @@ module Puma
285
448
  addr = client.peerip
286
449
  rescue Errno::ENOTCONN
287
450
  # Client disconnects can result in an inability to get the
288
- # peeraddr from the socket; default to localhost.
289
- 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
290
457
  end
291
458
 
292
459
  # Set unix socket addrs to localhost
293
- 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
294
467
 
295
468
  env[REMOTE_ADDR] = addr
296
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]
297
474
  end
298
- # private :normalize_env
475
+ private :normalize_env
299
476
 
300
477
  # @param header_key [#to_s]
301
478
  # @return [Boolean]
@@ -326,7 +503,7 @@ module Puma
326
503
  to_add = nil
327
504
 
328
505
  env.each do |k,v|
329
- 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"
330
507
  if to_delete
331
508
  to_delete << k
332
509
  else
@@ -354,7 +531,7 @@ module Puma
354
531
  # @version 5.0.3
355
532
  #
356
533
  def str_early_hints(headers)
357
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
534
+ eh_str = +""
358
535
  headers.each_pair do |k, vs|
359
536
  next if illegal_header_key?(k)
360
537
 
@@ -363,74 +540,82 @@ module Puma
363
540
  next if illegal_header_value?(v)
364
541
  eh_str << "#{k}: #{v}\r\n"
365
542
  end
366
- else
543
+ elsif !(vs.to_s.empty? || !illegal_header_value?(vs))
367
544
  eh_str << "#{k}: #{vs}\r\n"
368
545
  end
369
546
  end
370
- "#{eh_str}\r\n".freeze
547
+ eh_str.freeze
371
548
  end
372
549
  private :str_early_hints
373
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
+
374
559
  # Processes and write headers to the IOBuffer.
375
560
  # @param env [Hash] see Puma::Client#env, from request
376
561
  # @param status [Integer] the status returned by the Rack application
377
562
  # @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]
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
382
569
  # @version 5.0.3
383
570
  #
384
- 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
+
385
573
  line_ending = LINE_END
386
574
  colon = COLON
387
575
 
388
- 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
389
580
  if http_11
390
- res_info[:allow_chunked] = true
391
- 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
392
583
 
393
584
  # An optimization. The most common response is 200, so we can
394
585
  # reply with the proper 200 status without having to compute
395
586
  # the response header.
396
587
  #
397
588
  if status == 200
398
- lines << HTTP_11_200
589
+ io_buffer << HTTP_11_200
399
590
  else
400
- lines.append "HTTP/1.1 ", status.to_s, " ",
401
- fetch_status_code(status), line_ending
591
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
402
592
 
403
- 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]
404
594
  end
405
595
  else
406
- res_info[:allow_chunked] = false
407
- 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
408
598
 
409
599
  # Same optimization as above for HTTP/1.1
410
600
  #
411
601
  if status == 200
412
- lines << HTTP_10_200
602
+ io_buffer << HTTP_10_200
413
603
  else
414
- lines.append "HTTP/1.0 ", status.to_s, " ",
604
+ io_buffer.append "HTTP/1.0 #{status} ",
415
605
  fetch_status_code(status), line_ending
416
606
 
417
- 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]
418
608
  end
419
609
  end
420
610
 
421
611
  # regardless of what the client wants, we always close the connection
422
612
  # if running without request queueing
423
- res_info[:keep_alive] &&= @queue_requests
613
+ resp_info[:keep_alive] &&= @queue_requests
424
614
 
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)
615
+ # see prepare_response
616
+ resp_info[:keep_alive] &&= force_keep_alive
432
617
 
433
- res_info[:response_hijack] = nil
618
+ resp_info[:response_hijack] = nil
434
619
 
435
620
  headers.each do |k, vs|
436
621
  next if illegal_header_key?(k)
@@ -438,25 +623,34 @@ module Puma
438
623
  case k.downcase
439
624
  when CONTENT_LENGTH2
440
625
  next if illegal_header_value?(vs)
441
- res_info[:content_length] = vs
626
+ # nil.to_i is 0, nil&.to_i is nil
627
+ resp_info[:content_length] = vs&.to_i
442
628
  next
443
629
  when TRANSFER_ENCODING
444
- res_info[:allow_chunked] = false
445
- res_info[:content_length] = nil
630
+ resp_info[:allow_chunked] = false
631
+ resp_info[:content_length] = nil
632
+ resp_info[:transfer_encoding] = vs
446
633
  when HIJACK
447
- res_info[:response_hijack] = vs
634
+ resp_info[:response_hijack] = vs
448
635
  next
449
636
  when BANNED_HEADER_KEY
450
637
  next
451
638
  end
452
639
 
453
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
454
- 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|
455
649
  next if illegal_header_value?(v)
456
- lines.append k, colon, v, line_ending
650
+ io_buffer.append k, colon, v, line_ending
457
651
  end
458
652
  else
459
- lines.append k, colon, line_ending
653
+ io_buffer.append k, colon, line_ending
460
654
  end
461
655
  end
462
656
 
@@ -466,10 +660,11 @@ module Puma
466
660
  # Only set the header if we're doing something which is not the default
467
661
  # for this protocol version
468
662
  if http_11
469
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
663
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
470
664
  else
471
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
665
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
472
666
  end
667
+ resp_info
473
668
  end
474
669
  private :str_headers
475
670
  end