puma 5.6.9-java → 6.6.0-java

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +465 -18
  3. data/README.md +152 -42
  4. data/bin/puma-wild +1 -1
  5. data/docs/compile_options.md +34 -0
  6. data/docs/fork_worker.md +12 -4
  7. data/docs/java_options.md +54 -0
  8. data/docs/kubernetes.md +12 -0
  9. data/docs/nginx.md +1 -1
  10. data/docs/plugins.md +4 -0
  11. data/docs/restart.md +1 -0
  12. data/docs/signals.md +2 -2
  13. data/docs/stats.md +8 -3
  14. data/docs/systemd.md +13 -7
  15. data/docs/testing_benchmarks_local_files.md +150 -0
  16. data/docs/testing_test_rackup_ci_files.md +36 -0
  17. data/ext/puma_http11/extconf.rb +27 -17
  18. data/ext/puma_http11/http11_parser.c +1 -1
  19. data/ext/puma_http11/http11_parser.h +1 -1
  20. data/ext/puma_http11/http11_parser.java.rl +2 -2
  21. data/ext/puma_http11/http11_parser.rl +2 -2
  22. data/ext/puma_http11/http11_parser_common.rl +2 -2
  23. data/ext/puma_http11/mini_ssl.c +137 -19
  24. data/ext/puma_http11/org/jruby/puma/Http11.java +31 -10
  25. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +1 -1
  26. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +157 -53
  27. data/ext/puma_http11/puma_http11.c +21 -10
  28. data/lib/puma/app/status.rb +4 -4
  29. data/lib/puma/binder.rb +60 -55
  30. data/lib/puma/cli.rb +22 -20
  31. data/lib/puma/client.rb +93 -30
  32. data/lib/puma/cluster/worker.rb +27 -17
  33. data/lib/puma/cluster/worker_handle.rb +8 -6
  34. data/lib/puma/cluster.rb +121 -47
  35. data/lib/puma/commonlogger.rb +21 -14
  36. data/lib/puma/configuration.rb +101 -65
  37. data/lib/puma/const.rb +141 -93
  38. data/lib/puma/control_cli.rb +19 -15
  39. data/lib/puma/detect.rb +7 -4
  40. data/lib/puma/dsl.rb +521 -88
  41. data/lib/puma/error_logger.rb +22 -13
  42. data/lib/puma/events.rb +6 -126
  43. data/lib/puma/io_buffer.rb +39 -4
  44. data/lib/puma/jruby_restart.rb +0 -15
  45. data/lib/puma/launcher/bundle_pruner.rb +104 -0
  46. data/lib/puma/launcher.rb +121 -181
  47. data/lib/puma/log_writer.rb +147 -0
  48. data/lib/puma/minissl/context_builder.rb +27 -12
  49. data/lib/puma/minissl.rb +105 -11
  50. data/lib/puma/null_io.rb +42 -2
  51. data/lib/puma/plugin/systemd.rb +90 -0
  52. data/lib/puma/plugin/tmp_restart.rb +1 -1
  53. data/lib/puma/puma_http11.jar +0 -0
  54. data/lib/puma/rack/builder.rb +6 -6
  55. data/lib/puma/rack/urlmap.rb +1 -1
  56. data/lib/puma/rack_default.rb +19 -4
  57. data/lib/puma/reactor.rb +19 -10
  58. data/lib/puma/request.rb +368 -169
  59. data/lib/puma/runner.rb +65 -22
  60. data/lib/puma/sd_notify.rb +146 -0
  61. data/lib/puma/server.rb +161 -102
  62. data/lib/puma/single.rb +13 -11
  63. data/lib/puma/state_file.rb +3 -6
  64. data/lib/puma/thread_pool.rb +71 -21
  65. data/lib/puma/util.rb +1 -12
  66. data/lib/puma.rb +9 -10
  67. data/lib/rack/handler/puma.rb +116 -86
  68. data/tools/Dockerfile +2 -2
  69. metadata +17 -12
  70. data/lib/puma/queue_close.rb +0 -26
  71. data/lib/puma/systemd.rb +0 -46
  72. data/lib/rack/version_restriction.rb +0 -15
