Skip to content

Commit edbbd12

Browse files
Use released form data parsers.
Assisted-By: devx/e76b566a-41cd-407d-a2fd-c5b79c88879d
1 parent aa46c6b commit edbbd12

4 files changed

Lines changed: 39 additions & 56 deletions

File tree

lib/utopia/request.rb

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,18 @@
44
# Copyright, 2026, by Samuel Williams.
55

66
require "tempfile"
7+
require "stringio"
78

89
require "protocol/http/request"
910
require "protocol/multipart/form_data"
10-
require "protocol/url/encoding"
11+
require "protocol/url/form_data/parser"
1112

1213
module Utopia
1314
# Utopia's application-facing request wrapper.
1415
#
1516
# Protocol request methods are delegated to the underlying request; parsing
1617
# and application conveniences live here rather than on protocol-http itself.
1718
class Request
18-
# The content type used by URL-encoded HTML forms.
19-
FORM_URL_ENCODED = "application/x-www-form-urlencoded"
20-
21-
# The content type used by multipart HTML forms.
22-
MULTIPART_FORM_DATA = "multipart/form-data"
23-
24-
# The maximum nesting depth accepted for structured arguments.
25-
MAXIMUM_ARGUMENT_DEPTH = 8
26-
27-
# The default maximum size of a URL-encoded form body.
28-
MAXIMUM_URL_ENCODED_SIZE = Protocol::Multipart::FormData::MAXIMUM_FIELD_SIZE
29-
3019
FORM_DATA_UNDEFINED = Object.new.freeze
3120
private_constant :FORM_DATA_UNDEFINED
3221

@@ -331,8 +320,8 @@ def respond_to_missing?(name, include_private = false)
331320
def decode_arguments(query)
332321
return {} unless query
333322

334-
# HTML form encoding represents spaces using `+`, while Protocol::URL uses percent encoding.
335-
return Protocol::URL::Encoding.decode(query.gsub("+", "%20"), MAXIMUM_ARGUMENT_DEPTH)
323+
parser = Protocol::URL::FormData::Parser.new
324+
return parser.parse(StringIO.new(query))
336325
end
337326

338327
def decode_form_data(options)
@@ -342,10 +331,9 @@ def decode_form_data(options)
342331
content_type = Protocol::Multipart::Header::ContentType.coerce(value)
343332

344333
case content_type.type
345-
when FORM_URL_ENCODED
346-
maximum_size = options.fetch(:maximum_total_size, MAXIMUM_URL_ENCODED_SIZE)
347-
return decode_arguments(read_body(maximum_size))
348-
when MULTIPART_FORM_DATA
334+
when Protocol::URL::FormData::Parser::CONTENT_TYPE
335+
return decode_url_encoded_form(options)
336+
when Protocol::Multipart::FormData::Parser::CONTENT_TYPE
349337
boundary = content_type["boundary"]
350338

351339
unless boundary
@@ -358,41 +346,35 @@ def decode_form_data(options)
358346
end
359347
end
360348

361-
def read_body(maximum_size)
362-
content = String.new.b
363-
limit = Protocol::Multipart::ByteLimit.new(maximum_size, name: :form_size)
364-
365-
if body = self.body
366-
while chunk = body.read
367-
limit.consume(chunk.bytesize)
368-
content << chunk
369-
end
370-
end
349+
def decode_url_encoded_form(options)
350+
body = self.body
351+
return {} unless body
371352

372-
return content
353+
parser = Protocol::URL::FormData::Parser.new(**options)
354+
return parser.parse(body)
373355
end
374356

375357
def decode_multipart_form(boundary, **options)
376-
arguments = {}
377358
body = self.body
378359

379-
return arguments unless body
360+
return {} unless body
380361

381362
io = BodyIO.new(body)
363+
parser = Protocol::Multipart::FormData::Parser.new(**options)
382364

