puma 3.7.1 → 4.1.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 (74) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +229 -1
  3. data/README.md +179 -212
  4. data/docs/architecture.md +37 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/extconf.rb +8 -0
  15. data/ext/puma_http11/http11_parser.c +84 -84
  16. data/ext/puma_http11/http11_parser.rl +9 -9
  17. data/ext/puma_http11/mini_ssl.c +105 -9
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +30 -6
  21. data/lib/puma.rb +10 -0
  22. data/lib/puma/accept_nonblock.rb +2 -0
  23. data/lib/puma/app/status.rb +13 -0
  24. data/lib/puma/binder.rb +33 -18
  25. data/lib/puma/cli.rb +48 -33
  26. data/lib/puma/client.rb +94 -22
  27. data/lib/puma/cluster.rb +69 -21
  28. data/lib/puma/commonlogger.rb +2 -0
  29. data/lib/puma/configuration.rb +134 -136
  30. data/lib/puma/const.rb +16 -2
  31. data/lib/puma/control_cli.rb +31 -18
  32. data/lib/puma/convenient.rb +5 -3
  33. data/lib/puma/daemon_ext.rb +2 -0
  34. data/lib/puma/delegation.rb +2 -0
  35. data/lib/puma/detect.rb +2 -0
  36. data/lib/puma/dsl.rb +349 -113
  37. data/lib/puma/events.rb +8 -4
  38. data/lib/puma/io_buffer.rb +3 -6
  39. data/lib/puma/jruby_restart.rb +2 -1
  40. data/lib/puma/launcher.rb +60 -36
  41. data/lib/puma/minissl.rb +85 -28
  42. data/lib/puma/null_io.rb +2 -0
  43. data/lib/puma/plugin.rb +2 -0
  44. data/lib/puma/plugin/tmp_restart.rb +3 -2
  45. data/lib/puma/rack/builder.rb +4 -1
  46. data/lib/puma/rack/urlmap.rb +2 -0
  47. data/lib/puma/rack_default.rb +2 -0
  48. data/lib/puma/reactor.rb +218 -30
  49. data/lib/puma/runner.rb +18 -4
  50. data/lib/puma/server.rb +149 -56
  51. data/lib/puma/single.rb +16 -5
  52. data/lib/puma/state_file.rb +2 -0
  53. data/lib/puma/tcp_logger.rb +2 -0
  54. data/lib/puma/thread_pool.rb +59 -6
  55. data/lib/puma/util.rb +2 -6
  56. data/lib/rack/handler/puma.rb +58 -19
  57. data/tools/jungle/README.md +12 -2
  58. data/tools/jungle/init.d/README.md +2 -0
  59. data/tools/jungle/init.d/puma +8 -8
  60. data/tools/jungle/init.d/run-puma +1 -1
  61. data/tools/jungle/rc.d/README.md +74 -0
  62. data/tools/jungle/rc.d/puma +61 -0
  63. data/tools/jungle/rc.d/puma.conf +10 -0
  64. data/tools/trickletest.rb +1 -1
  65. metadata +25 -85
  66. data/.github/issue_template.md +0 -20
  67. data/Gemfile +0 -12
  68. data/Manifest.txt +0 -77
  69. data/Rakefile +0 -158
  70. data/gemfiles/2.1-Gemfile +0 -12
  71. data/lib/puma/compat.rb +0 -14
  72. data/lib/puma/java_io_buffer.rb +0 -45
  73. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  74. data/puma.gemspec +0 -52
data/lib/puma.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Standard libraries
2
4
  require 'socket'
3
5
  require 'tempfile'
@@ -12,4 +14,12 @@ module Puma
12
14
  autoload :Const, 'puma/const'
13
15
  autoload :Server, 'puma/server'
14
16
  autoload :Launcher, 'puma/launcher'
17
+
18
+ def self.stats_object=(val)
19
+ @get_stats = val
20
+ end
21
+
22
+ def self.stats
23
+ @get_stats.stats
24
+ end
15
25
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'openssl'
2
4
 
3
5
  module OpenSSL
@@ -1,5 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
1
5
  module Puma
2
6
  module App
7
+ # Check out {#call}'s source code to see what actions this web application
8
+ # can respond to.
3
9
  class Status
4
10
  def initialize(cli)
5
11
  @cli = cli
@@ -55,6 +61,13 @@ module Puma
55
61
  return rack_response(200, OK_STATUS)