data/lib/puma/request.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Puma
4
+ #———————————————————————— DO NOT USE — this class is for internal use only ———
5
+
4
6
 
5
7
  # The methods here are included in Server, but are separated into this file.
6
8
  # All the methods here pertain to passing the request to the app, then
@@ -10,7 +12,24 @@ module Puma
10
12
  # #handle_request, which is called in Server#process_client.
11
13
  # @version 5.0.3
12
14
  #
13
- module Request
15
+ module Request # :nodoc:
16
+
17
+ # Single element array body: smaller bodies are written to io_buffer first,
18
+ # then a single write from io_buffer. Larger sizes are written separately.
19
+ # Also fixes max size of chunked file body read.
20
+ BODY_LEN_MAX = 1_024 * 256
21
+
22
+ # File body: smaller bodies are combined with io_buffer, then written to
23
+ # socket. Larger bodies are written separately using `copy_stream`
24
+ IO_BODY_MAX = 1_024 * 64
25
+
26
+ # Array body: elements are collected in io_buffer. When io_buffer's size
27
+ # exceeds value, they are written to the socket.
28
+ IO_BUFFER_LEN_MAX = 1_024 * 512
29
+
30
+ SOCKET_WRITE_ERR_MSG = "Socket timeout writing data"
31
+
32
+ CUSTOM_STAT = 'CUSTOM'
14
33
 
15
34
  include Puma::Const
16
35
 
@@ -25,40 +44,43 @@ module Puma
25
44
  #
26
45
  # Finally, it'll return +true+ on keep-alive connections.
27
46
  # @param client [Puma::Client]
28
- # @param lines [Puma::IOBuffer]
29
47
  # @param requests [Integer]
30
48
  # @return [Boolean,:async]
31
49
  #
32
- def handle_request(client, lines, requests)
50
+ def handle_request(client, requests)
33
51
  env = client.env
34
- io = client.io # io may be a MiniSSL::Socket
52
+ io_buffer = client.io_buffer
53
+ socket = client.io # io may be a MiniSSL::Socket
54
+ app_body = nil
55
+
56
+ return false if closed_socket?(socket)
35
57
 
36
- return false if closed_socket?(io)
58
+ if client.http_content_length_limit_exceeded
59
+ return prepare_response(413, {}, ["Payload Too Large"], requests, client)
60
+ end
37
61
 
38
62
  normalize_env env, client
39
63
 
40
- env[PUMA_SOCKET] = io
64
+ env[PUMA_SOCKET] = socket
41
65
 
42
- if env[HTTPS_KEY] && io.peercert
43
- env[PUMA_PEERCERT] = io.peercert
66
+ if env[HTTPS_KEY] && socket.peercert
67
+ env[PUMA_PEERCERT] = socket.peercert
44
68
  end
45
69
 
46
70
  env[HIJACK_P] = true
47
71
  env[HIJACK] = client
48
72
 
49
- body = client.body
50
-
51
- head = env[REQUEST_METHOD] == HEAD
52
-
53
- env[RACK_INPUT] = body
73
+ env[RACK_INPUT] = client.body
54
74
  env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP
55
75
 
56
76
  if @early_hints
57
77
  env[EARLY_HINTS] = lambda { |headers|
58
78
  begin
59
- fast_write io, str_early_hints(headers)
79
+ unless (str = str_early_hints headers).empty?
80
+ fast_write_str socket, "HTTP/1.1 103 Early Hints\r\n#{str}\r\n"
81
+ end
60
82
  rescue ConnectionError => e
61
- @events.debug_error e
83
+ @log_writer.debug_error e
62
84
  # noop, if we lost the socket we just won't send the early hints
63
85
  end
64
86
  }
@@ -69,123 +91,179 @@ module Puma
69
91
  # A rack extension. If the app writes #call'ables to this
70
92
  # array, we will invoke them when the request is done.
71
93
  #
72
- after_reply = env[RACK_AFTER_REPLY] = []
94
+ env[RACK_AFTER_REPLY] ||= []
73
95
 
74
96
  begin
75
- begin
76
- status, headers, res_body = @thread_pool.with_force_shutdown do
97
+ if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
98
+ status, headers, app_body = @thread_pool.with_force_shutdown do
77
99
  @app.call(env)
