44# Copyright, 2026, by Samuel Williams.
55
66require "tempfile"
7+ require "stringio"
78
89require "protocol/http/request"
910require "protocol/multipart/form_data"
10- require "protocol/url/encoding "
11+ require "protocol/url/form_data/parser "
1112
1213module 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
0 commit comments