puma 5.6.8 → 6.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +322 -16
  3. data/README.md +79 -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/restart.md +1 -0
  10. data/docs/systemd.md +3 -6
  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 +16 -9
  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 +127 -19
  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 +157 -53
  23. data/ext/puma_http11/puma_http11.c +17 -9
  24. data/lib/puma/app/status.rb +4 -4
  25. data/lib/puma/binder.rb +50 -53
  26. data/lib/puma/cli.rb +16 -18
  27. data/lib/puma/client.rb +59 -19
  28. data/lib/puma/cluster/worker.rb +18 -11
  29. data/lib/puma/cluster/worker_handle.rb +4 -1
  30. data/lib/puma/cluster.rb +102 -40
  31. data/lib/puma/commonlogger.rb +21 -14
  32. data/lib/puma/configuration.rb +77 -59
  33. data/lib/puma/const.rb +129 -92
  34. data/lib/puma/control_cli.rb +15 -11
  35. data/lib/puma/detect.rb +7 -4
  36. data/lib/puma/dsl.rb +250 -56
  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 +102 -175
  43. data/lib/puma/log_writer.rb +147 -0
  44. data/lib/puma/minissl/context_builder.rb +26 -12
  45. data/lib/puma/minissl.rb +104 -11
  46. data/lib/puma/null_io.rb +16 -2
  47. data/lib/puma/plugin/systemd.rb +90 -0
  48. data/lib/puma/plugin/tmp_restart.rb +1 -1
  49. data/lib/puma/rack/builder.rb +6 -6
  50. data/lib/puma/rack/urlmap.rb +1 -1
  51. data/lib/puma/rack_default.rb +19 -4
  52. data/lib/puma/reactor.rb +19 -10
  53. data/lib/puma/request.rb +365 -170
  54. data/lib/puma/runner.rb +56 -20
  55. data/lib/puma/sd_notify.rb +149 -0
  56. data/lib/puma/server.rb +137 -89
  57. data/lib/puma/single.rb +13 -11
  58. data/lib/puma/state_file.rb +3 -6
  59. data/lib/puma/thread_pool.rb +57 -19
  60. data/lib/puma/util.rb +0 -11
  61. data/lib/puma.rb +9 -10
  62. data/lib/rack/handler/puma.rb +113 -86
  63. data/tools/Dockerfile +2 -2
  64. metadata +9 -5
  65. data/lib/puma/queue_close.rb +0 -26
  66. data/lib/puma/systemd.rb +0 -46
  67. 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