78
100
  end
101
+ else
102
+ @log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
103
+ status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
104
+ end
79
105
 
80
- return :async if client.hijacked
106
+ # app_body needs to always be closed, hold value in case lowlevel_error
107
+ # is called
108
+ res_body = app_body
81
109
 
82
- status = status.to_i
110
+ # full hijack, app called env['rack.hijack']
111
+ return :async if client.hijacked
83
112
 
84
- if status == -1
85
- unless headers.empty? and res_body == []
86
- raise "async response must have empty headers and body"
87
- end
113
+ status = status.to_i
88
114
 
89
- return :async
115
+ if status == -1
116
+ unless headers.empty? and res_body == []
117
+ raise "async response must have empty headers and body"
90
118
  end
91
- rescue ThreadPool::ForceShutdown => e
92
- @events.unknown_error e, client, "Rack app"
93
- @events.log "Detected force shutdown of a thread"
94
119
 
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)
120
+ return :async
100
121
  end
122
+ rescue ThreadPool::ForceShutdown => e
123
+ @log_writer.unknown_error e, client, "Rack app"
124
+ @log_writer.log "Detected force shutdown of a thread"
101
125
 
102
- res_info = {}
103
- res_info[:content_length] = nil
104
- res_info[:no_body] = head
126
+ status, headers, res_body = lowlevel_error(e, env, 503)
127
+ rescue Exception => e
128
+ @log_writer.unknown_error e, client, "Rack app"
105
129
 
106
- res_info[:content_length] = if res_body.kind_of? Array and res_body.size == 1
107
- res_body[0].bytesize
108
- else
109
- nil
110
- end
111
-
112
- cork_socket io
130
+ status, headers, res_body = lowlevel_error(e, env, 500)
131
+ end
132
+ prepare_response(status, headers, res_body, requests, client)
133
+ ensure
134
+ io_buffer.reset
135
+ uncork_socket client.io
136
+ app_body.close if app_body.respond_to? :close
137
+ client&.tempfile_close
138
+ after_reply = env[RACK_AFTER_REPLY] || []
139
+ begin
140
+ after_reply.each { |o| o.call }
141
+ rescue StandardError => e
142
+ @log_writer.debug_error e
143
+ end unless after_reply.empty?
144
+ end
113
145
 
114
- str_headers(env, status, headers, res_info, lines, requests, client)
146
+ # Assembles the headers and prepares the body for actually sending the
147
+ # response via `#fast_write_response`.
148
+ #
149
+ # @param status [Integer] the status returned by the Rack application
150
+ # @param headers [Hash] the headers returned by the Rack application
151
+ # @param res_body [Array] the body returned by the Rack application or
152
+ # a call to `Server#lowlevel_error`
153
+ # @param requests [Integer] number of inline requests handled
154
+ # @param client [Puma::Client]
155
+ # @return [Boolean,:async] keep-alive status or `:async`
156
+ def prepare_response(status, headers, res_body, requests, client)
157
+ env = client.env
158
+ socket = client.io
159
+ io_buffer = client.io_buffer
115
160
 
116
- line_ending = LINE_END
161
+ return false if closed_socket?(socket)
117
162
 