56
62
  end
57
63
 
64
+ when /\/gc$/
65
+ GC.start
66
+ return rack_response(200, OK_STATUS)
67
+
68
+ when /\/gc-stats$/
69
+ return rack_response(200, GC.stat.to_json)
70
+
58
71
  when /\/stats$/
59
72
  return rack_response(200, @cli.stats)
60
73
  else
data/lib/puma/binder.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'socket'
3
5
 
@@ -48,7 +50,14 @@ module Puma
48
50
 
49
51
  def close
50
52
  @ios.each { |i| i.close }
51
- @unix_paths.each { |i| File.unlink i }
53
+ @unix_paths.each do |i|
54
+ # Errno::ENOENT is intermittently raised
55
+ begin
56
+ unix_socket = UNIXSocket.new i
57
+ unix_socket.close
58
+ rescue Errno::ENOENT
59
+ end
60
+ end
52
61
  end
53
62
 
54
63
  def import_from_env
@@ -90,19 +99,19 @@ module Puma
90
99
  case uri.scheme
91
100
  when "tcp"
92
101
  if fd = @inherited_fds.delete(str)
93
- logger.log "* Inherited #{str}"
94
102
  io = inherit_tcp_listener uri.host, uri.port, fd
103
+ logger.log "* Inherited #{str}"
95
104
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
96
- logger.log "* Activated #{str}"
97
105
  io = inherit_tcp_listener uri.host, uri.port, sock
106
+ logger.log "* Activated #{str}"
98
107
  else
99
108
  params = Util.parse_query uri.query
100
109
 
101
110
  opt = params.key?('low_latency')
102
111
  bak = params.fetch('backlog', 1024).to_i
103
112
 
104
- logger.log "* Listening on #{str}"
105
113
  io = add_tcp_listener uri.host, uri.port, opt, bak
114
+ logger.log "* Listening on #{str}"
106
115
  end
107
116
 
108
117
  @listeners << [str, io] if io
@@ -110,17 +119,15 @@ module Puma
110
119
  path = "#{uri.host}#{uri.path}".gsub("%20", " ")
111
120
 
112
121
  if fd = @inherited_fds.delete(str)
113
- logger.log "* Inherited #{str}"
114
122
  io = inherit_unix_listener path, fd
123
+ logger.log "* Inherited #{str}"
115
124
  elsif sock = @activated_sockets.delete([ :unix, path ])
116
- logger.log "* Activated #{str}"
117
125
  io = inherit_unix_listener path, sock
126
+ logger.log "* Activated #{str}"
118
127
  else
119
- logger.log "* Listening on #{str}"
120
-
121
128
  umask = nil
122
129
  mode = nil
123
- backlog = nil
130
+ backlog = 1024
124
131
 
125
132
  if uri.query
126
133
  params = Util.parse_query uri.query
@@ -139,6 +146,7 @@ module Puma
139
146
  end
140
147
 
141
148
  io = add_unix_listener path, umask, mode, backlog
149
+ logger.log "* Listening on #{str}"
142
150
  end
143
151
 
144
152
  @listeners << [str, io]
@@ -162,6 +170,7 @@ module Puma
162
170
  end
163
171
 
164
172
  ctx.keystore_pass = params['keystore-pass']
173
+ ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
165
174
  else
166
175
  unless params['key']
167
176
  @events.error "Please specify the SSL key via 'key='"
@@ -182,8 +191,12 @@ module Puma
182
191
  end
183
192
 
184
193
  ctx.ca = params['ca'] if params['ca']
194
+ ctx.ssl_cipher_filter = params['ssl_cipher_filter'] if params['ssl_cipher_filter']
185
195
  end
186
196
 
197
+ ctx.no_tlsv1 = true if params['no_tlsv1'] == 'true'
198
+ ctx.no_tlsv1_1 = true if params['no_tlsv1_1'] == 'true'
199
+
187
200
  if params['verify_mode']
188
201
  ctx.verify_mode = case params['verify_mode']
189
202
  when "peer"
@@ -202,11 +215,11 @@ module Puma
202
215
  logger.log "* Inherited #{str}"
203
216
  io = inherit_ssl_listener fd, ctx
204
217
  elsif sock = @activated_sockets.delete([ :tcp, uri.host, uri.port ])
205
- logger.log "* Activated #{str}"
206
218
  io = inherit_ssl_listener sock, ctx
