puma 5.6.5 → 6.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +102 -11
  3. data/README.md +21 -17
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +1 -3
  7. data/docs/testing_benchmarks_local_files.md +150 -0
  8. data/docs/testing_test_rackup_ci_files.md +36 -0
  9. data/ext/puma_http11/extconf.rb +11 -8
  10. data/ext/puma_http11/http11_parser.c +1 -1
  11. data/ext/puma_http11/http11_parser.h +1 -1
  12. data/ext/puma_http11/http11_parser.java.rl +2 -2
  13. data/ext/puma_http11/http11_parser.rl +2 -2
  14. data/ext/puma_http11/http11_parser_common.rl +2 -2
  15. data/ext/puma_http11/mini_ssl.c +36 -15
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +156 -53
  19. data/ext/puma_http11/puma_http11.c +17 -9
  20. data/lib/puma/app/status.rb +3 -3
  21. data/lib/puma/binder.rb +36 -42
  22. data/lib/puma/cli.rb +11 -17
  23. data/lib/puma/client.rb +22 -12
  24. data/lib/puma/cluster/worker.rb +13 -11
  25. data/lib/puma/cluster/worker_handle.rb +4 -1
  26. data/lib/puma/cluster.rb +28 -25
  27. data/lib/puma/configuration.rb +74 -58
  28. data/lib/puma/const.rb +14 -18
  29. data/lib/puma/control_cli.rb +3 -6
  30. data/lib/puma/detect.rb +2 -0
  31. data/lib/puma/dsl.rb +93 -52
  32. data/lib/puma/error_logger.rb +17 -9
  33. data/lib/puma/events.rb +6 -126
  34. data/lib/puma/io_buffer.rb +29 -4
  35. data/lib/puma/jruby_restart.rb +2 -1
  36. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  37. data/lib/puma/launcher.rb +96 -156
  38. data/lib/puma/log_writer.rb +137 -0
  39. data/lib/puma/minissl/context_builder.rb +23 -12
  40. data/lib/puma/minissl.rb +82 -11
  41. data/lib/puma/plugin/tmp_restart.rb +1 -1
  42. data/lib/puma/rack/builder.rb +4 -4
  43. data/lib/puma/rack_default.rb +1 -1
  44. data/lib/puma/reactor.rb +3 -3
  45. data/lib/puma/request.rb +292 -161
  46. data/lib/puma/runner.rb +41 -20
  47. data/lib/puma/server.rb +53 -66
  48. data/lib/puma/single.rb +10 -10
  49. data/lib/puma/state_file.rb +1 -4
  50. data/lib/puma/systemd.rb +3 -2
  51. data/lib/puma/thread_pool.rb +16 -13
  52. data/lib/puma/util.rb +0 -11
  53. data/lib/puma.rb +11 -8
  54. data/lib/rack/handler/puma.rb +9 -9
  55. metadata +7 -3
  56. data/lib/puma/queue_close.rb +0 -26
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
104
 
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)
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
111
+ status, headers, res_body = lowlevel_error(e, env, 503)
112
+ rescue Exception => e
113
+ @log_writer.unknown_error e, client, "Rack app"
113
114
 
114
- str_headers(env, status, headers, res_info, lines, requests, client)
115
-
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]
@@ -354,7 +464,7 @@ module Puma
354
464
  # @version 5.0.3
355
465
  #
356
466
  def str_early_hints(headers)
357
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
467
+ eh_str = +"HTTP/1.1 103 Early Hints\r\n"
358
468
  headers.each_pair do |k, vs|
359
469
  next if illegal_header_key?(k)
360
470
 
@@ -371,66 +481,78 @@ module Puma
371
481
  end
372
482
  private :str_early_hints
373
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
+
374
492
  # Processes and write headers to the IOBuffer.
375
493
  # @param env [Hash] see Puma::Client#env, from request
376
494
  # @param status [Integer] the status returned by the Rack application
377
495
  # @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
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
380
499
  # @param requests [Integer] number of inline requests handled
381
500
  # @param client [Puma::Client]