118
- content_length = res_info[:content_length]
119
- response_hijack = res_info[:response_hijack]
163
+ # Close the connection after a reasonable number of inline requests
164
+ # if the server is at capacity and the listener has a new connection ready.
165
+ # This allows Puma to service connections fairly when the number
166
+ # of concurrent connections exceeds the size of the threadpool.
167
+ force_keep_alive = if @enable_keep_alives
168
+ requests < @max_fast_inline ||
169
+ @thread_pool.busy_threads < @max_threads ||
170
+ !client.listener.to_io.wait_readable(0)
171
+ else
172
+ # Always set force_keep_alive to false if the server has keep-alives not enabled.
173
+ false
174
+ end
120
175
 
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
176
+ resp_info = str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
177
+
178
+ close_body = false
179
+ response_hijack = nil
180
+ content_length = resp_info[:content_length]
181
+ keep_alive = resp_info[:keep_alive]
182
+
183
+ if res_body.respond_to?(:each) && !resp_info[:response_hijack]
184
+ # below converts app_body into body, dependent on app_body's characteristics, and
185
+ # content_length will be set if it can be determined
186
+ if !content_length && !resp_info[:transfer_encoding] && status != 204
187
+ if res_body.respond_to?(:to_ary) && (array_body = res_body.to_ary) &&
188
+ array_body.is_a?(Array)
189
+ body = array_body.compact
190
+ content_length = body.sum(&:bytesize)
191
+ elsif res_body.is_a?(File) && res_body.respond_to?(:size)
192
+ body = res_body
193
+ content_length = body.size
194
+ elsif res_body.respond_to?(:to_path) && File.readable?(fn = res_body.to_path)
195
+ body = File.open fn, 'rb'
196
+ content_length = body.size
197
+ close_body = true
198
+ else
199
+ body = res_body
124
200
  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
201
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) &&
202
+ File.readable?(fn = res_body.to_path)
203
+ body = File.open fn, 'rb'
204
+ content_length = body.size
205
+ close_body = true
206
+ elsif !res_body.is_a?(::File) && res_body.respond_to?(:filename) &&
207
+ res_body.respond_to?(:bytesize) && File.readable?(fn = res_body.filename)
208
+ # Sprockets::Asset
209
+ content_length = res_body.bytesize unless content_length
210
+ if (body_str = res_body.to_hash[:source])
211
+ body = [body_str]
212
+ else # avoid each and use a File object
213
+ body = File.open fn, 'rb'
214
+ close_body = true
215
+ end
216
+ else
217
+ body = res_body
137
218
  end
219
+ else
220
+ # partial hijack, from Rack spec:
221
+ # Servers must ignore the body part of the response tuple when the
222
+ # rack.hijack response header is present.
223
+ response_hijack = resp_info[:response_hijack] || res_body
224
+ end
138
225
 
139
- lines << line_ending
140
-
141
- fast_write io, lines.to_s
226
+ line_ending = LINE_END
142
227
 
143
- if response_hijack
144
- response_hijack.call io
145
- return :async
146
- end
228
+ cork_socket socket
147
229
 
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
230
+ if resp_info[:no_body]
231
+ # 101 (Switching Protocols) doesn't return here or have content_length,
232
+ # it should be using `response_hijack`
233
+ unless status == 101
234
+ if content_length && status != 204
235
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
159
236
  end
160
237
 
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"
238
+ io_buffer << LINE_END
239
+ fast_write_str socket, io_buffer.read_and_reset
240
+ socket.flush
241
+ return keep_alive
167
242
  end
168
-
169
- ensure
170
- begin
171
- uncork_socket io
172
-
173
- body.close
174
- client.tempfile.unlink if client.tempfile
175
- ensure
176
- # Whatever happens, we MUST call `close` on the response body.
177
- # Otherwise Rack::BodyProxy callbacks may not fire and lead to various state leaks
178
- res_body.close if res_body.respond_to? :close
243
+ else
244
+ if content_length
245
+ io_buffer.append CONTENT_LENGTH_S, content_length.to_s, line_ending
246
+ chunked = false
247
+ elsif !response_hijack && resp_info[:allow_chunked]
248
+ io_buffer << TRANSFER_ENCODING_CHUNKED
249
+ chunked = true
179
250
  end
251
+ end
180
252
 
181
- begin
182
- after_reply.each { |o| o.call }
183
- rescue StandardError => e
184
- @log_writer.debug_error e
185
- end
253
+ io_buffer << line_ending
254
+
255
+ # partial hijack, we write headers, then hand the socket to the app via
256
+ # response_hijack.call
257
+ if response_hijack
258
+ fast_write_str socket, io_buffer.read_and_reset
259
+ uncork_socket socket
260
+ response_hijack.call socket
261
+ return :async
186
262
  end
187
263
 
188
- res_info[:keep_alive]
264
+ fast_write_response socket, body, io_buffer, chunked, content_length.to_i
265
+ body.close if close_body
266
+ keep_alive
189
267
  end
190
268
 
191
269
  # @param env [Hash] see Puma::Client#env, from request
