tiny-fast-gem 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/puma-8.0.2/History.md +3334 -0
  3. data/puma-8.0.2/LICENSE +29 -0
  4. data/puma-8.0.2/README.md +484 -0
  5. data/puma-8.0.2/bin/puma +10 -0
  6. data/puma-8.0.2/bin/puma-wild +25 -0
  7. data/puma-8.0.2/bin/pumactl +12 -0
  8. data/puma-8.0.2/docs/5.0-Upgrade.md +98 -0
  9. data/puma-8.0.2/docs/6.0-Upgrade.md +56 -0
  10. data/puma-8.0.2/docs/7.0-Upgrade.md +52 -0
  11. data/puma-8.0.2/docs/8.0-Upgrade.md +100 -0
  12. data/puma-8.0.2/docs/architecture.md +74 -0
  13. data/puma-8.0.2/docs/compile_options.md +55 -0
  14. data/puma-8.0.2/docs/deployment.md +137 -0
  15. data/puma-8.0.2/docs/fork_worker.md +41 -0
  16. data/puma-8.0.2/docs/grpc.md +62 -0
  17. data/puma-8.0.2/docs/images/favicon.svg +1 -0
  18. data/puma-8.0.2/docs/images/puma-connection-flow-no-reactor.png +0 -0
  19. data/puma-8.0.2/docs/images/puma-connection-flow.png +0 -0
  20. data/puma-8.0.2/docs/images/puma-general-arch.png +0 -0
  21. data/puma-8.0.2/docs/images/running-puma.svg +1 -0
  22. data/puma-8.0.2/docs/images/standard-logo.svg +1 -0
  23. data/puma-8.0.2/docs/java_options.md +54 -0
  24. data/puma-8.0.2/docs/jungle/README.md +9 -0
  25. data/puma-8.0.2/docs/jungle/rc.d/README.md +74 -0
  26. data/puma-8.0.2/docs/jungle/rc.d/puma +61 -0
  27. data/puma-8.0.2/docs/jungle/rc.d/puma.conf +10 -0
  28. data/puma-8.0.2/docs/kubernetes.md +73 -0
  29. data/puma-8.0.2/docs/nginx.md +80 -0
  30. data/puma-8.0.2/docs/plugins.md +42 -0
  31. data/puma-8.0.2/docs/rails_dev_mode.md +28 -0
  32. data/puma-8.0.2/docs/restart.md +65 -0
  33. data/puma-8.0.2/docs/signals.md +98 -0
  34. data/puma-8.0.2/docs/stats.md +148 -0
  35. data/puma-8.0.2/docs/systemd.md +253 -0
  36. data/puma-8.0.2/docs/testing_benchmarks_local_files.md +150 -0
  37. data/puma-8.0.2/docs/testing_test_rackup_ci_files.md +36 -0
  38. data/puma-8.0.2/ext/puma_http11/PumaHttp11Service.java +17 -0
  39. data/puma-8.0.2/ext/puma_http11/extconf.rb +65 -0
  40. data/puma-8.0.2/ext/puma_http11/http11_parser.c +1057 -0
  41. data/puma-8.0.2/ext/puma_http11/http11_parser.h +65 -0
  42. data/puma-8.0.2/ext/puma_http11/http11_parser.java.rl +131 -0
  43. data/puma-8.0.2/ext/puma_http11/http11_parser.rl +149 -0
  44. data/puma-8.0.2/ext/puma_http11/http11_parser_common.rl +54 -0
  45. data/puma-8.0.2/ext/puma_http11/mini_ssl.c +852 -0
  46. data/puma-8.0.2/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  47. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
  48. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/Http11.java +321 -0
  49. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/Http11Parser.java +441 -0
  50. data/puma-8.0.2/ext/puma_http11/org/jruby/puma/MiniSSL.java +509 -0
  51. data/puma-8.0.2/ext/puma_http11/puma_http11.c +499 -0
  52. data/puma-8.0.2/lib/puma/app/status.rb +104 -0
  53. data/puma-8.0.2/lib/puma/binder.rb +511 -0
  54. data/puma-8.0.2/lib/puma/cli.rb +245 -0
  55. data/puma-8.0.2/lib/puma/client.rb +756 -0
  56. data/puma-8.0.2/lib/puma/client_env.rb +171 -0
  57. data/puma-8.0.2/lib/puma/cluster/worker.rb +183 -0
  58. data/puma-8.0.2/lib/puma/cluster/worker_handle.rb +127 -0
  59. data/puma-8.0.2/lib/puma/cluster.rb +634 -0
  60. data/puma-8.0.2/lib/puma/cluster_accept_loop_delay.rb +91 -0
  61. data/puma-8.0.2/lib/puma/commonlogger.rb +115 -0
  62. data/puma-8.0.2/lib/puma/configuration.rb +522 -0
  63. data/puma-8.0.2/lib/puma/const.rb +308 -0
  64. data/puma-8.0.2/lib/puma/control_cli.rb +320 -0
  65. data/puma-8.0.2/lib/puma/detect.rb +58 -0
  66. data/puma-8.0.2/lib/puma/dsl.rb +1562 -0
  67. data/puma-8.0.2/lib/puma/error_logger.rb +115 -0
  68. data/puma-8.0.2/lib/puma/events.rb +72 -0
  69. data/puma-8.0.2/lib/puma/io_buffer.rb +50 -0
  70. data/puma-8.0.2/lib/puma/jruby_restart.rb +11 -0
  71. data/puma-8.0.2/lib/puma/json_serialization.rb +96 -0
  72. data/puma-8.0.2/lib/puma/launcher/bundle_pruner.rb +102 -0
  73. data/puma-8.0.2/lib/puma/launcher.rb +501 -0
  74. data/puma-8.0.2/lib/puma/log_writer.rb +153 -0
  75. data/puma-8.0.2/lib/puma/minissl/context_builder.rb +96 -0
  76. data/puma-8.0.2/lib/puma/minissl.rb +458 -0
  77. data/puma-8.0.2/lib/puma/null_io.rb +101 -0
  78. data/puma-8.0.2/lib/puma/plugin/systemd.rb +90 -0
  79. data/puma-8.0.2/lib/puma/plugin/tmp_restart.rb +36 -0
  80. data/puma-8.0.2/lib/puma/plugin.rb +111 -0
  81. data/puma-8.0.2/lib/puma/rack/builder.rb +297 -0
  82. data/puma-8.0.2/lib/puma/rack/urlmap.rb +93 -0
  83. data/puma-8.0.2/lib/puma/rack_default.rb +24 -0
  84. data/puma-8.0.2/lib/puma/reactor.rb +131 -0
  85. data/puma-8.0.2/lib/puma/response.rb +532 -0
  86. data/puma-8.0.2/lib/puma/runner.rb +211 -0
  87. data/puma-8.0.2/lib/puma/sd_notify.rb +146 -0
  88. data/puma-8.0.2/lib/puma/server.rb +773 -0
  89. data/puma-8.0.2/lib/puma/server_plugin_control.rb +32 -0
  90. data/puma-8.0.2/lib/puma/single.rb +72 -0
  91. data/puma-8.0.2/lib/puma/state_file.rb +69 -0
  92. data/puma-8.0.2/lib/puma/thread_pool.rb +517 -0
  93. data/puma-8.0.2/lib/puma/util.rb +134 -0
  94. data/puma-8.0.2/lib/puma.rb +88 -0
  95. data/puma-8.0.2/lib/rack/handler/puma.rb +144 -0
  96. data/puma-8.0.2/tools/Dockerfile +26 -0
  97. data/puma-8.0.2/tools/trickletest.rb +44 -0
  98. data/tiny-fast-gem.gemspec +12 -0
  99. metadata +138 -0
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puma
4
+ class UnsupportedBackend < StandardError; end
5
+
6
+ # Monitors a collection of IO objects, calling a block whenever
7
+ # any monitored object either receives data or times out, or when the Reactor shuts down.
8
+ #
9
+ # The waiting/wake up is performed with nio4r, which will use the appropriate backend (libev,
10
+ # Java NIO or just plain IO#select). The call to `NIO::Selector#select` will
11
+ # 'wakeup' any IO object that receives data.
12
+ #
13
+ # This class additionally tracks a timeout for every added object,
14
+ # and wakes up any object when its timeout elapses.
15
+ #
16
+ # The implementation uses a Queue to synchronize adding new objects from the internal select loop.
17
+ class Reactor
18
+
19
+ # @!attribute [rw] reactor_max
20
+ # Maximum number of clients in the selector. Reset with calls to `Server.stats`.
21
+ attr_accessor :reactor_max
22
+ attr_reader :reactor_size
23
+
24
+ # Create a new Reactor to monitor IO objects added by #add.
25
+ # The provided block will be invoked when an IO has data available to read,
26
+ # its timeout elapses, or when the Reactor shuts down.
27
+ def initialize(backend, &block)
28
+ require 'nio'
29
+ valid_backends = [:auto, *::NIO::Selector.backends]
30
+ unless valid_backends.include?(backend)
31
+ raise ArgumentError.new("unsupported IO selector backend: #{backend} (available backends: #{valid_backends.join(', ')})")
32
+ end
33
+
34
+ @selector = ::NIO::Selector.new(NIO::Selector.backends.delete(backend))
35
+ @input = Queue.new
36
+ @timeouts = []
37
+ @block = block
38
+ @reactor_size = 0
39
+ @reactor_max = 0
40
+ end
41
+
42
+ # Run the internal select loop, using a background thread by default.
43
+ def run(background=true)
44
+ if background
45
+ @thread = Thread.new do
46
+ Puma.set_thread_name "reactor"
47
+ select_loop
48
+ end
49
+ else
50
+ select_loop
51
+ end
52
+ end
53
+
54
+ # Add a new client to monitor.
55
+ # The object must respond to #timeout and #timeout_at.
56
+ # Returns false if the reactor is already shut down.
57
+ def add(client)
58
+ @input << client
59
+ @selector.wakeup
60
+ true
61
+ rescue ClosedQueueError, IOError # Ignore if selector is already closed
62
+ false
63
+ end
64
+
65
+ # Shutdown the reactor, blocking until the background thread is finished.
66
+ def shutdown
67
+ @input.close
68
+ begin
69
+ @selector.wakeup
70
+ rescue IOError # Ignore if selector is already closed
71
+ end
72
+ @thread&.join
73
+ end
74
+
75
+ private
76
+
77
+ def select_loop
78
+ begin
79
+ until @input.closed? && @input.empty?
80
+ # Wakeup any registered object that receives incoming data.
81
+ # Block until the earliest timeout or Selector#wakeup is called.
82
+ timeout = (earliest = @timeouts.first) && earliest.timeout
83
+ @selector.select(timeout) do |monitor|
84
+ wakeup!(monitor.value)
85
+ end
86
+
87
+ # Wakeup all objects that timed out.
88
+ timed_out = @timeouts.take_while { |client| client.timeout == 0 }
89
+ timed_out.each { |client| wakeup!(client) }
90
+
91
+ unless @input.empty?
92
+ until @input.empty?
93
+ client = @input.pop
94
+ register(client) if client.io_ok?
95
+ end
96
+ @timeouts.sort_by!(&:timeout_at)
97
+ end
98
+ end
99
+ rescue StandardError => e
100
+ STDERR.puts "Error in reactor loop escaped: #{e.message} (#{e.class})"
101
+ STDERR.puts e.backtrace
102
+
103
+ retry
104
+ end
105
+
106
+ # Wakeup all remaining objects on shutdown.
107
+ @timeouts.each(&@block)
108
+ @selector.close
109
+ end
110
+
111
+ # Start monitoring the object.
112
+ def register(client)
113
+ @selector.register(client.to_io, :r).value = client
114
+ @reactor_size += 1
115
+ @reactor_max = @reactor_size if @reactor_max < @reactor_size
116
+ @timeouts << client
117
+ rescue ArgumentError
118
+ # unreadable clients raise error when processed by NIO
119
+ end
120
+
121
+ # 'Wake up' a monitored object by calling the provided block.
122
+ # Stop monitoring the object if the block returns `true`.
123
+ def wakeup!(client)
124
+ if @block.call client
125
+ @selector.deregister client.to_io
126
+ @reactor_size -= 1
127
+ @timeouts.delete client
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,532 @@
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 Response # :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'
33
+
34
+ include Puma::Const
35
+
36
+ # Takes the request contained in +client+, invokes the Rack application to construct
37
+ # the response and writes it back to +client.io+.
38
+ #
39
+ # It'll return +:close+ when the connection is closed, this doesn't mean
40
+ # that the response wasn't successful.
41
+ #
42
+ # It'll return +:keep_alive+ if the connection is a pipeline or keep-alive connection.
43
+ # Which may contain additional requests.
44
+ #
45
+ # It'll return +:async+ if the connection remains open but will be handled
46
+ # elsewhere, i.e. the connection has been hijacked by the Rack application.
47
+ #
48
+ # Finally, it'll return +true+ on keep-alive connections.
49
+ # @param processor [Puma::ThreadPool::ProcessorThread]
50
+ # @param client [Puma::Client]
51
+ # @param requests [Integer]
52
+ # @return [:close, :keep_alive, :async]
53
+ def handle_request(processor, client, requests)
54
+ env = client.env
55
+ io_buffer = client.io_buffer
56
+ socket = client.io # io may be a MiniSSL::Socket
57
+ app_body = nil
58
+ error = nil
59
+
60
+ return :close if closed_socket?(socket)
61
+
62
+ if @early_hints
63
+ env[EARLY_HINTS] = lambda { |headers|
64
+ begin
65
+ unless (str = str_early_hints headers).empty?
66
+ fast_write_str socket, "HTTP/1.1 103 Early Hints\r\n#{str}\r\n"
67
+ end
68
+ rescue ConnectionError => e
69
+ @log_writer.debug_error e
70
+ # noop, if we lost the socket we just won't send the early hints
71
+ end
72
+ }
73
+ end
74
+
75
+ env["puma.mark_as_io_bound"] = -> { processor.mark_as_io_thread! }
76
+
77
+ begin
78
+ status, headers, app_body = @thread_pool.with_force_shutdown do
79
+ @app.call(env)
80
+ end
81
+
82
+ # app_body needs to always be closed, hold value in case lowlevel_error
83
+ # is called
84
+ res_body = app_body
85
+
86
+ # full hijack, app called env['rack.hijack']
87
+ return :async if client.hijacked
88
+
89
+ status = status.to_i
90
+
91
+ if status == -1
92
+ unless headers.empty? and res_body == []
93
+ raise "async response must have empty headers and body"
94
+ end
95
+
96
+ return :async
97
+ end
98
+ rescue ThreadPool::ForceShutdown => error
99
+ @log_writer.unknown_error error, client, "Rack app"
100
+ @log_writer.log "Detected force shutdown of a thread"
101
+
102
+ status, headers, res_body = lowlevel_error(error, env, 503)
103
+ rescue Exception => error
104
+ @log_writer.unknown_error error, client, "Rack app"
105
+
106
+ status, headers, res_body = lowlevel_error(error, env, 500)
107
+ end
108
+ prepare_response(status, headers, res_body, requests, client)
109
+ ensure
110
+ io_buffer.reset
111
+ app_body.close if app_body.respond_to? :close
112
+ client&.tempfile_close
113
+ if after_reply = env[RACK_AFTER_REPLY]
114
+ after_reply.each do |o|
115
+ begin
116
+ o.call
117
+ rescue StandardError => e
118
+ @log_writer.debug_error e
119
+ end
120
+ end
121
+ end
122
+
123
+ if response_finished = env[RACK_RESPONSE_FINISHED]
124
+ response_finished.reverse_each do |o|
125
+ begin
126
+ o.call(env, status, headers, error)
127
+ rescue StandardError => e
128
+ @log_writer.debug_error e
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # Assembles the headers and prepares the body for actually sending the
135
+ # response via `#fast_write_response`.
136
+ #
137
+ # @param status [Integer] the status returned by the Rack application
138
+ # @param headers [Hash] the headers returned by the Rack application
139
+ # @param res_body [Array] the body returned by the Rack application or
140
+ # a call to `Server#lowlevel_error`
141
+ # @param requests [Integer] number of inline requests handled
142
+ # @param client [Puma::Client]
143
+ # @return [:close, :keep_alive, :async]
144
+ def prepare_response(status, headers, res_body, requests, client)
145
+ env = client.env
146
+ socket = client.io
147
+ io_buffer = client.io_buffer
148
+
149
+ return :close if closed_socket?(socket)
150
+
151
+ # Close the connection after a reasonable number of inline requests
152
+ force_keep_alive = @enable_keep_alives && client.requests_served < @max_keep_alive
153
+
154
+ resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
155
+
156
+ close_body = false
157
+ response_hijack = nil
158
+ content_length = resp_info[:content_length]
159
+ keep_alive = resp_info[:keep_alive]
160
+
161
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
162
+ # below converts app_body into body, dependent on app_body's characteristics, and
163
+ # content_length will be set if it can be determined
164
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
165
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
166
+ array_body.is_a?(Array)
167
+ body = array_body.compact
168
+ content_length = body.sum(&:bytesize)
169
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
170
+ body = res_body
171
+ content_length = body.size
172
+ elsif res_body.respond_to?(:to_path) && (fn = res_body.to_path) &&
173
+ File.readable?(fn)
174
+ body = File.open fn, 'rb'
175
+ content_length = body.size
176
+ close_body = true
177
+ else
178
+ body = res_body
179
+ end
180
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
181
+ (fn = res_body.to_path) && File.readable?(fn = res_body.to_path)
182
+ body = File.open fn, 'rb'
183
+ content_length = body.size
184
+ close_body = true
185
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
186
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
187
+ # Sprockets::Asset
188
+ content_length = res_body.bytesize unless content_length
189
+ if (body_str = res_body.to_hash[:source])
190
+ body = [body_str]
191
+ else # avoid each and use a File object
192
+ body = File.open fn, 'rb'
193
+ close_body = true
194
+ end
195
+ else
196
+ body = res_body
197
+ end
198
+ else
199
+ # partial hijack, from Rack spec:
200
+ # Servers must ignore the body part of the response tuple when the
201
+ # rack.hijack response header is present.
202
+ response_hijack = resp_info[:response_hijack] || res_body
203
+ end
204
+
205
+ line_ending = LINE_END
206
+
207
+ cork_socket socket
208
+
209
+ if resp_info[:no_body]
210
+ # 101 (Switching Protocols) doesn't return here or have content_length,
211
+ # it should be using `response_hijack`
212
+ unless status == 101
213
+ if content_length && status != 204
214
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
215
+ end
216
+
217
+ io_buffer << LINE_END
218
+ fast_write_str socket, io_buffer.read_and_reset
219
+
220
+ uncork_socket socket.flush
221
+ return keep_alive ? :keep_alive : :close
222
+ end
223
+ else
224
+ if content_length
225
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
226
+ chunked = false
227
+ elsif !response_hijack && resp_info[:allow_chunked]
228
+ io_buffer << TRANSFER_ENCODING_CHUNKED
229
+ chunked = true
230
+ end
231
+ end
232
+
233
+ io_buffer << line_ending
234
+
235
+ # partial hijack, we write headers, then hand the socket to the app via
236
+ # response_hijack.call
237
+ if response_hijack
238
+ fast_write_str socket, io_buffer.read_and_reset
239
+ uncork_socket socket.flush
240
+ response_hijack.call socket
241
+ return :async
242
+ end
243
+
244
+ fast_write_response socket, body, io_buffer, chunked, content_length.to_i
245
+ body.close if close_body
246
+
247
+ # if we're shutting down, close keep_alive connections
248
+ !shutting_down? && keep_alive ? :keep_alive : :close
249
+ end
250
+
251
+ # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
252
+ # and body segments (called by `fast_write_response`).
253
+ # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
254
+ # Large strings may not be written in one pass, especially if `io` is a
255
+ # `MiniSSL::Socket`.
256
+ # @param socket [#write_nonblock] the request/response socket
257
+ # @param str [String] the string written to the io
258
+ # @raise [ConnectionError]
259
+ #
260
+ def fast_write_str(socket, str)
261
+ n = 0
262
+ byte_size = str.bytesize
263
+ while n < byte_size
264
+ begin
265
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
266
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
267
+ unless socket.wait_writable WRITE_TIMEOUT
268
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
269
+ end
270
+ retry
271
+ rescue Errno::EPIPE, SystemCallError, IOError
272
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
273
+ end
274
+ end
275
+ end
276
+
277
+ # Used to write headers and body.
278
+ # Writes to a socket (normally `Client#io`) using `#fast_write_str`.
279
+ # Accumulates `body` items into `io_buffer`, then writes to socket.
280
+ # @param socket [#write] the response socket
281
+ # @param body [Enumerable, File] the body object
282
+ # @param io_buffer [Puma::IOBuffer] contains headers
283
+ # @param chunked [Boolean]
284
+ # @paramn content_length [Integer
285
+ # @raise [ConnectionError]
286
+ #
287
+ def fast_write_response(socket, body, io_buffer, chunked, content_length)
288
+ if body.is_a?(::File) && body.respond_to?(:read)
289
+ if chunked # would this ever happen?
290
+ while chunk = body.read(BODY_LEN_MAX)
291
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
292
+ end
293
+ fast_write_str socket, CLOSE_CHUNKED
294
+ else
295
+ if content_length <= IO_BODY_MAX
296
+ io_buffer.write body.read(content_length)
297
+ fast_write_str socket, io_buffer.read_and_reset
298
+ else
299
+ fast_write_str socket, io_buffer.read_and_reset
300
+ IO.copy_stream body, socket
301
+ end
302
+ end
303
+ elsif body.is_a?(::Array) && body.length == 1
304
+ body_first = nil
305
+ # using body_first = body.first causes issues?
306
+ body.each { |str| body_first ||= str }
307
+
308
+ if body_first.is_a?(::String) && body_first.bytesize < BODY_LEN_MAX
309
+ # smaller body, write to io_buffer first
310
+ io_buffer.write body_first
311
+ fast_write_str socket, io_buffer.read_and_reset
312
+ else
313
+ # large body, write both header & body to socket
314
+ fast_write_str socket, io_buffer.read_and_reset
315
+ fast_write_str socket, body_first
316
+ end
317
+ elsif body.is_a?(::Array)
318
+ # for array bodies, flush io_buffer to socket when size is greater than
319
+ # IO_BUFFER_LEN_MAX
320
+ if chunked
321
+ body.each do |part|
322
+ next if (byte_size = part.bytesize).zero?
323
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
324
+ if io_buffer.length > IO_BUFFER_LEN_MAX
325
+ fast_write_str socket, io_buffer.read_and_reset
326
+ end
327
+ end
328
+ io_buffer.write CLOSE_CHUNKED
329
+ else
330
+ body.each do |part|
331
+ next if part.bytesize.zero?
332
+ io_buffer.write part
333
+ if io_buffer.length > IO_BUFFER_LEN_MAX
334
+ fast_write_str socket, io_buffer.read_and_reset
335
+ end
336
+ end
337
+ end
338
+ # may write last body part for non-chunked, also headers if array is empty
339
+ fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
340
+ else
341
+ # for enum bodies
342
+ if chunked
343
+ empty_body = true
344
+ body.each do |part|
345
+ next if part.nil? || (byte_size = part.bytesize).zero?
346
+ empty_body = false
347
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
348
+ fast_write_str socket, io_buffer.read_and_reset
349
+ end
350
+ if empty_body
351
+ io_buffer << CLOSE_CHUNKED
352
+ fast_write_str socket, io_buffer.read_and_reset
353
+ else
354
+ fast_write_str socket, CLOSE_CHUNKED
355
+ end
356
+ else
357
+ fast_write_str socket, io_buffer.read_and_reset
358
+ body.each do |part|
359
+ next if part.bytesize.zero?
360
+ fast_write_str socket, part
361
+ end
362
+ end
363
+ end
364
+ socket.flush
365
+ uncork_socket socket
366
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
367
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
368
+ rescue Errno::EPIPE, SystemCallError, IOError
369
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
370
+ end
371
+
372
+ private :fast_write_str, :fast_write_response
373
+
374
+ # @param header_key [#to_s]
375
+ # @return [Boolean]
376
+ #
377
+ def illegal_header_key?(header_key)
378
+ !!(ILLEGAL_HEADER_KEY_REGEX =~ header_key.to_s)
379
+ end
380
+
381
+ # @param header_value [#to_s]
382
+ # @return [Boolean]
383
+ #
384
+ def illegal_header_value?(header_value)
385
+ !!(ILLEGAL_HEADER_VALUE_REGEX =~ header_value.to_s)
386
+ end
387
+
388
+ # Used in the lambda for env[ `Puma::Const::EARLY_HINTS` ]
389
+ # @param headers [Hash] the headers returned by the Rack application
390
+ # @return [String]
391
+ # @version 5.0.3
392
+ #
393
+ def str_early_hints(headers)
394
+ eh_str = +""
395
+ headers.each_pair do |k, vs|
396
+ next if illegal_header_key?(k)
397
+
398
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
399
+ vs.to_s.split(NEWLINE).each do |v|
400
+ next if illegal_header_value?(v)
401
+ eh_str << "#{k}: #{v}\r\n"
402
+ end
403
+ elsif !(vs.to_s.empty? || !illegal_header_value?(vs))
404
+ eh_str << "#{k}: #{vs}\r\n"
405
+ end
406
+ end
407
+ eh_str.freeze
408
+ end
409
+ private :str_early_hints
410
+
411
+ # @param status [Integer] status from the app
412
+ # @return [String] the text description from Puma::HTTP_STATUS_CODES
413
+ #
414
+ def fetch_status_code(status)
415
+ HTTP_STATUS_CODES.fetch(status) { CUSTOM_STAT }
416
+ end
417
+ private :fetch_status_code
418
+
419
+ # Processes and write headers to the IOBuffer.
420
+ # @param env [Hash] see Puma::Client#env, from request
421
+ # @param status [Integer] the status returned by the Rack application
422
+ # @param headers [Hash] the headers returned by the Rack application
423
+ # @param content_length [Integer,nil] content length if it can be determined from the
424
+ # response body
425
+ # @param io_buffer [Puma::IOBuffer] modified inn place
426
+ # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
427
+ # status and `@max_keep_alive`
428
+ # @return [Hash] resp_info
429
+ # @version 5.0.3
430
+ #
431
+ def str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
432
+
433
+ line_ending = LINE_END
434
+ colon = COLON
435
+
436
+ resp_info = {}
437
+ resp_info[:no_body] = env[REQUEST_METHOD] == HEAD
438
+
439
+ http_11 = env[SERVER_PROTOCOL] == HTTP_11
440
+ if http_11
441
+ resp_info[:allow_chunked] = true
442
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
443
+
444
+ # An optimization. The most common response is 200, so we can
445
+ # reply with the proper 200 status without having to compute
446
+ # the response header.
447
+ #
448
+ if status == 200
449
+ io_buffer << HTTP_11_200
450
+ else
451
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
452
+
453
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
454
+ end
455
+ else
456
+ resp_info[:allow_chunked] = false
457
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
458
+
459
+ # Same optimization as above for HTTP/1.1
460
+ #
461
+ if status == 200
462
+ io_buffer << HTTP_10_200
463
+ else
464
+ io_buffer.append "HTTP/1.0 #{status} ",
465
+ fetch_status_code(status), line_ending
466
+
467
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
468
+ end
469
+ end
470
+
471
+ # regardless of what the client wants, we always close the connection
472
+ # if running without request queueing
473
+ resp_info[:keep_alive] &&= @queue_requests
474
+
475
+ # see prepare_response
476
+ resp_info[:keep_alive] &&= force_keep_alive
477
+
478
+ resp_info[:response_hijack] = nil
479
+
480
+ headers.each do |k, vs|
481
+ next if illegal_header_key?(k)
482
+
483
+ key = k.downcase
484
+ case key
485
+ when CONTENT_LENGTH2
486
+ next if illegal_header_value?(vs)
487
+ # nil.to_i is 0, nil&.to_i is nil
488
+ resp_info[:content_length] = vs&.to_i
489
+ next
490
+ when TRANSFER_ENCODING
491
+ resp_info[:allow_chunked] = false
492
+ resp_info[:content_length] = nil
493
+ resp_info[:transfer_encoding] = vs
494
+ when HIJACK
495
+ resp_info[:response_hijack] = vs
496
+ next
497
+ when BANNED_HEADER_KEY
498
+ next
499
+ end
500
+
501
+ ary = if vs.is_a?(::Array) && !vs.empty?
502
+ vs
503
+ elsif vs.respond_to?(:to_s) && !vs.to_s.empty?
504
+ vs.to_s.split NEWLINE
505
+ else
506
+ nil
507
+ end
508
+ if ary
509
+ ary.each do |v|
510
+ next if illegal_header_value?(v)
511
+ io_buffer.append key, colon, v, line_ending
512
+ end
513
+ else
514
+ io_buffer.append key, colon, line_ending
515
+ end
516
+ end
517
+
518
+ # HTTP/1.1 & 1.0 assume different defaults:
519
+ # - HTTP 1.0 assumes the connection will be closed if not specified
520
+ # - HTTP 1.1 assumes the connection will be kept alive if not specified.
521
+ # Only set the header if we're doing something which is not the default
522
+ # for this protocol version
523
+ if http_11
524
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
525
+ else
526
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
527
+ end
528
+ resp_info
529
+ end
530
+ private :str_headers
531
+ end
532
+ end