puma 5.6.9-java → 6.0.0-java

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