puma 2.2.0 → 8.0.2
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.
- checksums.yaml +7 -0
- data/History.md +3334 -0
- data/LICENSE +23 -20
- data/README.md +387 -100
- data/bin/puma-wild +25 -0
- data/bin/pumactl +1 -1
- data/docs/5.0-Upgrade.md +98 -0
- data/docs/6.0-Upgrade.md +56 -0
- data/docs/7.0-Upgrade.md +52 -0
- data/docs/8.0-Upgrade.md +100 -0
- data/docs/architecture.md +74 -0
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +137 -0
- data/docs/fork_worker.md +41 -0
- data/docs/grpc.md +62 -0
- data/docs/images/favicon.svg +1 -0
- data/docs/images/puma-connection-flow-no-reactor.png +0 -0
- data/docs/images/puma-connection-flow.png +0 -0
- data/docs/images/puma-general-arch.png +0 -0
- data/docs/images/running-puma.svg +1 -0
- data/docs/images/standard-logo.svg +1 -0
- data/docs/java_options.md +54 -0
- data/docs/jungle/README.md +9 -0
- data/docs/jungle/rc.d/README.md +74 -0
- data/docs/jungle/rc.d/puma +61 -0
- data/docs/jungle/rc.d/puma.conf +10 -0
- data/docs/kubernetes.md +73 -0
- data/docs/nginx.md +5 -5
- data/docs/plugins.md +42 -0
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +65 -0
- data/docs/signals.md +98 -0
- data/docs/stats.md +148 -0
- data/docs/systemd.md +253 -0
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -2
- data/ext/puma_http11/extconf.rb +59 -2
- data/ext/puma_http11/http11_parser.c +319 -487
- data/ext/puma_http11/http11_parser.h +15 -13
- data/ext/puma_http11/http11_parser.java.rl +64 -94
- data/ext/puma_http11/http11_parser.rl +27 -24
- data/ext/puma_http11/http11_parser_common.rl +8 -8
- data/ext/puma_http11/mini_ssl.c +696 -38
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/EnvKey.java +241 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +241 -145
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +174 -221
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +413 -193
- data/ext/puma_http11/puma_http11.c +183 -175
- data/lib/puma/app/status.rb +77 -29
- data/lib/puma/binder.rb +349 -119
- data/lib/puma/cli.rb +163 -801
- data/lib/puma/client.rb +627 -144
- data/lib/puma/client_env.rb +171 -0
- data/lib/puma/cluster/worker.rb +183 -0
- data/lib/puma/cluster/worker_handle.rb +127 -0
- data/lib/puma/cluster.rb +634 -0
- data/lib/puma/cluster_accept_loop_delay.rb +91 -0
- data/lib/puma/commonlogger.rb +115 -0
- data/lib/puma/configuration.rb +420 -198
- data/lib/puma/const.rb +266 -115
- data/lib/puma/control_cli.rb +219 -120
- data/lib/puma/detect.rb +56 -2
- data/lib/puma/dsl.rb +1562 -0
- data/lib/puma/error_logger.rb +115 -0
- data/lib/puma/events.rb +44 -60
- data/lib/puma/io_buffer.rb +48 -5
- data/lib/puma/jruby_restart.rb +2 -51
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +102 -0
- data/lib/puma/launcher.rb +501 -0
- data/lib/puma/log_writer.rb +153 -0
- data/lib/puma/minissl/context_builder.rb +96 -0
- data/lib/puma/minissl.rb +355 -37
- data/lib/puma/null_io.rb +79 -12
- data/lib/puma/plugin/systemd.rb +90 -0
- data/lib/puma/plugin/tmp_restart.rb +36 -0
- data/lib/puma/plugin.rb +111 -0
- data/lib/puma/rack/builder.rb +297 -0
- data/lib/puma/rack/urlmap.rb +93 -0
- data/lib/puma/rack_default.rb +21 -4
- data/lib/puma/reactor.rb +102 -133
- data/lib/puma/response.rb +532 -0
- data/lib/puma/runner.rb +211 -0
- data/lib/puma/sd_notify.rb +146 -0
- data/lib/puma/server.rb +582 -445
- data/lib/puma/server_plugin_control.rb +32 -0
- data/lib/puma/single.rb +72 -0
- data/lib/puma/state_file.rb +69 -0
- data/lib/puma/thread_pool.rb +389 -57
- data/lib/puma/util.rb +125 -0
- data/lib/puma.rb +80 -6
- data/lib/rack/handler/puma.rb +125 -47
- data/tools/Dockerfile +26 -0
- data/tools/trickletest.rb +44 -0
- metadata +88 -148
- data/COPYING +0 -55
- data/Gemfile +0 -10
- data/History.txt +0 -302
- data/Manifest.txt +0 -60
- data/Rakefile +0 -165
- data/TODO +0 -5
- data/docs/config.md +0 -0
- data/ext/puma_http11/ext_help.h +0 -15
- data/ext/puma_http11/io_buffer.c +0 -154
- data/lib/puma/accept_nonblock.rb +0 -23
- data/lib/puma/capistrano.rb +0 -34
- data/lib/puma/compat.rb +0 -11
- data/lib/puma/daemon_ext.rb +0 -20
- data/lib/puma/delegation.rb +0 -11
- data/lib/puma/java_io_buffer.rb +0 -45
- data/lib/puma/rack_patch.rb +0 -25
- data/puma.gemspec +0 -45
- data/test/test_app_status.rb +0 -88
- data/test/test_cli.rb +0 -171
- data/test/test_config.rb +0 -16
- data/test/test_http10.rb +0 -27
- data/test/test_http11.rb +0 -126
- data/test/test_integration.rb +0 -154
- data/test/test_iobuffer.rb +0 -38
- data/test/test_minissl.rb +0 -25
- data/test/test_null_io.rb +0 -31
- data/test/test_persistent.rb +0 -238
- data/test/test_puma_server.rb +0 -224
- data/test/test_rack_handler.rb +0 -10
- data/test/test_rack_server.rb +0 -141
- data/test/test_thread_pool.rb +0 -146
- data/test/test_unix_socket.rb +0 -39
- data/test/test_ws.rb +0 -89
- data/tools/jungle/init.d/README.md +0 -54
- data/tools/jungle/init.d/puma +0 -332
- data/tools/jungle/init.d/run-puma +0 -3
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -52
data/lib/puma/client.rb
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
# We need to use this for a jruby work around on both 1.8 and 1.9.
|
|
3
|
-
# So this either creates the constant (on 1.8), or harmlessly
|
|
4
|
-
# reopens it (on 1.9).
|
|
5
|
-
module WaitReadable
|
|
6
|
-
end
|
|
7
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
8
2
|
|
|
9
|
-
|
|
3
|
+
require_relative 'detect'
|
|
4
|
+
require_relative 'io_buffer'
|
|
5
|
+
require_relative 'client_env'
|
|
6
|
+
require 'tempfile'
|
|
10
7
|
|
|
11
8
|
if Puma::IS_JRUBY
|
|
12
9
|
# We have to work around some OpenSSL buffer/io-readiness bugs
|
|
@@ -19,255 +16,741 @@ module Puma
|
|
|
19
16
|
|
|
20
17
|
class ConnectionError < RuntimeError; end
|
|
21
18
|
|
|
22
|
-
class
|
|
23
|
-
|
|
19
|
+
class HttpParserError501 < IOError; end
|
|
20
|
+
|
|
21
|
+
#———————————————————————— DO NOT USE — this class is for internal use only ———
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# An instance of this class wraps a connection/socket.
|
|
25
|
+
# For example, this could be an http request from a browser or from CURL.
|
|
26
|
+
#
|
|
27
|
+
# An instance of `Puma::Client` can be used as if it were an IO object
|
|
28
|
+
# by the reactor. The reactor is expected to call `#to_io`
|
|
29
|
+
# on any non-IO objects it polls. For example, nio4r internally calls
|
|
30
|
+
# `IO::try_convert` (which may call `#to_io`) when a new socket is
|
|
31
|
+
# registered.
|
|
32
|
+
#
|
|
33
|
+
# Instances of this class are responsible for knowing if the request line,
|
|
34
|
+
# headers and body are fully buffered and verified via the `try_to_finish` method.
|
|
35
|
+
# All verification of each request is done in the `Client` object.
|
|
36
|
+
# They can be used to "time out" a response via the `timeout_at` reader.
|
|
37
|
+
#
|
|
38
|
+
# Most of the code for env processing and verification is contained
|
|
39
|
+
# in `Puma::ClientEnv`, which is included.
|
|
40
|
+
#
|
|
41
|
+
class Client # :nodoc:
|
|
42
|
+
|
|
43
|
+
include ClientEnv
|
|
44
|
+
|
|
45
|
+
# this tests all values but the last, which must be chunked
|
|
46
|
+
ALLOWED_TRANSFER_ENCODING = %w[compress deflate gzip].freeze
|
|
47
|
+
|
|
48
|
+
# chunked body validation
|
|
49
|
+
CHUNK_SIZE_INVALID = /[^\h]/.freeze
|
|
50
|
+
CHUNK_VALID_ENDING = Const::LINE_END
|
|
51
|
+
CHUNK_VALID_ENDING_SIZE = CHUNK_VALID_ENDING.bytesize
|
|
52
|
+
|
|
53
|
+
# The maximum number of bytes we'll buffer looking for a valid
|
|
54
|
+
# chunk header.
|
|
55
|
+
MAX_CHUNK_HEADER_SIZE = 4096
|
|
56
|
+
|
|
57
|
+
# The maximum amount of excess data the client sends
|
|
58
|
+
# using chunk size extensions before we abort the connection.
|
|
59
|
+
MAX_CHUNK_EXCESS = 16 * 1024
|
|
60
|
+
|
|
61
|
+
# Content-Length header value validation
|
|
62
|
+
CONTENT_LENGTH_VALUE_INVALID = /[^\d]/.freeze
|
|
63
|
+
|
|
64
|
+
TE_ERR_MSG = 'Invalid Transfer-Encoding'
|
|
65
|
+
|
|
66
|
+
# See:
|
|
67
|
+
# https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.1.1
|
|
68
|
+
# https://httpwg.org/specs/rfc9112.html#rfc.section.6.1
|
|
69
|
+
STRIP_OWS = /\A[ \t]+|[ \t]+\z/
|
|
24
70
|
|
|
25
|
-
|
|
71
|
+
# The object used for a request with no body. All requests with
|
|
72
|
+
# no body share this one object since it has no state.
|
|
73
|
+
EmptyBody = NullIO.new
|
|
74
|
+
|
|
75
|
+
attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
|
|
76
|
+
:tempfile, :io_buffer, :http_content_length_limit_exceeded,
|
|
77
|
+
:requests_served, :error_status_code
|
|
78
|
+
|
|
79
|
+
attr_writer :peerip, :http_content_length_limit, :supported_http_methods
|
|
80
|
+
|
|
81
|
+
attr_accessor :remote_addr_header, :listener, :env_set_http_version
|
|
82
|
+
|
|
83
|
+
def initialize(io, env=nil)
|
|
26
84
|
@io = io
|
|
27
85
|
@to_io = io.to_io
|
|
86
|
+
@io_buffer = IOBuffer.new
|
|
28
87
|
@proto_env = env
|
|
29
|
-
@env = env
|
|
88
|
+
@env = env&.dup
|
|
30
89
|
|
|
31
90
|
@parser = HttpParser.new
|
|
32
91
|
@parsed_bytes = 0
|
|
33
92
|
@read_header = true
|
|
93
|
+
@read_proxy = false
|
|
34
94
|
@ready = false
|
|
35
95
|
|
|
36
96
|
@body = nil
|
|
97
|
+
@body_read_start = nil
|
|
37
98
|
@buffer = nil
|
|
99
|
+
@tempfile = nil
|
|
38
100
|
|
|
39
101
|
@timeout_at = nil
|
|
40
102
|
|
|
41
103
|
@requests_served = 0
|
|
42
104
|
@hijacked = false
|
|
105
|
+
|
|
106
|
+
@http_content_length_limit = nil
|
|
107
|
+
@http_content_length_limit_exceeded = nil
|
|
108
|
+
@error_status_code = nil
|
|
109
|
+
|
|
110
|
+
@peerip = nil
|
|
111
|
+
@peer_family = nil
|
|
112
|
+
@listener = nil
|
|
113
|
+
@remote_addr_header = nil
|
|
114
|
+
@expect_proxy_proto = false
|
|
115
|
+
|
|
116
|
+
@body_remain = 0
|
|
117
|
+
|
|
118
|
+
@in_last_chunk = false
|
|
119
|
+
|
|
120
|
+
# need unfrozen ASCII-8BIT, +'' is UTF-8
|
|
121
|
+
@read_buffer = String.new # rubocop: disable Performance/UnfreezeString
|
|
43
122
|
end
|
|
44
123
|
|
|
45
|
-
|
|
124
|
+
# Remove in Puma 7?
|
|
125
|
+
def closed?
|
|
126
|
+
@to_io.closed?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Test to see if io meets a bare minimum of functioning, @to_io needs to be
|
|
130
|
+
# used for MiniSSL::Socket
|
|
131
|
+
def io_ok?
|
|
132
|
+
@to_io.is_a?(::BasicSocket) && !closed?
|
|
133
|
+
end
|
|
46
134
|
|
|
135
|
+
# @!attribute [r] inspect
|
|
47
136
|
def inspect
|
|
48
137
|
"#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
|
|
49
138
|
end
|
|
50
139
|
|
|
51
|
-
# For the hijack protocol
|
|
52
|
-
#
|
|
53
|
-
def
|
|
140
|
+
# For the full hijack protocol, `env['rack.hijack']` is set to
|
|
141
|
+
# `client.method :full_hijack`
|
|
142
|
+
def full_hijack
|
|
54
143
|
@hijacked = true
|
|
55
144
|
env[HIJACK_IO] ||= @io
|
|
56
145
|
end
|
|
57
146
|
|
|
147
|
+
# @!attribute [r] in_data_phase
|
|
148
|
+
def in_data_phase
|
|
149
|
+
!(@read_header || @read_proxy)
|
|
150
|
+
end
|
|
151
|
+
|
|
58
152
|
def set_timeout(val)
|
|
59
|
-
@timeout_at =
|
|
153
|
+
@timeout_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + val
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Number of seconds until the timeout elapses.
|
|
157
|
+
# @!attribute [r] timeout
|
|
158
|
+
def timeout
|
|
159
|
+
[@timeout_at - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
60
160
|
end
|
|
61
161
|
|
|
62
|
-
def reset
|
|
162
|
+
def reset
|
|
63
163
|
@parser.reset
|
|
164
|
+
@io_buffer.reset
|
|
64
165
|
@read_header = true
|
|
166
|
+
@read_proxy = !!@expect_proxy_proto && @requests_served.zero?
|
|
65
167
|
@env = @proto_env.dup
|
|
66
|
-
@body = nil
|
|
67
168
|
@parsed_bytes = 0
|
|
68
169
|
@ready = false
|
|
170
|
+
@body_remain = 0
|
|
171
|
+
@peerip = nil if @remote_addr_header
|
|
172
|
+
@in_last_chunk = false
|
|
173
|
+
@http_content_length_limit_exceeded = nil
|
|
174
|
+
@error_status_code = nil
|
|
175
|
+
end
|
|
69
176
|
|
|
177
|
+
# only used with back-to-back requests contained in the buffer
|
|
178
|
+
def process_back_to_back_requests
|
|
70
179
|
if @buffer
|
|
71
|
-
|
|
180
|
+
return false unless try_to_parse_proxy_protocol
|
|
72
181
|
|
|
73
|
-
|
|
74
|
-
return setup_body
|
|
75
|
-
elsif @parsed_bytes >= MAX_HEADER
|
|
76
|
-
raise HttpParserError,
|
|
77
|
-
"HEADER is longer than allowed, aborting client early."
|
|
78
|
-
end
|
|
182
|
+
@parsed_bytes = parser_execute
|
|
79
183
|
|
|
80
|
-
|
|
81
|
-
elsif fast_check &&
|
|
82
|
-
IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
|
|
83
|
-
return try_to_finish
|
|
184
|
+
@parser.finished? ? process_env_body : false
|
|
84
185
|
end
|
|
85
186
|
end
|
|
86
187
|
|
|
188
|
+
# if a client sends back-to-back requests, the buffer may contain one or more
|
|
189
|
+
# of them.
|
|
190
|
+
def has_back_to_back_requests?
|
|
191
|
+
!(@buffer.nil? || @buffer.empty?)
|
|
192
|
+
end
|
|
193
|
+
|
|
87
194
|
def close
|
|
195
|
+
tempfile_close
|
|
88
196
|
begin
|
|
89
197
|
@io.close
|
|
90
|
-
rescue IOError
|
|
198
|
+
rescue IOError, Errno::EBADF
|
|
91
199
|
end
|
|
92
200
|
end
|
|
93
201
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
body =
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
unless cl
|
|
103
|
-
@buffer = body.empty? ? nil : body
|
|
104
|
-
@body = EmptyBody
|
|
105
|
-
@requests_served += 1
|
|
106
|
-
@ready = true
|
|
107
|
-
return true
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
remain = cl.to_i - body.bytesize
|
|
111
|
-
|
|
112
|
-
if remain <= 0
|
|
113
|
-
@body = StringIO.new(body)
|
|
114
|
-
@buffer = nil
|
|
115
|
-
@requests_served += 1
|
|
116
|
-
@ready = true
|
|
117
|
-
return true
|
|
118
|
-
end
|
|
202
|
+
def tempfile_close
|
|
203
|
+
tf_path = @tempfile&.path
|
|
204
|
+
@tempfile&.close
|
|
205
|
+
File.unlink(tf_path) if tf_path
|
|
206
|
+
@tempfile = nil
|
|
207
|
+
@body = nil
|
|
208
|
+
rescue Errno::ENOENT, IOError
|
|
209
|
+
end
|
|
119
210
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
211
|
+
# If necessary, read the PROXY protocol from the buffer. Returns
|
|
212
|
+
# false if more data is needed.
|
|
213
|
+
def try_to_parse_proxy_protocol
|
|
214
|
+
if @read_proxy
|
|
215
|
+
if @expect_proxy_proto == :v1
|
|
216
|
+
crlf_index = @buffer.index "\r\n"
|
|
217
|
+
|
|
218
|
+
unless crlf_index
|
|
219
|
+
if "PROXY ".start_with? @buffer
|
|
220
|
+
return false
|
|
221
|
+
elsif @buffer.start_with? "PROXY "
|
|
222
|
+
if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
223
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
224
|
+
end
|
|
225
|
+
return false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@read_proxy = false
|
|
229
|
+
return true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
|
|
233
|
+
raise ConnectionError, "PROXY protocol v1 line is too long"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
|
|
237
|
+
if md[1]
|
|
238
|
+
@peerip = md[1].split(" ")[0]
|
|
239
|
+
end
|
|
240
|
+
@buffer = md.post_match
|
|
241
|
+
end
|
|
242
|
+
# if the buffer has a \r\n but doesn't have a PROXY protocol
|
|
243
|
+
# request, this is just HTTP from a non-PROXY client; move on
|
|
244
|
+
@read_proxy = false
|
|
245
|
+
return @buffer.size > 0
|
|
246
|
+
end
|
|
127
247
|
end
|
|
128
|
-
|
|
129
|
-
@body.write body
|
|
130
|
-
|
|
131
|
-
@body_remain = remain
|
|
132
|
-
|
|
133
|
-
@read_header = false
|
|
134
|
-
|
|
135
|
-
return false
|
|
248
|
+
true
|
|
136
249
|
end
|
|
137
250
|
|
|
138
251
|
def try_to_finish
|
|
139
|
-
return read_body
|
|
252
|
+
return read_body if in_data_phase
|
|
140
253
|
|
|
254
|
+
data = nil
|
|
141
255
|
begin
|
|
142
256
|
data = @io.read_nonblock(CHUNK_SIZE)
|
|
143
|
-
rescue
|
|
257
|
+
rescue IO::WaitReadable
|
|
144
258
|
return false
|
|
259
|
+
rescue EOFError
|
|
260
|
+
# Swallow error, don't log
|
|
145
261
|
rescue SystemCallError, IOError
|
|
146
262
|
raise ConnectionError, "Connection error detected during read"
|
|
147
263
|
end
|
|
148
264
|
|
|
265
|
+
# No data means a closed socket
|
|
266
|
+
unless data
|
|
267
|
+
@buffer = nil
|
|
268
|
+
set_ready
|
|
269
|
+
raise EOFError
|
|
270
|
+
end
|
|
271
|
+
|
|
149
272
|
if @buffer
|
|
150
273
|
@buffer << data
|
|
151
274
|
else
|
|
152
275
|
@buffer = data
|
|
153
276
|
end
|
|
154
277
|
|
|
155
|
-
|
|
278
|
+
return false unless try_to_parse_proxy_protocol
|
|
279
|
+
|
|
280
|
+
@parsed_bytes = parser_execute
|
|
281
|
+
|
|
282
|
+
@parser.finished? ? process_env_body : false
|
|
283
|
+
end
|
|
156
284
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"HEADER is longer than allowed, aborting client early."
|
|
285
|
+
def eagerly_finish
|
|
286
|
+
return true if @ready
|
|
287
|
+
while @to_io.wait_readable(0) # rubocop: disable Style/WhileUntilModifier
|
|
288
|
+
return true if try_to_finish
|
|
162
289
|
end
|
|
163
|
-
|
|
164
290
|
false
|
|
165
291
|
end
|
|
166
292
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
293
|
+
def finish(timeout)
|
|
294
|
+
return if @ready
|
|
295
|
+
@to_io.wait_readable(timeout) || timeout! until try_to_finish
|
|
296
|
+
end
|
|
170
297
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
end
|
|
298
|
+
# Wraps `@parser.execute` and adds meaningful error messages
|
|
299
|
+
# @return [Integer] bytes of buffer read by parser
|
|
300
|
+
#
|
|
301
|
+
def parser_execute
|
|
302
|
+
ret = @parser.execute(@env, @buffer, @parsed_bytes)
|
|
177
303
|
|
|
178
|
-
|
|
179
|
-
|
|
304
|
+
if @env[REQUEST_METHOD] && @supported_http_methods != :any && !@supported_http_methods.key?(@env[REQUEST_METHOD])
|
|
305
|
+
raise HttpParserError501, "#{@env[REQUEST_METHOD]} method is not supported"
|
|
306
|
+
end
|
|
307
|
+
ret
|
|
308
|
+
rescue => e
|
|
309
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
310
|
+
raise e unless HttpParserError === e && e.message.include?('non-SSL')
|
|
311
|
+
|
|
312
|
+
req, _ = @buffer.split "\r\n\r\n"
|
|
313
|
+
request_line, headers = req.split "\r\n", 2
|
|
314
|
+
|
|
315
|
+
# below checks for request issues and changes error message accordingly
|
|
316
|
+
if !@env.key? REQUEST_METHOD
|
|
317
|
+
if request_line.count(' ') != 2
|
|
318
|
+
# maybe this is an SSL connection ?
|
|
319
|
+
raise e
|
|
180
320
|
else
|
|
181
|
-
|
|
321
|
+
method = request_line[/\A[^ ]+/]
|
|
322
|
+
raise e, "Invalid HTTP format, parsing fails. Bad method #{method}"
|
|
182
323
|
end
|
|
324
|
+
elsif !@env.key? REQUEST_PATH
|
|
325
|
+
path = request_line[/\A[^ ]+ +([^ ?\r\n]+)/, 1]
|
|
326
|
+
raise e, "Invalid HTTP format, parsing fails. Bad path #{path}"
|
|
327
|
+
elsif request_line.match?(/\A[^ ]+ +[^ ?\r\n]+\?/) && !@env.key?(QUERY_STRING)
|
|
328
|
+
query = request_line[/\A[^ ]+ +[^? ]+\?([^ ]+)/, 1]
|
|
329
|
+
raise e, "Invalid HTTP format, parsing fails. Bad query #{query}"
|
|
330
|
+
elsif !@env.key? SERVER_PROTOCOL
|
|
331
|
+
# protocol is bad
|
|
332
|
+
text = request_line[/[^ ]*\z/]
|
|
333
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad protocol #{text}"
|
|
334
|
+
elsif !headers.empty?
|
|
335
|
+
# headers are bad
|
|
336
|
+
hdrs = headers.split("\r\n").map { |h| h.gsub "\n", '\n'}.join "\n"
|
|
337
|
+
raise HttpParserError, "Invalid HTTP format, parsing fails. Bad headers\n#{hdrs}"
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# processes the `env` and the request body
|
|
342
|
+
def process_env_body
|
|
343
|
+
temp = setup_body
|
|
344
|
+
normalize_env
|
|
345
|
+
req_env_post_parse
|
|
346
|
+
raise HttpParserError if @error_status_code
|
|
347
|
+
temp
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def timeout!
|
|
351
|
+
write_error(408) if in_data_phase
|
|
352
|
+
raise ConnectionError
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def write_error(status_code)
|
|
356
|
+
begin
|
|
357
|
+
@io << ERROR_RESPONSE[status_code]
|
|
358
|
+
rescue StandardError
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def peerip
|
|
363
|
+
return @peerip if @peerip
|
|
364
|
+
|
|
365
|
+
if @remote_addr_header
|
|
366
|
+
hdr = (@env[@remote_addr_header] || socket_peerip).split(/[\s,]/).first
|
|
367
|
+
@peerip = hdr
|
|
368
|
+
return hdr
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
@peerip ||= socket_peerip
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def peer_family
|
|
375
|
+
return @peer_family if @peer_family
|
|
376
|
+
|
|
377
|
+
@peer_family ||= begin
|
|
378
|
+
@io.local_address.afamily
|
|
379
|
+
rescue
|
|
380
|
+
Socket::AF_INET
|
|
381
|
+
end
|
|
382
|
+
end
|
|
183
383
|
|
|
184
|
-
|
|
384
|
+
# Returns true if the persistent connection can be closed immediately
|
|
385
|
+
# without waiting for the configured idle/shutdown timeout.
|
|
386
|
+
# @version 5.0.0
|
|
387
|
+
#
|
|
388
|
+
def can_close?
|
|
389
|
+
# Allow connection to close if we're not in the middle of parsing a request.
|
|
390
|
+
@parsed_bytes == 0
|
|
391
|
+
end
|
|
185
392
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"HEADER is longer than allowed, aborting client early."
|
|
393
|
+
def expect_proxy_proto=(val)
|
|
394
|
+
if val
|
|
395
|
+
if @read_header
|
|
396
|
+
@read_proxy = true
|
|
191
397
|
end
|
|
398
|
+
else
|
|
399
|
+
@read_proxy = false
|
|
400
|
+
end
|
|
401
|
+
@expect_proxy_proto = val
|
|
402
|
+
end
|
|
192
403
|
|
|
193
|
-
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
IPV4_MAPPED_IPV6_PREFIX = "::ffff:"
|
|
407
|
+
private_constant :IPV4_MAPPED_IPV6_PREFIX
|
|
408
|
+
|
|
409
|
+
def socket_peerip
|
|
410
|
+
unmap_ipv6(@io.peeraddr.last)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Converts IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) back to
|
|
414
|
+
# their IPv4 form. These addresses appear when IPv4 clients connect to
|
|
415
|
+
# a dual-stack IPv6 socket.
|
|
416
|
+
def unmap_ipv6(addr)
|
|
417
|
+
addr.delete_prefix(IPV4_MAPPED_IPV6_PREFIX)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Checks the request `Transfer-Encoding` and/or `Content-Length` to see if
|
|
421
|
+
# they are valid. Raises errors if not, otherwise reads the body.
|
|
422
|
+
# @return [Boolean] true if the body can be completely read, false otherwise
|
|
423
|
+
#
|
|
424
|
+
def setup_body
|
|
425
|
+
@body_read_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
426
|
+
|
|
427
|
+
if @env[HTTP_EXPECT] == CONTINUE
|
|
428
|
+
# TODO allow a hook here to check the headers before going forward
|
|
429
|
+
@io << HTTP_11_100
|
|
430
|
+
@io.flush
|
|
194
431
|
end
|
|
195
432
|
|
|
196
|
-
|
|
197
|
-
return true if @ready
|
|
433
|
+
@read_header = false
|
|
198
434
|
|
|
199
|
-
|
|
200
|
-
|
|
435
|
+
parser_body = @parser.body
|
|
436
|
+
|
|
437
|
+
te = @env[TRANSFER_ENCODING2]
|
|
438
|
+
if te
|
|
439
|
+
te_lwr = te.downcase
|
|
440
|
+
if te.include? ','
|
|
441
|
+
te_ary = te_lwr.split(',').each { |te| te.gsub!(STRIP_OWS, "") }
|
|
442
|
+
te_count = te_ary.count CHUNKED
|
|
443
|
+
te_valid = te_ary[0..-2].all? { |e| ALLOWED_TRANSFER_ENCODING.include? e }
|
|
444
|
+
if te_count > 1
|
|
445
|
+
raise HttpParserError , "#{TE_ERR_MSG}, multiple chunked: '#{te}'"
|
|
446
|
+
elsif te_ary.last != CHUNKED
|
|
447
|
+
raise HttpParserError , "#{TE_ERR_MSG}, last value must be chunked: '#{te}'"
|
|
448
|
+
elsif !te_valid
|
|
449
|
+
raise HttpParserError501, "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
450
|
+
end
|
|
451
|
+
@env.delete TRANSFER_ENCODING2
|
|
452
|
+
return setup_chunked_body parser_body
|
|
453
|
+
elsif te_lwr == CHUNKED
|
|
454
|
+
@env.delete TRANSFER_ENCODING2
|
|
455
|
+
return setup_chunked_body parser_body
|
|
456
|
+
elsif ALLOWED_TRANSFER_ENCODING.include? te_lwr
|
|
457
|
+
raise HttpParserError , "#{TE_ERR_MSG}, single value must be chunked: '#{te}'"
|
|
458
|
+
else
|
|
459
|
+
raise HttpParserError501 , "#{TE_ERR_MSG}, unknown value: '#{te}'"
|
|
201
460
|
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
@chunked_body = false
|
|
464
|
+
|
|
465
|
+
cl = @env[CONTENT_LENGTH]
|
|
202
466
|
|
|
203
|
-
|
|
204
|
-
|
|
467
|
+
if cl
|
|
468
|
+
# cannot contain characters that are not \d, or be empty
|
|
469
|
+
if CONTENT_LENGTH_VALUE_INVALID.match?(cl) || cl.empty?
|
|
470
|
+
@error_status_code = 400
|
|
471
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
472
|
+
raise HttpParserError, "Invalid Content-Length: #{cl.inspect}"
|
|
473
|
+
end
|
|
474
|
+
else
|
|
475
|
+
@buffer = parser_body.empty? ? nil : parser_body
|
|
476
|
+
@body = EmptyBody
|
|
477
|
+
set_ready
|
|
478
|
+
return true
|
|
205
479
|
end
|
|
206
480
|
|
|
207
|
-
|
|
481
|
+
content_length = cl.to_i
|
|
482
|
+
|
|
483
|
+
raise_above_http_content_limit if @http_content_length_limit&.< content_length
|
|
208
484
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
485
|
+
remain = content_length - parser_body.bytesize
|
|
486
|
+
|
|
487
|
+
if remain <= 0
|
|
488
|
+
# Part of the parser_body is a pipelined request OR garbage. We'll deal with that later.
|
|
489
|
+
if content_length == 0
|
|
490
|
+
@body = EmptyBody
|
|
491
|
+
if parser_body.empty?
|
|
492
|
+
@buffer = nil
|
|
493
|
+
else
|
|
494
|
+
@buffer = parser_body
|
|
495
|
+
end
|
|
496
|
+
elsif remain == 0
|
|
497
|
+
@body = StringIO.new parser_body
|
|
498
|
+
@buffer = nil
|
|
499
|
+
else
|
|
500
|
+
@body = StringIO.new(parser_body[0,content_length])
|
|
501
|
+
@buffer = parser_body[content_length..-1]
|
|
502
|
+
end
|
|
503
|
+
set_ready
|
|
504
|
+
return true
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
if remain > MAX_BODY
|
|
508
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
509
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
510
|
+
@body.binmode
|
|
511
|
+
@tempfile = @body
|
|
512
|
+
else
|
|
513
|
+
# The parser_body[0,0] trick is to get an empty string in the same
|
|
514
|
+
# encoding as parser_body.
|
|
515
|
+
@body = StringIO.new parser_body[0,0]
|
|
213
516
|
end
|
|
214
|
-
|
|
517
|
+
|
|
518
|
+
@body.write parser_body
|
|
519
|
+
|
|
520
|
+
@body_remain = remain
|
|
521
|
+
|
|
522
|
+
false
|
|
523
|
+
end
|
|
215
524
|
|
|
216
525
|
def read_body
|
|
526
|
+
if @chunked_body
|
|
527
|
+
return read_chunked_body
|
|
528
|
+
end
|
|
529
|
+
|
|
217
530
|
# Read an odd sized chunk so we can read even sized ones
|
|
218
531
|
# after this
|
|
219
532
|
remain = @body_remain
|
|
220
533
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
534
|
+
# don't bother with reading zero bytes
|
|
535
|
+
unless remain.zero?
|
|
536
|
+
begin
|
|
537
|
+
chunk = @io.read_nonblock(remain.clamp(0, CHUNK_SIZE), @read_buffer)
|
|
538
|
+
rescue IO::WaitReadable
|
|
539
|
+
return false
|
|
540
|
+
rescue SystemCallError, IOError
|
|
541
|
+
raise ConnectionError, "Connection error detected during read"
|
|
542
|
+
end
|
|
226
543
|
|
|
227
|
-
|
|
228
|
-
chunk
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
544
|
+
# No chunk means a closed socket
|
|
545
|
+
unless chunk
|
|
546
|
+
@body.close
|
|
547
|
+
@buffer = nil
|
|
548
|
+
set_ready
|
|
549
|
+
raise EOFError
|
|
550
|
+
end
|
|
234
551
|
|
|
235
|
-
|
|
236
|
-
unless chunk
|
|
237
|
-
@body.close
|
|
238
|
-
@buffer = nil
|
|
239
|
-
@requests_served += 1
|
|
240
|
-
@ready = true
|
|
241
|
-
raise EOFError
|
|
552
|
+
remain -= @body.write(chunk)
|
|
242
553
|
end
|
|
243
554
|
|
|
244
|
-
remain -= @body.write(chunk)
|
|
245
|
-
|
|
246
555
|
if remain <= 0
|
|
247
556
|
@body.rewind
|
|
248
557
|
@buffer = nil
|
|
249
|
-
|
|
250
|
-
|
|
558
|
+
set_ready
|
|
559
|
+
true
|
|
560
|
+
else
|
|
561
|
+
@body_remain = remain
|
|
562
|
+
false
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def read_chunked_body
|
|
567
|
+
while true
|
|
568
|
+
begin
|
|
569
|
+
chunk = @io.read_nonblock(CHUNK_SIZE, @read_buffer)
|
|
570
|
+
rescue IO::WaitReadable
|
|
571
|
+
return false
|
|
572
|
+
rescue SystemCallError, IOError
|
|
573
|
+
raise ConnectionError, "Connection error detected during read"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# No chunk means a closed socket
|
|
577
|
+
unless chunk
|
|
578
|
+
@body.close
|
|
579
|
+
@buffer = nil
|
|
580
|
+
set_ready
|
|
581
|
+
raise EOFError
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
if decode_chunk(chunk)
|
|
585
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
|
586
|
+
return true
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def setup_chunked_body(body)
|
|
592
|
+
@chunked_body = true
|
|
593
|
+
@partial_part_left = 0
|
|
594
|
+
@prev_chunk = ""
|
|
595
|
+
@excess_cr = 0
|
|
596
|
+
|
|
597
|
+
@body = Tempfile.create(Const::PUMA_TMP_BASE)
|
|
598
|
+
File.unlink @body.path unless IS_WINDOWS
|
|
599
|
+
@body.binmode
|
|
600
|
+
@tempfile = @body
|
|
601
|
+
@chunked_content_length = 0
|
|
602
|
+
|
|
603
|
+
if decode_chunk(body)
|
|
604
|
+
@env[CONTENT_LENGTH] = @chunked_content_length.to_s
|
|
251
605
|
return true
|
|
252
606
|
end
|
|
607
|
+
end
|
|
253
608
|
|
|
254
|
-
|
|
609
|
+
# @version 5.0.0
|
|
610
|
+
def write_chunk(str)
|
|
611
|
+
if @http_content_length_limit&.< @chunked_content_length + str.bytesize
|
|
612
|
+
raise_above_http_content_limit
|
|
613
|
+
end
|
|
255
614
|
|
|
256
|
-
|
|
615
|
+
@chunked_content_length += @body.write(str)
|
|
257
616
|
end
|
|
258
617
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
@
|
|
262
|
-
|
|
618
|
+
def decode_chunk(chunk)
|
|
619
|
+
if @partial_part_left > 0
|
|
620
|
+
if @partial_part_left <= chunk.size
|
|
621
|
+
if @partial_part_left > 2
|
|
622
|
+
write_chunk(chunk[0..(@partial_part_left-3)]) # skip the \r\n
|
|
623
|
+
end
|
|
624
|
+
chunk = chunk[@partial_part_left..-1]
|
|
625
|
+
@partial_part_left = 0
|
|
626
|
+
else
|
|
627
|
+
if @partial_part_left > 2
|
|
628
|
+
if @partial_part_left == chunk.size + 1
|
|
629
|
+
# Don't include the last \r
|
|
630
|
+
write_chunk(chunk[0..(@partial_part_left-3)])
|
|
631
|
+
else
|
|
632
|
+
# don't include the last \r\n
|
|
633
|
+
write_chunk(chunk)
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
@partial_part_left -= chunk.size
|
|
637
|
+
return false
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
if @prev_chunk.empty?
|
|
642
|
+
io = StringIO.new(chunk)
|
|
643
|
+
else
|
|
644
|
+
io = StringIO.new(@prev_chunk+chunk)
|
|
645
|
+
@prev_chunk = ""
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
while !io.eof?
|
|
649
|
+
line = io.gets
|
|
650
|
+
if line.end_with?(CHUNK_VALID_ENDING)
|
|
651
|
+
# Puma doesn't process chunk extensions, but should parse if they're
|
|
652
|
+
# present, which is the reason for the semicolon regex
|
|
653
|
+
chunk_hex = line.strip[/\A[^;]+/]
|
|
654
|
+
if CHUNK_SIZE_INVALID.match? chunk_hex
|
|
655
|
+
raise HttpParserError, "Invalid chunk size: '#{chunk_hex}'"
|
|
656
|
+
end
|
|
657
|
+
len = chunk_hex.to_i(16)
|
|
658
|
+
if len == 0
|
|
659
|
+
@in_last_chunk = true
|
|
660
|
+
@body.rewind
|
|
661
|
+
rest = io.read
|
|
662
|
+
if rest.bytesize < CHUNK_VALID_ENDING_SIZE
|
|
663
|
+
@buffer = nil
|
|
664
|
+
@partial_part_left = CHUNK_VALID_ENDING_SIZE - rest.bytesize
|
|
665
|
+
return false
|
|
666
|
+
else
|
|
667
|
+
# if the next character is a CRLF, set buffer to everything after that CRLF
|
|
668
|
+
start_of_rest = if rest.start_with?(CHUNK_VALID_ENDING)
|
|
669
|
+
CHUNK_VALID_ENDING_SIZE
|
|
670
|
+
else # we have started a trailer section, which we do not support. skip it!
|
|
671
|
+
rest.index(CHUNK_VALID_ENDING*2) + CHUNK_VALID_ENDING_SIZE*2
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
@buffer = rest[start_of_rest..-1]
|
|
675
|
+
@buffer = nil if @buffer.empty?
|
|
676
|
+
set_ready
|
|
677
|
+
return true
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
# Track the excess as a function of the size of the
|
|
682
|
+
# header vs the size of the actual data. Excess can
|
|
683
|
+
# go negative (and is expected to) when the body is
|
|
684
|
+
# significant.
|
|
685
|
+
# The additional of chunk_hex.size and 2 compensates
|
|
686
|
+
# for a client sending 1 byte in a chunked body over
|
|
687
|
+
# a long period of time, making sure that that client
|
|
688
|
+
# isn't accidentally eventually punished.
|
|
689
|
+
@excess_cr += (line.size - len - chunk_hex.size - 2)
|
|
690
|
+
|
|
691
|
+
if @excess_cr >= MAX_CHUNK_EXCESS
|
|
692
|
+
raise HttpParserError, "Maximum chunk excess detected"
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
len += 2
|
|
696
|
+
|
|
697
|
+
part = io.read(len)
|
|
698
|
+
|
|
699
|
+
unless part
|
|
700
|
+
@partial_part_left = len
|
|
701
|
+
next
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
got = part.size
|
|
705
|
+
|
|
706
|
+
case
|
|
707
|
+
when got == len
|
|
708
|
+
# proper chunked segment must end with "\r\n"
|
|
709
|
+
if part.end_with? CHUNK_VALID_ENDING
|
|
710
|
+
write_chunk(part[0..-3]) # to skip the ending \r\n
|
|
711
|
+
else
|
|
712
|
+
raise HttpParserError, "Chunk size mismatch"
|
|
713
|
+
end
|
|
714
|
+
when got <= len - 2
|
|
715
|
+
write_chunk(part)
|
|
716
|
+
@partial_part_left = len - part.size
|
|
717
|
+
when got == len - 1 # edge where we get just \r but not \n
|
|
718
|
+
write_chunk(part[0..-2])
|
|
719
|
+
@partial_part_left = len - part.size
|
|
720
|
+
end
|
|
721
|
+
else
|
|
722
|
+
if @prev_chunk.size + line.size >= MAX_CHUNK_HEADER_SIZE
|
|
723
|
+
raise HttpParserError, "maximum size of chunk header exceeded"
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
@prev_chunk = line
|
|
727
|
+
return false
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
if @in_last_chunk
|
|
732
|
+
set_ready
|
|
733
|
+
true
|
|
734
|
+
else
|
|
735
|
+
false
|
|
263
736
|
end
|
|
264
737
|
end
|
|
265
738
|
|
|
266
|
-
def
|
|
267
|
-
|
|
268
|
-
@
|
|
269
|
-
rescue StandardError
|
|
739
|
+
def set_ready
|
|
740
|
+
if @body_read_start
|
|
741
|
+
@env['puma.request_body_wait'] = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - @body_read_start
|
|
270
742
|
end
|
|
743
|
+
@requests_served += 1
|
|
744
|
+
@ready = true
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def raise_above_http_content_limit
|
|
748
|
+
@http_content_length_limit_exceeded = true
|
|
749
|
+
@buffer = nil
|
|
750
|
+
@body = EmptyBody
|
|
751
|
+
@error_status_code = 413
|
|
752
|
+
@env[HTTP_CONNECTION] = 'close'
|
|
753
|
+
raise HttpParserError, "Payload Too Large"
|
|
271
754
|
end
|
|
272
755
|
end
|
|
273
756
|
end
|