219
+ logger.log "* Activated #{str}"
207
220
  else
208
- logger.log "* Listening on #{str}"
209
221
  io = add_ssl_listener uri.host, uri.port, ctx
222
+ logger.log "* Listening on #{str}"
210
223
  end
211
224
 
212
225
  @listeners << [str, io] if io
@@ -245,9 +258,10 @@ module Puma
245
258
  end
246
259
  end
247
260
 
248
- def localhost_addresses
249
- addrs = TCPSocket.gethostbyname "localhost"
250
- addrs[3..-1].uniq
261
+ def loopback_addresses
262
+ Socket.ip_address_list.select do |addrinfo|
263
+ addrinfo.ipv6_loopback? || addrinfo.ipv4_loopback?
264
+ end.map { |addrinfo| addrinfo.ip_address }.uniq
251
265
  end
252
266
 
253
267
  # Tell the server to listen on host +host+, port +port+.
@@ -259,7 +273,7 @@ module Puma
259
273
  #
260
274
  def add_tcp_listener(host, port, optimize_for_latency=true, backlog=1024)
261
275
  if host == "localhost"
262
- localhost_addresses.each do |addr|
276
+ loopback_addresses.each do |addr|
263
277
  add_tcp_listener addr, port, optimize_for_latency, backlog
264
278
  end
265
279
  return
@@ -298,7 +312,7 @@ module Puma
298
312
  MiniSSL.check
299
313
 
300
314
  if host == "localhost"
301
- localhost_addresses.each do |addr|
315
+ loopback_addresses.each do |addr|
302
316
  add_ssl_listener addr, port, ctx, optimize_for_latency, backlog
303
317
  end
304
318
  return
@@ -312,6 +326,7 @@ module Puma
312
326
  s.setsockopt(Socket::SOL_SOCKET,Socket::SO_REUSEADDR, true)
313
327
  s.listen backlog
314
328
 
329
+
315
330
  ssl = MiniSSL::Server.new s, ctx
316
331
  env = @proto_env.dup
317
332
  env[HTTPS_KEY] = HTTPS
@@ -343,7 +358,7 @@ module Puma
343
358
 
344
359
  # Tell the server to listen on +path+ as a UNIX domain socket.
345
360
  #
346
- def add_unix_listener(path, umask=nil, mode=nil, backlog=nil)
361
+ def add_unix_listener(path, umask=nil, mode=nil, backlog=1024)
347
362
  @unix_paths << path
348
363
 
349
364
  # Let anyone connect by default
@@ -364,7 +379,7 @@ module Puma
364
379
  end
365
380
 
366
381
  s = UNIXServer.new(path)
367
- s.listen backlog if backlog
382
+ s.listen backlog
368
383
  @ios << s
369
384
  ensure
370
385
  File.umask old_mask
data/lib/puma/cli.rb CHANGED
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
  require 'uri'
3
5
 
6
+ require 'puma'
4
7
  require 'puma/configuration'
5
8
  require 'puma/launcher'
6
9
  require 'puma/const'
@@ -47,21 +50,21 @@ module Puma
47
50
  @parser.parse! @argv
48
51
 
49
52
  if file = @argv.shift
50
- @conf.configure do |c|
51
- c.rackup file
53
+ @conf.configure do |user_config, file_config|
54
+ file_config.rackup file
52
55
  end
53
56
  end
54
57
  rescue UnsupportedOption
55
58
  exit 1
56
59
  end
57
60
 
58
- @conf.configure do |c|
61
+ @conf.configure do |user_config, file_config|
59
62
  if @stdout || @stderr
60
- c.stdout_redirect @stdout, @stderr, @append
63
+ user_config.stdout_redirect @stdout, @stderr, @append
61
64
  end
62
65
 
63
66
  if @control_url
64
- c.activate_control_app @control_url, @control_options
67
+ user_config.activate_control_app @control_url, @control_options
65
68
  end
66
69
  end
67
70
 
@@ -83,27 +86,35 @@ module Puma
83
86
  raise UnsupportedOption
84
87
  end
85
88
 
89
+ def configure_control_url(command_line_arg)
90
+ if command_line_arg
91
+ @control_url = command_line_arg
92
+ elsif Puma.jruby?
93
+ unsupported "No default url available on JRuby"
94
+ end
95
+ end
96
+
86
97
  # Build the OptionParser object to handle the available options.
87
98
  #
88
99
 
89
100
  def setup_options
