puma 5.6.4 → 6.3.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

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