383365
begin
384-
Protocol::Multipart::FormData.parse(io, boundary, **options) do |name, value|
366+
result = parser.parse(io, boundary:) do |_name, value|
385367
if value.is_a?(Protocol::Multipart::FormData::Upload)
386-
value = create_upload(value)
368+
create_upload(value)
369+
else
370+
value
387371
end
388-
389-
assign_argument(arguments, name, value)
390372
end
391373
ensure
392374
io.close
393375
end
394376

395-
return arguments
377+
return result
396378
end
397379

398380
def create_upload(upload)
@@ -411,20 +393,6 @@ def create_upload(upload)
411393
end
412394
end
413395

414-
def assign_argument(arguments, name, value)
415-
keys = Protocol::URL::Encoding.split(name)
416-
417-
if keys.empty?
418-
raise ArgumentError, "Invalid argument name: #{name.inspect}!"
419-
end
420-
421-
if keys.size > MAXIMUM_ARGUMENT_DEPTH
422-
raise ArgumentError, "Argument depth exceeded limit!"
423-
end
424-
425-
Protocol::URL::Encoding.assign(keys, value, arguments)
426-
end
427-
428396
def parse_cookies(cookie_header)
429397
cookies = {}
430398

test/utopia/application.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def response_object.to_response
7474
Protocol::HTTP::Body::Buffered.wrap("value=large")
7575
]
7676

77-
expect{application.call(request)}.to raise_exception(RangeError, message: be =~ /form_size exceeded/)
77+
expect{application.call(request)}.to raise_exception(RangeError, message: be =~ /total_size exceeded/)
7878
end
7979

8080
it "loads a top-level application constant" do

test/utopia/request.rb

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@
7070
}
7171
end
7272

73+
it "distinguishes absent and empty query values" do
74+
request.path = "/search?absent&empty="
75+
76+
expect(request.query_arguments).to be == {"absent" => nil, "empty" => ""}
77+
end
78+
7379
it "decodes URL encoded form data" do
7480
request.headers["content-type"] = "application/x-www-form-urlencoded"
7581
request.body = Protocol::HTTP::Body::Buffered.wrap("user[name]=Samuel&query=hello+world")
@@ -111,7 +117,16 @@
111117

112118
expect do
113119
request.form_data(maximum_total_size: 4)
114-
end.to raise_exception(RangeError, message: be =~ /form_size exceeded/)
120+
end.to raise_exception(RangeError, message: be =~ /total_size exceeded/)
121+
end
122+
123+
it "limits URL encoded form pairs" do
124+
request.headers["content-type"] = "application/x-www-form-urlencoded"
125+
request.body = Protocol::HTTP::Body::Buffered.wrap("a=1&b=2")
126+
127+
expect do
128+
request.form_data(maximum_pair_count: 1)
129+
end.to raise_exception(RangeError, message: be =~ /pair_count exceeded/)
115130
end
116131

117132
it "allows endpoint-specific limits on the first form decode" do
@@ -250,7 +265,7 @@
250265
derived.headers["content-type"] = "application/x-www-form-urlencoded"
251266
derived.body = Protocol::HTTP::Body::Buffered.wrap("value=large")
252267

253-
expect{derived.form_data}.to raise_exception(RangeError, message: be =~ /form_size exceeded/)
268+
expect{derived.form_data}.to raise_exception(RangeError, message: be =~ /total_size exceeded/)
254269
end
255270

256271
it "preserves the original request path across multiple derived requests" do

utopia.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ Gem::Specification.new do |spec|
3535
spec.add_dependency "msgpack"
3636
spec.add_dependency "net-smtp"
3737
spec.add_dependency "protocol-http", "~> 0.68"
38-
spec.add_dependency "protocol-multipart", "~> 0.2"
39-
spec.add_dependency "protocol-url", "~> 0.4"
38+
spec.add_dependency "protocol-multipart", "~> 0.3"
39+
spec.add_dependency "protocol-url", "~> 0.7"
4040
spec.add_dependency "samovar", "~> 2.1"
4141
spec.add_dependency "traces", "~> 0.10"
4242
spec.add_dependency "variant", "~> 0.1"

0 commit comments

Comments
 (0)