90
- @conf = Configuration.new do |c|
101
+ @conf = Configuration.new do |user_config, file_config|
91
102
  @parser = OptionParser.new do |o|
92
103
  o.on "-b", "--bind URI", "URI to bind to (tcp://, unix://, ssl://)" do |arg|
93
- c.bind arg
104
+ user_config.bind arg
94
105
  end
95
106
 
96
107
  o.on "-C", "--config PATH", "Load PATH as a config file" do |arg|
97
- c.load arg
108
+ file_config.load arg
98
109
  end
99
110
 
100
- o.on "--control URL", "The bind url to use for the control server",
101
- "Use 'auto' to use temp unix server" do |arg|
102
- if arg
103
- @control_url = arg
104
- elsif Puma.jruby?
105
- unsupported "No default url available on JRuby"
106
- end
111
+ o.on "--control-url URL", "The bind url to use for the control server. Use 'auto' to use temp unix server" do |arg|
112
+ configure_control_url(arg)
113
+ end
114
+
115
+ # alias --control-url for backwards-compatibility
116
+ o.on "--control URL", "DEPRECATED alias for --control-url" do |arg|
117
+ configure_control_url(arg)
107
118
  end
108
119
 
109
120
  o.on "--control-token TOKEN",
@@ -112,21 +123,21 @@ module Puma
112
123
  end
113
124
 
114
125
  o.on "-d", "--daemon", "Daemonize the server into the background" do
115
- c.daemonize
116
- c.quiet
126
+ user_config.daemonize
127
+ user_config.quiet
117
128
  end
118
129
 
119
130
  o.on "--debug", "Log lowlevel debugging information" do
120
- c.debug
131
+ user_config.debug
121
132
  end
122
133
 
123
134
  o.on "--dir DIR", "Change to DIR before starting" do |d|
124
- c.directory d
135
+ user_config.directory d
125
136
  end
126
137
 
127
138
  o.on "-e", "--environment ENVIRONMENT",
128
139
  "The environment to run the Rack app on (default development)" do |arg|
129
- c.environment arg
140
+ user_config.environment arg
130
141
  end
131
142
 
132
143
  o.on "-I", "--include PATH", "Specify $LOAD_PATH directories" do |arg|
@@ -135,50 +146,54 @@ module Puma
135
146
 
136
147
  o.on "-p", "--port PORT", "Define the TCP port to bind to",
137
148
  "Use -b for more advanced options" do |arg|
138
- c.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
149
+ user_config.bind "tcp://#{Configuration::DefaultTCPHost}:#{arg}"
139
150
  end
140
151
 
141
152
  o.on "--pidfile PATH", "Use PATH as a pidfile" do |arg|
142
- c.pidfile arg
153
+ user_config.pidfile arg
143
154
  end
144
155
 
145
156
  o.on "--preload", "Preload the app. Cluster mode only" do
146
- c.preload_app!
157
+ user_config.preload_app!
147
158
  end
148
159
 
149
160
  o.on "--prune-bundler", "Prune out the bundler env if possible" do
150
- c.prune_bundler
161
+ user_config.prune_bundler
151
162
  end
152
163
 
153
164
  o.on "-q", "--quiet", "Do not log requests internally (default true)" do
154
- c.quiet
165
+ user_config.quiet
155
166
  end
156
167
 
157
168
  o.on "-v", "--log-requests", "Log requests as they occur" do
158
- c.log_requests
169
+ user_config.log_requests
159
170
  end
160
171
 
161
172
  o.on "-R", "--restart-cmd CMD",
162
173
  "The puma command to run during a hot restart",
163
174
  "Default: inferred" do |cmd|
164
- c.restart_command cmd
175
+ user_config.restart_command cmd
165
176
  end
166
177
 
167
178
  o.on "-S", "--state PATH", "Where to store the state details" do |arg|
168
- c.state_path arg
179
+ user_config.state_path arg
169
180
  end
170
181
 
171
182
  o.on '-t', '--threads INT', "min:max threads to use (default 0:16)" do |arg|
172
183
  min, max = arg.split(":")
173
184
  if max
174
- c.threads min, max
185
+ user_config.threads min, max
175
186
  else
176
- c.threads min, min
187
+ user_config.threads min, min
177
188
  end
178
189
  end
179
190
 
180
191
  o.on "--tcp-mode", "Run the app in raw TCP mode instead of HTTP mode" do