501
+ # @return [Hash] resp_info
382
502
  # @version 5.0.3
383
503
  #
384
- def str_headers(env, status, headers, res_info, lines, requests, client)
504
+ def str_headers(env, status, headers, res_body, io_buffer, requests, client)
385
505
  line_ending = LINE_END
386
506
  colon = COLON
387
507
 
388
- 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
389
512
  if http_11
390
- res_info[:allow_chunked] = true
391
- 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
392
515
 
393
516
  # An optimization. The most common response is 200, so we can
394
517
  # reply with the proper 200 status without having to compute
395
518
  # the response header.
396
519
  #
397
520
  if status == 200
398
- lines << HTTP_11_200
521
+ io_buffer << HTTP_11_200
399
522
  else
400
- lines.append "HTTP/1.1 ", status.to_s, " ",
401
- fetch_status_code(status), line_ending
523
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
402
524
 
403
- 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]
404
526
  end
405
527
  else
406
- res_info[:allow_chunked] = false
407
- 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
408
530
 
409
531
  # Same optimization as above for HTTP/1.1
410
532
  #
411
533
  if status == 200
412
- lines << HTTP_10_200
534
+ io_buffer << HTTP_10_200
413
535
  else
414
- lines.append "HTTP/1.0 ", status.to_s, " ",
536
+ io_buffer.append "HTTP/1.0 #{status} ",
415
537
  fetch_status_code(status), line_ending
416
538
 
417
- 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]
418
540
  end
419
541
  end
420
542
 
421
543
  # regardless of what the client wants, we always close the connection
422
544
  # if running without request queueing
423
- res_info[:keep_alive] &&= @queue_requests
545
+ resp_info[:keep_alive] &&= @queue_requests
424
546
 
425
547
  # Close the connection after a reasonable number of inline requests
426
548
  # if the server is at capacity and the listener has a new connection ready.
427
549
  # This allows Puma to service connections fairly when the number
428
550
  # of concurrent connections exceeds the size of the threadpool.
429
- res_info[:keep_alive] &&= requests < @max_fast_inline ||
551
+ resp_info[:keep_alive] &&= requests < @max_fast_inline ||
430
552
  @thread_pool.busy_threads < @max_threads ||
431
553
  !client.listener.to_io.wait_readable(0)
432
554
 
433
- res_info[:response_hijack] = nil
555
+ resp_info[:response_hijack] = nil
434
556
 
435
557
  headers.each do |k, vs|
436
558
  next if illegal_header_key?(k)
@@ -438,25 +560,33 @@ module Puma
438
560
  case k.downcase
439
561
  when CONTENT_LENGTH2
440
562
  next if illegal_header_value?(vs)
441
- res_info[:content_length] = vs
563
+ resp_info[:content_length] = vs
442
564
  next
443
565
  when TRANSFER_ENCODING
444
- res_info[:allow_chunked] = false
445
- res_info[:content_length] = nil
566
+ resp_info[:allow_chunked] = false
567
+ resp_info[:content_length] = nil
568
+ resp_info[:transfer_encoding] = vs
446
569
  when HIJACK
447
- res_info[:response_hijack] = vs
570
+ resp_info[:response_hijack] = vs
448
571
  next
449
572
  when BANNED_HEADER_KEY
450
573
  next
451
574
  end
452
575
 
453
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
454
- 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|
455
585
  next if illegal_header_value?(v)
456
- lines.append k, colon, v, line_ending
586
+ io_buffer.append k, colon, v, line_ending
457
587
  end
458
588
  else
459
- lines.append k, colon, line_ending
589
+ io_buffer.append k, colon, line_ending
460
590
  end
461
591
  end
462
592
 
@@ -466,10 +596,11 @@ module Puma
466
596
  # Only set the header if we're doing something which is not the default
467
597
  # for this protocol version
468
598
  if http_11
469
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
599
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
470
600
  else
471
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
601
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
472
602
  end
603
+ resp_info
473
604
  end
474
605
  private :str_headers
475
606
  end