@@ -199,45 +277,132 @@ module Puma
199
277
  end
200
278
  end
201
279
 
202
- # Writes to an io (normally Client#io) using #syswrite
203
- # @param io [#syswrite] the io to write to
280
+ # Used to write 'early hints', 'no body' responses, 'hijacked' responses,
281
+ # and body segments (called by `fast_write_response`).
282
+ # Writes a string to a socket (normally `Client#io`) using `write_nonblock`.
283
+ # Large strings may not be written in one pass, especially if `io` is a
284
+ # `MiniSSL::Socket`.
285
+ # @param socket [#write_nonblock] the request/response socket
204
286
  # @param str [String] the string written to the io
205
287
  # @raise [ConnectionError]
206
288
  #
207
- def fast_write(io, str)
289
+ def fast_write_str(socket, str)
208
290
  n = 0
209
- while true
291
+ byte_size = str.bytesize
292
+ while n < byte_size
210
293
  begin
211
- n = io.syswrite str
294
+ n += socket.write_nonblock(n.zero? ? str : str.byteslice(n..-1))
212
295
  rescue Errno::EAGAIN, Errno::EWOULDBLOCK
213
- unless io.wait_writable WRITE_TIMEOUT
214
- raise ConnectionError, "Socket timeout writing data"
296
+ unless socket.wait_writable WRITE_TIMEOUT
297
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
215
298
  end
216
-
217
299
  retry
218
300
  rescue Errno::EPIPE, SystemCallError, IOError
219
- raise ConnectionError, "Socket timeout writing data"
301
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
220
302
  end
221
-
222
- return if n == str.bytesize
223
- str = str.byteslice(n..-1)
224
303
  end
225
304
  end
226
- private :fast_write
227
305
 
228
- # @param status [Integer] status from the app
229
- # @return [String] the text description from Puma::HTTP_STATUS_CODES
306
+ # Used to write headers and body.
307
+ # Writes to a socket (normally `Client#io`) using `#fast_write_str`.
308
+ # Accumulates `body` items into `io_buffer`, then writes to socket.
309
+ # @param socket [#write] the response socket
310
+ # @param body [Enumerable, File] the body object
311
+ # @param io_buffer [Puma::IOBuffer] contains headers
312
+ # @param chunked [Boolean]
313
+ # @paramn content_length [Integer
314
+ # @raise [ConnectionError]
230
315
  #
