puma 3.11.2 → 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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +1708 -422
  3. data/LICENSE +23 -20
  4. data/README.md +190 -64
  5. data/bin/puma-wild +3 -9
  6. data/docs/architecture.md +59 -21
  7. data/docs/compile_options.md +55 -0
  8. data/docs/deployment.md +69 -58
  9. data/docs/fork_worker.md +31 -0
  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 +9 -0
  14. data/docs/jungle/rc.d/README.md +74 -0
  15. data/docs/jungle/rc.d/puma +61 -0
  16. data/docs/jungle/rc.d/puma.conf +10 -0
  17. data/docs/kubernetes.md +66 -0
  18. data/docs/nginx.md +1 -1
  19. data/docs/plugins.md +22 -12
  20. data/docs/rails_dev_mode.md +28 -0
  21. data/docs/restart.md +47 -22
  22. data/docs/signals.md +13 -11
  23. data/docs/stats.md +142 -0
  24. data/docs/systemd.md +100 -115
  25. data/docs/testing_benchmarks_local_files.md +150 -0
  26. data/docs/testing_test_rackup_ci_files.md +36 -0
  27. data/ext/puma_http11/PumaHttp11Service.java +2 -2
  28. data/ext/puma_http11/ext_help.h +1 -1
  29. data/ext/puma_http11/extconf.rb +61 -3
  30. data/ext/puma_http11/http11_parser.c +106 -118
  31. data/ext/puma_http11/http11_parser.h +2 -2
  32. data/ext/puma_http11/http11_parser.java.rl +22 -38
  33. data/ext/puma_http11/http11_parser.rl +6 -4
  34. data/ext/puma_http11/http11_parser_common.rl +6 -6
  35. data/ext/puma_http11/mini_ssl.c +376 -93
  36. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  37. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  38. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +84 -99
  39. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +250 -88
  40. data/ext/puma_http11/puma_http11.c +49 -57
  41. data/lib/puma/app/status.rb +71 -49
  42. data/lib/puma/binder.rb +243 -148
  43. data/lib/puma/cli.rb +50 -35
  44. data/lib/puma/client.rb +369 -232
  45. data/lib/puma/cluster/worker.rb +175 -0
  46. data/lib/puma/cluster/worker_handle.rb +97 -0
  47. data/lib/puma/cluster.rb +268 -235
  48. data/lib/puma/commonlogger.rb +4 -2
  49. data/lib/puma/configuration.rb +116 -88
  50. data/lib/puma/const.rb +49 -30
  51. data/lib/puma/control_cli.rb +124 -77
  52. data/lib/puma/detect.rb +33 -2
  53. data/lib/puma/dsl.rb +685 -138
  54. data/lib/puma/error_logger.rb +112 -0
  55. data/lib/puma/events.rb +17 -111
  56. data/lib/puma/io_buffer.rb +34 -5
  57. data/lib/puma/jruby_restart.rb +4 -59
  58. data/lib/puma/json_serialization.rb +96 -0
  59. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  60. data/lib/puma/launcher.rb +197 -130
  61. data/lib/puma/log_writer.rb +137 -0
  62. data/lib/puma/minissl/context_builder.rb +92 -0
  63. data/lib/puma/minissl.rb +256 -70
  64. data/lib/puma/null_io.rb +20 -1
  65. data/lib/puma/plugin/tmp_restart.rb +3 -1
  66. data/lib/puma/plugin.rb +9 -13
  67. data/lib/puma/rack/builder.rb +8 -9
  68. data/lib/puma/rack/urlmap.rb +2 -0
  69. data/lib/puma/rack_default.rb +3 -1
  70. data/lib/puma/reactor.rb +90 -187
  71. data/lib/puma/request.rb +607 -0
  72. data/lib/puma/runner.rb +94 -71
  73. data/lib/puma/server.rb +336 -703
  74. data/lib/puma/single.rb +27 -72
  75. data/lib/puma/state_file.rb +46 -7
  76. data/lib/puma/systemd.rb +47 -0
  77. data/lib/puma/thread_pool.rb +185 -91
  78. data/lib/puma/util.rb +23 -10
  79. data/lib/puma.rb +68 -3
  80. data/lib/rack/handler/puma.rb +17 -14
  81. data/tools/Dockerfile +16 -0
  82. data/tools/trickletest.rb +0 -1
  83. metadata +53 -30
  84. data/ext/puma_http11/io_buffer.c +0 -155
  85. data/lib/puma/accept_nonblock.rb +0 -23
  86. data/lib/puma/compat.rb +0 -14
  87. data/lib/puma/convenient.rb +0 -23
  88. data/lib/puma/daemon_ext.rb +0 -31
  89. data/lib/puma/delegation.rb +0 -11
  90. data/lib/puma/java_io_buffer.rb +0 -45
  91. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  92. data/lib/puma/tcp_logger.rb +0 -39
  93. data/tools/jungle/README.md +0 -13
  94. data/tools/jungle/init.d/README.md +0 -59
  95. data/tools/jungle/init.d/puma +0 -421
  96. data/tools/jungle/init.d/run-puma +0 -18
  97. data/tools/jungle/upstart/README.md +0 -61
  98. data/tools/jungle/upstart/puma-manager.conf +0 -31
  99. data/tools/jungle/upstart/puma.conf +0 -69