181
- c.tcp_mode!
192
+ user_config.tcp_mode!
193
+ end
194
+
195
+ o.on "--early-hints", "Enable early hints support" do
196
+ user_config.early_hints
182
197
  end
183
198
 
184
199
  o.on "-V", "--version", "Print the version information" do
@@ -188,11 +203,11 @@ module Puma
188
203
 
189
204
  o.on "-w", "--workers COUNT",
190
205
  "Activate cluster mode: How many worker processes to create" do |arg|
191
- c.workers arg
206
+ user_config.workers arg
192
207
  end
193
208
 
194
209
  o.on "--tag NAME", "Additional text to display in process listing" do |arg|
195
- c.tag arg
210
+ user_config.tag arg
196
211
  end
197
212
 
198
213
  o.on "--redirect-stdout FILE", "Redirect STDOUT to a specific file" do |arg|
data/lib/puma/client.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class IO
2
4
  # We need to use this for a jruby work around on both 1.8 and 1.9.
3
5
  # So this either creates the constant (on 1.8), or harmlessly
@@ -21,6 +23,18 @@ module Puma
21
23
 
22
24
  class ConnectionError < RuntimeError; end
23
25
 
26
+ # An instance of this class represents a unique request from a client.
27
+ # For example a web request from a browser or from CURL. This
28
+ #
29
+ # An instance of `Puma::Client` can be used as if it were an IO object
30
+ # by the reactor, that's because the latter is expected to call `#to_io`
31
+ # on any non-IO objects it polls. For example nio4r internally calls
32
+ # `IO::try_convert` (which may call `#to_io`) when a new socket is
33
+ # registered.
34
+ #
35
+ # Instances of this class are responsible for knowing if
36
+ # the header and body are fully buffered via the `try_to_finish` method.
37
+ # They can be used to "time out" a response via the `timeout_at` reader.
24
38
  class Client
25
39
  include Puma::Const
26
40
  extend Puma::Delegation
@@ -41,6 +55,7 @@ module Puma
41
55
  @ready = false
42
56
 
43
57
  @body = nil
58
+ @body_read_start = nil
44
59
  @buffer = nil
45
60
  @tempfile = nil
46
61
 
@@ -51,6 +66,10 @@ module Puma
51
66
 
52
67
  @peerip = nil
53
68
  @remote_addr_header = nil
69
+
70
+ @body_remain = 0
71
+
72
+ @in_last_chunk = false
54
73
  end
55
74
 
56
75
  attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
@@ -89,6 +108,9 @@ module Puma
89
108
  @tempfile = nil
90
109
  @parsed_bytes = 0
91
110
  @ready = false
111
+ @body_remain = 0
112
+ @peerip = nil
113
+ @in_last_chunk = false
92
114
 
93
115
  if @buffer
94
116
  @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
@@ -101,9 +123,16 @@ module Puma
101
123
  end
102
124
 
103
125
  return false
104
- elsif fast_check &&
105
- IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
106
- return try_to_finish
126
+ else
127
+ begin
128
+ if fast_check &&
129
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
130
+ return try_to_finish
131
+ end
132
+ rescue IOError
133
+ # swallow it
134
+ end
135
+
107
136
  end
108
137
  end
109
138
 
@@ -111,6 +140,7 @@ module Puma
111
140
  begin
112
141
  @io.close
113
142
  rescue IOError
143
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
114
144
  end
115
145
  end
116
146
 
@@ -133,10 +163,13 @@ module Puma
133
163
  def decode_chunk(chunk)
134
164
  if @partial_part_left > 0
135
165
  if @partial_part_left <= chunk.size
136
- @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
166
+ if @partial_part_left > 2
167
+ @body << chunk[0..(@partial_part_left-3)] # skip the \r\n
168
+ end
137
169
  chunk = chunk[@partial_part_left..-1]
170
+ @partial_part_left = 0
138
171
  else
139
- @body << chunk
172
+ @body << chunk if @partial_part_left > 2 # don't include the last \r\n
140
173
  @partial_part_left -= chunk.size
141
174
  return false
142
175
  end
@@ -154,12 +187,20 @@ module Puma
154
187
  if line.end_with?("\r\n")
155
188
  len = line.strip.to_i(16)
156
189
  if len == 0
190
+ @in_last_chunk = true
157
191
  @body.rewind
158
192
  rest = io.read