231
- def fetch_status_code(status)
232
- HTTP_STATUS_CODES.fetch(status) { 'CUSTOM' }
316
+ def fast_write_response(socket, body, io_buffer, chunked, content_length)
317
+ if body.is_a?(::File) && body.respond_to?(:read)
318
+ if chunked # would this ever happen?
319
+ while chunk = body.read(BODY_LEN_MAX)
320
+ io_buffer.append chunk.bytesize.to_s(16), LINE_END, chunk, LINE_END
321
+ end
322
+ fast_write_str socket, CLOSE_CHUNKED
323
+ else
324
+ if content_length <= IO_BODY_MAX
325
+ io_buffer.write body.read(content_length)
326
+ fast_write_str socket, io_buffer.read_and_reset
327
+ else
328
+ fast_write_str socket, io_buffer.read_and_reset
329
+ IO.copy_stream body, socket
330
+ end
331
+ end
332
+ elsif body.is_a?(::Array) && body.length == 1
333
+ body_first = nil
334
+ # using body_first = body.first causes issues?
335
+ body.each { |str| body_first ||= str }
336
+
337
+ if body_first.is_a?(::String) && body_first.bytesize < BODY_LEN_MAX
338
+ # smaller body, write to io_buffer first
339
+ io_buffer.write body_first
340
+ fast_write_str socket, io_buffer.read_and_reset
341
+ else
342
+ # large body, write both header & body to socket
343
+ fast_write_str socket, io_buffer.read_and_reset
344
+ fast_write_str socket, body_first
345
+ end
346
+ elsif body.is_a?(::Array)
347
+ # for array bodies, flush io_buffer to socket when size is greater than
348
+ # IO_BUFFER_LEN_MAX
349
+ if chunked
350
+ body.each do |part|
351
+ next if (byte_size = part.bytesize).zero?
352
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
353
+ if io_buffer.length > IO_BUFFER_LEN_MAX
354
+ fast_write_str socket, io_buffer.read_and_reset
355
+ end
356
+ end
357
+ io_buffer.write CLOSE_CHUNKED
358
+ else
359
+ body.each do |part|
360
+ next if part.bytesize.zero?
361
+ io_buffer.write part
362
+ if io_buffer.length > IO_BUFFER_LEN_MAX
363
+ fast_write_str socket, io_buffer.read_and_reset
364
+ end
365
+ end
366
+ end
367
+ # may write last body part for non-chunked, also headers if array is empty
368
+ fast_write_str(socket, io_buffer.read_and_reset) unless io_buffer.length.zero?
369
+ else
370
+ # for enum bodies
371
+ if chunked
372
+ empty_body = true
373
+ body.each do |part|
374
+ next if part.nil? || (byte_size = part.bytesize).zero?
375
+ empty_body = false
376
+ io_buffer.append byte_size.to_s(16), LINE_END, part, LINE_END
377
+ fast_write_str socket, io_buffer.read_and_reset
378
+ end
379
+ if empty_body
380
+ io_buffer << CLOSE_CHUNKED
381
+ fast_write_str socket, io_buffer.read_and_reset
382
+ else
383
+ fast_write_str socket, CLOSE_CHUNKED
384
+ end
385
+ else
386
+ fast_write_str socket, io_buffer.read_and_reset
387
+ body.each do |part|
388
+ next if part.bytesize.zero?
389
+ fast_write_str socket, part
390
+ end
391
+ end
392
+ end
393
+ socket.flush
394
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
395
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
396
+ rescue Errno::EPIPE, SystemCallError, IOError
397
+ raise ConnectionError, SOCKET_WRITE_ERR_MSG
233
398
  end
234
- private :fetch_status_code
399
+
400
+ private :fast_write_str, :fast_write_response
235
401
 
236
402
  # Given a Hash +env+ for the request read from +client+, add
237
403
  # and fixup keys to comply with Rack's env guidelines.
238
404
  # @param env [Hash] see Puma::Client#env, from request
239
405
  # @param client [Puma::Client] only needed for Client#peerip
240
- # @todo make private in 6.0.0
241
406
  #
242
407
  def normalize_env(env, client)
243
408
  if host = env[HTTP_HOST]
@@ -259,17 +424,19 @@ module Puma
259
424
 
260
425
  unless env[REQUEST_PATH]
261
426
  # it might be a dumbass full host request header
262
- uri = URI.parse(env[REQUEST_URI])
427
+ uri = begin
428
+ URI.parse(env[REQUEST_URI])
429
+ rescue URI::InvalidURIError
430
+ raise Puma::HttpParserError
431
+ end
263
432
  env[REQUEST_PATH] = uri.path
264
433
 
265
- raise "No REQUEST PATH" unless env[REQUEST_PATH]
266
-
267
434
  # A nil env value will cause a LintError (and fatal errors elsewhere),
268
435
  # so only set the env value if there actually is a value.
269
436
  env[QUERY_STRING] = uri.query if uri.query
270
437
  end
271
438
 
272
- env[PATH_INFO] = env[REQUEST_PATH]
439
+ env[PATH_INFO] = env[REQUEST_PATH].to_s # #to_s in case it's nil
273
440
 
274
441
  # From https://www.ietf.org/rfc/rfc3875 :
275
442
  # "Script authors should be aware that the REMOTE_ADDR and
@@ -285,17 +452,31 @@ module Puma
285
452
  addr = client.peerip
286
453
  rescue Errno::ENOTCONN
287
454
  # Client disconnects can result in an inability to get the
288
- # peeraddr from the socket; default to localhost.
289
- addr = LOCALHOST_IP
455
+ # peeraddr from the socket; default to unspec.
456
+ if client.peer_family == Socket::AF_INET6
457
+ addr = UNSPECIFIED_IPV6
458
+ else
459
+ addr = UNSPECIFIED_IPV4
460
+ end
290
461
  end
291
462
 