@@ -0,0 +1,607 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
5
+
6
+
7
+ # The methods here are included in Server, but are separated into this file.
8
+ # All the methods here pertain to passing the request to the app, then
9
+ # writing the response back to the client.
10
+ #
11
+ # None of the methods here are called externally, with the exception of
12
+ # #handle_request, which is called in Server#process_client.
13
+ # @version 5.0.3
14
+ #
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'
30
+
31
+ include Puma::Const
32
+
33
+ # Takes the request contained in +client+, invokes the Rack application to construct
34
+ # the response and writes it back to +client.io+.
35
+ #
36
+ # It'll return +false+ when the connection is closed, this doesn't mean
37
+ # that the response wasn't successful.
38
+ #
39
+ # It'll return +:async+ if the connection remains open but will be handled
40
+ # elsewhere, i.e. the connection has been hijacked by the Rack application.
41
+ #
42
+ # Finally, it'll return +true+ on keep-alive connections.
43
+ # @param client [Puma::Client]
44
+ # @param io_buffer [Puma::IOBuffer]
45
+ # @param requests [Integer]
46
+ # @return [Boolean,:async]
47
+ #
48
+ def handle_request(client, io_buffer, requests)
49
+ env = client.env
50
+ socket = client.io # io may be a MiniSSL::Socket
51
+
52
+ return false if closed_socket?(socket)
53
+
54
+ normalize_env env, client
55
+
56
+ env[PUMA_SOCKET] = socket
57
+
58
+ if env[HTTPS_KEY] && socket.peercert
59
+ env[PUMA_PEERCERT] = socket.peercert
60
+ end
61
+
62
+ env[HIJACK_P] = true
63
+ env[HIJACK] = client
64
+
65
+ env[RACK_INPUT] = client.body
66
+ env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
67
+
68
+ if @early_hints
69
+ env[EARLY_HINTS] = lambda { |headers|
70
+ begin
71
+ fast_write_str socket, str_early_hints(headers)
72
+ rescue ConnectionError => e
73
+ @log_writer.debug_error e
74
+ # noop, if we lost the socket we just won't send the early hints
75
+ end
76
+ }
77
+ end
78
+
79
+ req_env_post_parse env
80
+
81
+ # A rack extension. If the app writes #call'ables to this
82
+ # array, we will invoke them when the request is done.
83
+ #
84
+ env[RACK_AFTER_REPLY] ||= []
85
+
86
+ begin
87
+ if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
88
+ status, headers, res_body = @thread_pool.with_force_shutdown do
89
+ @app.call(env)
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
95
+
96
+ return :async if client.hijacked
97
+
98
+ status = status.to_i
99
+
100
+ if status == -1
101
+ unless headers.empty? and res_body == []
102
+ raise "async response must have empty headers and body"
103
+ end
104
+
105
+ return :async
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"
110
+
111
+ status, headers, res_body = lowlevel_error(e, env, 503)
112
+ rescue Exception => e
113
+ @log_writer.unknown_error e, client, "Rack app"
114
+
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
119
+
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 }
153
+ end
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
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
172
+
173
+ line_ending = LINE_END
174
+
175
+ content_length = resp_info[:content_length]
176
+ keep_alive = resp_info[:keep_alive]
177
+
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
183
+
184
+ cork_socket socket
185
+
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
189
+ end
190
+
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
202
+
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
209
+ end
210
+
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?
225
+ end
226
+
227
+ # @param env [Hash] see Puma::Client#env, from request
228
+ # @return [Puma::Const::PORT_443,Puma::Const::PORT_80]
229
+ #
230
+ def default_server_port(env)
231
+ if ['on', HTTPS].include?(env[HTTPS_KEY]) || env[HTTP_X_FORWARDED_PROTO].to_s[0...5] == HTTPS || env[HTTP_X_FORWARDED_SCHEME] == HTTPS || env[HTTP_X_FORWARDED_SSL] == "on"
232
+ PORT_443
233
+ else
234
+ PORT_80
235
+ end
236
+ end
237
+
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
244
+ # @param str [String] the string written to the io
245
+ # @raise [ConnectionError]
246
+ #
247
+ def fast_write_str(socket, str)
248
+ n = 0
249
+ byte_size = str.bytesize
250
+ while n < byte_size
251
+ begin
252
+ n += socket.syswrite(n.zero? ? str : str.byteslice(n..-1))
253
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
254
+ unless socket.wait_writable WRITE_TIMEOUT
255
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
256
+ end
257
+ retry
258
+ rescue Errno::EPIPE, SystemCallError, IOError
259
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
260
+ end
261
+ end
262
+ end
263
+
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]
272
+ #
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
331
+ end
332
+
333
+ private :fast_write_str, :fast_write_response
334
+
335
+ # Given a Hash +env+ for the request read from +client+, add
336
+ # and fixup keys to comply with Rack's env guidelines.
337
+ # @param env [Hash] see Puma::Client#env, from request
338
+ # @param client [Puma::Client] only needed for Client#peerip
339
+ #
340
+ def normalize_env(env, client)
341
+ if host = env[HTTP_HOST]
342
+ # host can be a hostname, ipv4 or bracketed ipv6. Followed by an optional port.
343
+ if colon = host.rindex("]:") # IPV6 with port
344
+ env[SERVER_NAME] = host[0, colon+1]
345
+ env[SERVER_PORT] = host[colon+2, host.bytesize]
346
+ elsif !host.start_with?("[") && colon = host.index(":") # not hostname or IPV4 with port
347
+ env[SERVER_NAME] = host[0, colon]
348
+ env[SERVER_PORT] = host[colon+1, host.bytesize]
349
+ else
350
+ env[SERVER_NAME] = host
351
+ env[SERVER_PORT] = default_server_port(env)
352
+ end
353
+ else
354
+ env[SERVER_NAME] = LOCALHOST
355
+ env[SERVER_PORT] = default_server_port(env)
356
+ end
357
+
358
+ unless env[REQUEST_PATH]
359
+ # it might be a dumbass full host request header
360
+ uri = URI.parse(env[REQUEST_URI])
361
+ env[REQUEST_PATH] = uri.path
362
+
363
+ # A nil env value will cause a LintError (and fatal errors elsewhere),
364
+ # so only set the env value if there actually is a value.
365
+ env[QUERY_STRING] = uri.query if uri.query
366
+ end
367
+
368
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
369
+
370
+ # From https://www.ietf.org/rfc/rfc3875 :
371
+ # "Script authors should be aware that the REMOTE_ADDR and
372
+ # REMOTE_HOST meta-variables (see sections 4.1.8 and 4.1.9)
373
+ # may not identify the ultimate source of the request.
374
+ # They identify the client for the immediate request to the
375
+ # server; that client may be a proxy, gateway, or other
376
+ # intermediary acting on behalf of the actual source client."
377
+ #
378
+
379
+ unless env.key?(REMOTE_ADDR)
380
+ begin
381
+ addr = client.peerip
382
+ rescue Errno::ENOTCONN
383
+ # Client disconnects can result in an inability to get the
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
390
+ end
391
+
392
+ # Set unix socket addrs to localhost
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
400
+
401
+ env[REMOTE_ADDR] = addr
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]
407
+ end
408
+ private :normalize_env
409
+
410
+ # @param header_key [#to_s]
411
+ # @return [Boolean]
412
+ #
413
+ def illegal_header_key?(header_key)
414
+ !!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
415
+ end
416
+
417
+ # @param header_value [#to_s]
418
+ # @return [Boolean]
419
+ #
420
+ def illegal_header_value?(header_value)
421
+ !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
422
+ end
423
+ private :illegal_header_key?, :illegal_header_value?
424
+
425
+ # Fixup any headers with `,` in the name to have `_` now. We emit
426
+ # headers with `,` in them during the parse phase to avoid ambiguity
427
+ # with the `-` to `_` conversion for critical headers. But here for
428
+ # compatibility, we'll convert them back. This code is written to
429
+ # avoid allocation in the common case (ie there are no headers
430
+ # with `,` in their names), that's why it has the extra conditionals.
431
+ # @param env [Hash] see Puma::Client#env, from request, modifies in place
432
+ # @version 5.0.3
433
+ #
434
+ def req_env_post_parse(env)
435
+ to_delete = nil
436
+ to_add = nil
437
+
438
+ env.each do |k,v|
439
+ if k.start_with?("HTTP_") and k.include?(",") and k != "HTTP_TRANSFER,ENCODING"
440
+ if to_delete
441
+ to_delete << k
442
+ else
443
+ to_delete = [k]
444
+ end
445
+
446
+ unless to_add
447
+ to_add = {}
448
+ end
449
+
450
+ to_add[k.tr(",", "_")] = v
451
+ end
452
+ end
453
+
454
+ if to_delete
455
+ to_delete.each { |k| env.delete(k) }
456
+ env.merge! to_add
457
+ end
458
+ end
459
+ private :req_env_post_parse
460
+
461
+ # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
462
+ # @param headers [Hash] the headers returned by the Rack application
463
+ # @return [String]
464
+ # @version 5.0.3
465
+ #
466
+ def str_early_hints(headers)
467
+ eh_str = +"HTTP/1.1 103 Early Hints\r\n"
468
+ headers.each_pair do |k, vs|
469
+ next if illegal_header_key?(k)
470
+
471
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
472
+ vs.to_s.split(NEWLINE).each do |v|
473
+ next if illegal_header_value?(v)
474
+ eh_str << "#{k}: #{v}\r\n"
475
+ end
476
+ else
477
+ eh_str << "#{k}: #{vs}\r\n"
478
+ end
479
+ end
480
+ "#{eh_str}\r\n".freeze
481
+ end
482
+ private :str_early_hints
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
+
492
+ # Processes and write headers to the IOBuffer.
493
+ # @param env [Hash] see Puma::Client#env, from request
494
+ # @param status [Integer] the status returned by the Rack application
495
+ # @param headers [Hash] the headers returned by the Rack application
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
499
+ # @param requests [Integer] number of inline requests handled
500
+ # @param client [Puma::Client]
501
+ # @return [Hash] resp_info
502
+ # @version 5.0.3
503
+ #
504
+ def str_headers(env, status, headers, res_body, io_buffer, requests, client)
505
+ line_ending = LINE_END
506
+ colon = COLON
507
+
508
+ resp_info = {}
509
+ resp_info[:no_body] = env[REQUEST_METHOD] == HEAD
510
+
511
+ http_11 = env[SERVER_PROTOCOL] == HTTP_11
512
+ if http_11
513
+ resp_info[:allow_chunked] = true
514
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
515
+
516
+ # An optimization. The most common response is 200, so we can
517
+ # reply with the proper 200 status without having to compute
518
+ # the response header.
519
+ #
520
+ if status == 200
521
+ io_buffer << HTTP_11_200
522
+ else
523
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
524
+
525
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
526
+ end
527
+ else
528
+ resp_info[:allow_chunked] = false
529
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
530
+
531
+ # Same optimization as above for HTTP/1.1
532
+ #
533
+ if status == 200
534
+ io_buffer << HTTP_10_200
535
+ else
536
+ io_buffer.append "HTTP/1.0 #{status} ",
537
+ fetch_status_code(status), line_ending
538
+
539
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
540
+ end
541
+ end
542
+
543
+ # regardless of what the client wants, we always close the connection
544
+ # if running without request queueing
545
+ resp_info[:keep_alive] &&= @queue_requests
546
+
547
+ # Close the connection after a reasonable number of inline requests
548
+ # if the server is at capacity and the listener has a new connection ready.
549
+ # This allows Puma to service connections fairly when the number
550
+ # of concurrent connections exceeds the size of the threadpool.
551
+ resp_info[:keep_alive] &&= requests < @max_fast_inline ||
552
+ @thread_pool.busy_threads < @max_threads ||
553
+ !client.listener.to_io.wait_readable(0)
554
+
555
+ resp_info[:response_hijack] = nil
556
+
557
+ headers.each do |k, vs|
558
+ next if illegal_header_key?(k)
559
+
560
+ case k.downcase
561
+ when CONTENT_LENGTH2
562
+ next if illegal_header_value?(vs)
563
+ resp_info[:content_length] = vs
564
+ next
565
+ when TRANSFER_ENCODING
566
+ resp_info[:allow_chunked] = false
567
+ resp_info[:content_length] = nil
568
+ resp_info[:transfer_encoding] = vs
569
+ when HIJACK
570
+ resp_info[:response_hijack] = vs
571
+ next
572
+ when BANNED_HEADER_KEY
573
+ next
574
+ end
575
+
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|
585
+ next if illegal_header_value?(v)
586
+ io_buffer.append k, colon, v, line_ending
587
+ end
588
+ else
589
+ io_buffer.append k, colon, line_ending
590
+ end
591
+ end
592
+
593
+ # HTTP/1.1 & 1.0 assume different defaults:
594
+ # - HTTP 1.0 assumes the connection will be closed if not specified
595
+ # - HTTP 1.1 assumes the connection will be kept alive if not specified.
596
+ # Only set the header if we're doing something which is not the default
597
+ # for this protocol version
598
+ if http_11
599
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
600
+ else
601
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
602
+ end
603
+ resp_info
604
+ end
605
+ private :str_headers
606
+ end
607
+ end