159
- @buffer = rest.empty? ? nil : rest
160
- @requests_served += 1
161
- @ready = true
162
- return true
193
+ last_crlf_size = "\r\n".bytesize
194
+ if rest.bytesize < last_crlf_size
195
+ @buffer = nil
196
+ @partial_part_left = last_crlf_size - rest.bytesize
197
+ return false
198
+ else
199
+ @buffer = rest[last_crlf_size..-1]
200
+ @buffer = nil if @buffer.empty?
201
+ set_ready
202
+ return true
203
+ end
163
204
  end
164
205
 
165
206
  len += 2
@@ -189,14 +230,19 @@ module Puma
189
230
  end
190
231
  end
191
232
 
192
- return false
233
+ if @in_last_chunk
234
+ set_ready
235
+ true
236
+ else
237
+ false
238
+ end
193
239
  end
194
240
 
195
241
  def read_chunked_body
196
242
  while true
197
243
  begin
198
244
  chunk = @io.read_nonblock(4096)
199
- rescue Errno::EAGAIN
245
+ rescue IO::WaitReadable
200
246
  return false
201
247
  rescue SystemCallError, IOError
202
248
  raise ConnectionError, "Connection error detected during read"
@@ -206,8 +252,7 @@ module Puma
206
252
  unless chunk
207
253
  @body.close
208
254
  @buffer = nil
209
- @requests_served += 1
210
- @ready = true
255
+ set_ready
211
256
  raise EOFError
212
257
  end
213
258
 
@@ -216,6 +261,15 @@ module Puma
216
261
  end
217
262
 
218
263
  def setup_body
264
+ @body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
265
+
266
+ if @env[HTTP_EXPECT] == CONTINUE
267
+ # TODO allow a hook here to check the headers before
268
+ # going forward
269
+ @io << HTTP_11_100
270
+ @io.flush
271
+ end
272
+
219
273
  @read_header = false
220
274
 
221
275
  body = @parser.body
@@ -233,8 +287,7 @@ module Puma
233
287
  unless cl
234
288
  @buffer = body.empty? ? nil : body
235
289
  @body = EmptyBody
236
- @requests_served += 1
237
- @ready = true
290
+ set_ready
238
291
  return true
239
292
  end
240
293
 
@@ -243,8 +296,7 @@ module Puma
243
296
  if remain <= 0
244
297
  @body = StringIO.new(body)
245
298
  @buffer = nil
246
- @requests_served += 1
247
- @ready = true
299
+ set_ready
248
300
  return true
249
301
  end
250
302
 
@@ -272,10 +324,17 @@ module Puma
272
324
  data = @io.read_nonblock(CHUNK_SIZE)
273
325
  rescue Errno::EAGAIN
274
326
  return false
275
- rescue SystemCallError, IOError
327
+ rescue SystemCallError, IOError, EOFError
276
328
  raise ConnectionError, "Connection error detected during read"
277
329
  end
278
330
 
331
+ # No data means a closed socket
332
+ unless data
333
+ @buffer = nil
334
+ set_ready
335
+ raise EOFError
336
+ end
337
+
279
338
  if @buffer
280
339
  @buffer << data
281
340
  else
@@ -305,6 +364,13 @@ module Puma
305
364
  raise e
306
365
  end
307
366
 
367
+ # No data means a closed socket
368
+ unless data
369
+ @buffer = nil
370
+ set_ready
371
+ raise EOFError
372
+ end
373
+
308
374
  if @buffer
309
375
  @buffer << data
310
376
  else
@@ -378,8 +444,7 @@ module Puma
378
444
  unless chunk
379
445
  @body.close
380
446
  @buffer = nil
381
- @requests_served += 1
382
- @ready = true
447
+ set_ready
383
448
  raise EOFError
384
449
  end
385
450
 
@@ -388,8 +453,7 @@ module Puma
388
453
  if remain <= 0
389
454
  @body.rewind
390
455
  @buffer = nil
391
- @requests_served += 1
392
- @ready = true
456
+ set_ready
393
457
  return true
394
458
  end
395
459
 
@@ -398,6 +462,14 @@ module Puma
398
462
  false
399
463
  end
400
464
 
465
+ def set_ready
466
+ if @body_read_start
467
+ @env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - @body_read_start
468
+ end
469
+ @requests_served += 1
470
+ @ready = true
471
+ end
472
+
401
473
  def write_400
402
474
  begin
403
475
  @io << ERROR_400_RESPONSE