292
463
  # Set unix socket addrs to localhost
293
- addr = LOCALHOST_IP if addr.empty?
464
+ if addr.empty?
465
+ if client.peer_family == Socket::AF_INET6
466
+ addr = LOCALHOST_IPV6
467
+ else
468
+ addr = LOCALHOST_IPV4
469
+ end
470
+ end
294
471
 
295
472
  env[REMOTE_ADDR] = addr
296
473
  end
474
+
475
+ # The legacy HTTP_VERSION header can be sent as a client header.
476
+ # Rack v4 may remove using HTTP_VERSION. If so, remove this line.
477
+ env[HTTP_VERSION] = env[SERVER_PROTOCOL]
297
478
  end
298
- # private :normalize_env
479
+ private :normalize_env
299
480
 
300
481
  # @param header_key [#to_s]
301
482
  # @return [Boolean]
@@ -367,7 +548,7 @@ module Puma
367
548
  # @version 5.0.3
368
549
  #
369
550
  def str_early_hints(headers)
370
- eh_str = "HTTP/1.1 103 Early Hints\r\n".dup
551
+ eh_str = +""
371
552
  headers.each_pair do |k, vs|
372
553
  next if illegal_header_key?(k)
373
554
 
@@ -376,74 +557,82 @@ module Puma
376
557
  next if illegal_header_value?(v)
377
558
  eh_str << "#{k}: #{v}\r\n"
378
559
  end
379
- else
560
+ elsif !(vs.to_s.empty? || !illegal_header_value?(vs))
380
561
  eh_str << "#{k}: #{vs}\r\n"
381
562
  end
382
563
  end
383
- "#{eh_str}\r\n".freeze
564
+ eh_str.freeze
384
565
  end
385
566
  private :str_early_hints
386
567
 
568
+ # @param status [Integer] status from the app
569
+ # @return [String] the text description from Puma::HTTP_STATUS_CODES
570
+ #
571
+ def fetch_status_code(status)
572
+ HTTP_STATUS_CODES.fetch(status) { CUSTOM_STAT }
573
+ end
574
+ private :fetch_status_code
575
+
387
576
  # Processes and write headers to the IOBuffer.
388
577
  # @param env [Hash] see Puma::Client#env, from request
389
578
  # @param status [Integer] the status returned by the Rack application
390
579
  # @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
393
- # @param requests [Integer] number of inline requests handled
394
- # @param client [Puma::Client]
580
+ # @param content_length [Integer,nil] content length if it can be determined from the
581
+ # response body
582
+ # @param io_buffer [Puma::IOBuffer] modified inn place
583
+ # @param force_keep_alive [Boolean] 'anded' with keep_alive, based on system
584
+ # status and `@max_fast_inline`
585
+ # @return [Hash] resp_info
395
586
  # @version 5.0.3
396
587
  #
397
- def str_headers(env, status, headers, res_info, lines, requests, client)
588
+ def str_headers(env, status, headers, res_body, io_buffer, force_keep_alive)
589
+
398
590
  line_ending = LINE_END
399
591
  colon = COLON
400
592
 
401
- http_11 = env[HTTP_VERSION] == HTTP_11
593
+ resp_info = {}
594
+ resp_info[:no_body] = env[REQUEST_METHOD] == HEAD
595
+
596
+ http_11 = env[SERVER_PROTOCOL] == HTTP_11
402
597
  if http_11
403
- res_info[:allow_chunked] = true
404
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
598
+ resp_info[:allow_chunked] = true
599
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase != CLOSE
405
600
 
406
601
  # An optimization. The most common response is 200, so we can
407
602
  # reply with the proper 200 status without having to compute
408
603
  # the response header.
409
604
  #
410
605
  if status == 200
411
- lines << HTTP_11_200
606
+ io_buffer << HTTP_11_200
412
607
  else
413
- lines.append "HTTP/1.1 ", status.to_s, " ",
414
- fetch_status_code(status), line_ending
608
+ io_buffer.append "#{HTTP_11} #{status} ", fetch_status_code(status), line_ending
415
609
 
416
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
610
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
417
611
  end
418
612
  else
419
- res_info[:allow_chunked] = false
420
- res_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
613
+ resp_info[:allow_chunked] = false
614
+ resp_info[:keep_alive] = env.fetch(HTTP_CONNECTION, "").downcase == KEEP_ALIVE
421
615
 
422
616
  # Same optimization as above for HTTP/1.1
423
617
  #
424
618
  if status == 200
425
- lines << HTTP_10_200
619
+ io_buffer << HTTP_10_200
426
620
  else
427
- lines.append "HTTP/1.0 ", status.to_s, " ",
621
+ io_buffer.append "HTTP/1.0 #{status} ",
428
622
  fetch_status_code(status), line_ending
429
623
 
430
- res_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
624
+ resp_info[:no_body] ||= status < 200 || STATUS_WITH_NO_ENTITY_BODY[status]
431
625
  end
432
626
  end
433
627
 
434
628
  # regardless of what the client wants, we always close the connection
435
629
  # if running without request queueing
436
- res_info[:keep_alive] &&= @queue_requests
630
+ resp_info[:keep_alive] &&= @queue_requests
437
631
 
438
- # Close the connection after a reasonable number of inline requests
439
- # if the server is at capacity and the listener has a new connection ready.
440
- # This allows Puma to service connections fairly when the number
441
- # of concurrent connections exceeds the size of the threadpool.
442
- res_info[:keep_alive] &&= requests < @max_fast_inline ||
443
- @thread_pool.busy_threads < @max_threads ||
444
- !client.listener.to_io.wait_readable(0)
632
+ # see prepare_response
633
+ resp_info[:keep_alive] &&= force_keep_alive
445
634
 
446
- res_info[:response_hijack] = nil
635
+ resp_info[:response_hijack] = nil
447
636
 
448
637
  headers.each do |k, vs|
449
638
  next if illegal_header_key?(k)
@@ -451,25 +640,34 @@ module Puma
451
640
  case k.downcase
452
641
  when CONTENT_LENGTH2
453
642
  next if illegal_header_value?(vs)
454
- res_info[:content_length] = vs
643
+ # nil.to_i is 0, nil&.to_i is nil
644
+ resp_info[:content_length] = vs&.to_i
455
645
  next
456
646
  when TRANSFER_ENCODING
457
- res_info[:allow_chunked] = false
458
- res_info[:content_length] = nil
647
+ resp_info[:allow_chunked] = false
648
+ resp_info[:content_length] = nil
649
+ resp_info[:transfer_encoding] = vs
459
650
  when HIJACK
460
- res_info[:response_hijack] = vs
651
+ resp_info[:response_hijack] = vs
461
652
  next
462
653
  when BANNED_HEADER_KEY
463
654
  next
464
655
  end
465
656
 
466
- if vs.respond_to?(:to_s) && !vs.to_s.empty?
467
- vs.to_s.split(NEWLINE).each do |v|
657
+ ary = if vs.is_a?(::Array) && !vs.empty?
658
+ vs
659
+ elsif vs.respond_to?(:to_s) && !vs.to_s.empty?
660
+ vs.to_s.split NEWLINE
661
+ else
662
+ nil
663
+ end
664
+ if ary
665
+ ary.each do |v|
468
666
  next if illegal_header_value?(v)
469
- lines.append k, colon, v, line_ending
667
+ io_buffer.append k, colon, v, line_ending
470
668
  end
471
669
  else
472
- lines.append k, colon, line_ending
670
+ io_buffer.append k, colon, line_ending
473
671
  end
474
672
  end
475
673
 
@@ -479,10 +677,11 @@ module Puma
479
677
  # Only set the header if we're doing something which is not the default
480
678
  # for this protocol version
481
679
  if http_11
482
- lines << CONNECTION_CLOSE if !res_info[:keep_alive]
680
+ io_buffer << CONNECTION_CLOSE if !resp_info[:keep_alive]
483
681
  else
484
- lines << CONNECTION_KEEP_ALIVE if res_info[:keep_alive]
682
+ io_buffer << CONNECTION_KEEP_ALIVE if resp_info[:keep_alive]
485
683
  end
684
+ resp_info
486
685
  end
487
686
  private :str_headers
488
687
  end