HTTP::Body, added support for chunked requests
Andy Grundman [Sat, 23 Feb 2008 14:53:13 +0000 (14:53 +0000)]
16 files changed:
Changes
Makefile.PL
lib/HTTP/Body.pm
lib/HTTP/Body/MultiPart.pm
t/04multipart.t
t/05urlencoded.t
t/06octetstream.t
t/data/multipart/012-content.dat [new file with mode: 0644]
t/data/multipart/012-headers.yml [new file with mode: 0644]
t/data/multipart/012-results.yml [new file with mode: 0644]
t/data/octetstream/003-content.dat [new file with mode: 0644]
t/data/octetstream/003-headers.yml [new file with mode: 0644]
t/data/octetstream/003-results.dat [new file with mode: 0644]
t/data/urlencoded/003-content.dat [new file with mode: 0644]
t/data/urlencoded/003-headers.yml [new file with mode: 0644]
t/data/urlencoded/003-results.yml [new file with mode: 0644]

diff --git a/Changes b/Changes
index 9497e8e..5965ddc 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,5 +1,8 @@
 This file documents the revision history for Perl extension HTTP::Body.
 
+1.00
+        - Added support for chunked requests if no $length value is passed to new().
+
 0.9   2007-03-27 14:00:00
         - Fixed bug where empty fields in multipart/form-data were ignored.
           (Ton Voon)
index 0783869..3e3de46 100644 (file)
@@ -6,9 +6,10 @@ WriteMakefile(
     NAME         => 'HTTP::Body',
     VERSION_FROM => 'lib/HTTP/Body.pm',
     PREREQ_PM    => {
-        Carp         => 0,
-        File::Temp   => '0.14',
-        IO::File     => 0,
-        YAML         => '0.39'
+        Carp          => 0,
+        File::Temp    => '0.14',
+        HTTP::Headers => 0, 
+        IO::File      => 0,
+        YAML          => '0.39'
     }
 );
index 4dd8f9f..3025a88 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 
 use Carp       qw[ ];
 
-our $VERSION = 0.91;
+our $VERSION = 1.00;
 
 our $TYPES = {
     'application/octet-stream'          => 'HTTP::Body::OctetStream',
@@ -16,6 +16,9 @@ require HTTP::Body::OctetStream;
 require HTTP::Body::UrlEncoded;
 require HTTP::Body::MultiPart;
 
+use HTTP::Headers;
+use HTTP::Message;
+
 =head1 NAME
 
 HTTP::Body - HTTP Body Parser
@@ -53,6 +56,8 @@ HTTP::Body parses chunks of HTTP POST data and supports
 application/octet-stream, application/x-www-form-urlencoded, and
 multipart/form-data.
 
+Chunked bodies are supported by not passing a length value to new().
+
 It is currently used by L<Catalyst> to parse POST bodies.
 
 =head1 METHODS
@@ -69,8 +74,8 @@ returns a L<HTTP::Body> object.
 sub new {
     my ( $class, $content_type, $content_length ) = @_;
 
-    unless ( @_ == 3 ) {
-        Carp::croak( $class, '->new( $content_type, $content_length )' );
+    unless ( @_ >= 2 ) {
+        Carp::croak( $class, '->new( $content_type, [ $content_length ] )' );
     }
 
     my $type;
@@ -90,8 +95,10 @@ sub new {
 
     my $self = {
         buffer         => '',
+        chunk_buffer   => '',
         body           => undef,
-        content_length => $content_length,
+        chunked        => !defined $content_length,
+        content_length => defined $content_length ? $content_length : -1,
         content_type   => $content_type,
         length         => 0,
         param          => {},
@@ -114,6 +121,57 @@ length before adding self.
 sub add {
     my $self = shift;
     
+    if ( $self->{chunked} ) {
+        $self->{chunk_buffer} .= $_[0];
+        
+        while ( $self->{chunk_buffer} =~ m/^([\da-fA-F]+).*\x0D\x0A/ ) {
+            my $chunk_len = hex($1);
+            
+            if ( $chunk_len == 0 ) {
+                # Strip chunk len
+                $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
+                
+                # End of data, there may be trailing headers
+                if (  my ($headers) = $self->{chunk_buffer} =~ m/(.*)\x0D\x0A/s ) {
+                    if ( my $message = HTTP::Message->parse( $headers ) ) {
+                        $self->{trailing_headers} = $message->headers;
+                    }
+                }
+                
+                $self->{chunk_buffer} = '';
+                
+                # Set content_length equal to the amount of data we read,
+                # so the spin methods can finish up.
+                $self->{content_length} = $self->{length};
+            }
+            else {
+                # Make sure we have the whole chunk in the buffer (+CRLF)
+                if ( length( $self->{chunk_buffer} ) >= $chunk_len ) {
+                    # Strip chunk len
+                    $self->{chunk_buffer} =~ s/^([\da-fA-F]+).*\x0D\x0A//;
+                    
+                    # Pull chunk data out of chunk buffer into real buffer
+                    $self->{buffer} .= substr $self->{chunk_buffer}, 0, $chunk_len, '';
+                
+                    # Strip remaining CRLF
+                    $self->{chunk_buffer} =~ s/^\x0D\x0A//;
+                
+                    $self->{length} += $chunk_len;
+                }
+                else {
+                    # Not enough data for this chunk, wait for more calls to add()
+                    return;
+                }
+            }
+            
+            unless ( $self->{state} eq 'done' ) {
+                $self->spin;
+            }
+        }
+        
+        return;
+    }
+    
     my $cl = $self->content_length;
 
     if ( defined $_[0] ) {
@@ -147,19 +205,20 @@ sub body {
     return $self->{body};
 }
 
-=item buffer
+=item chunked
 
-read only accessor for the buffer.
+Returns 1 if the request is chunked.
 
 =cut
 
-sub buffer {
-    return shift->{buffer};
+sub chunked {
+    return shift->{chunked};
 }
 
 =item content_length
 
-read only accessor for content length
+Returns the content-length for the body data if known.
+Returns -1 if the request is chunked.
 
 =cut
 
@@ -169,7 +228,7 @@ sub content_length {
 
 =item content_type
 
-read only accessor for the content type
+Returns the content-type of the body data.
 
 =cut
 
@@ -189,7 +248,9 @@ sub init {
 
 =item length
 
-read only accessor for body length.
+Returns the total length of data we expect to read if known.
+In the case of a chunked request, returns the amount of data
+read so far.
 
 =cut
 
@@ -197,6 +258,17 @@ sub length {
     return shift->{length};
 }
 
+=item trailing_headers
+
+If a chunked request body had trailing headers, trailing_headers will
+return an HTTP::Headers object populated with those headers.
+
+=cut
+
+sub trailing_headers {
+    return shift->{trailing_headers};
+}
+
 =item spin
 
 Abstract method to spin the io handle.
@@ -209,7 +281,7 @@ sub spin {
 
 =item state
 
-accessor for body state.
+Returns the current state of the parser.
 
 =cut
 
@@ -221,7 +293,7 @@ sub state {
 
 =item param
 
-accesor for http parameters.
+Get/set body parameters.
 
 =cut
 
@@ -248,6 +320,8 @@ sub param {
 
 =item upload
 
+Get/set file uploads.
+
 =cut
 
 sub upload {
@@ -273,16 +347,14 @@ sub upload {
 
 =back
 
-=head1 BUGS
-
-Chunked requests are currently not supported.
-
 =head1 AUTHOR
 
 Christian Hansen, C<ch@ngmedia.com>
 
 Sebastian Riedel, C<sri@cpan.org>
 
+Andy Grundman, C<andy@hybridized.org>
+
 =head1 LICENSE
 
 This library is free software. You can redistribute it and/or modify 
index b5860f1..5ff3970 100644 (file)
@@ -274,7 +274,7 @@ sub handler {
 
                 $part->{fh}       = $fh;
                 $part->{tempname} = $fh->filename;
-               }
+            }
         }
     }
 
@@ -285,7 +285,7 @@ sub handler {
     if ( $part->{done} ) {
 
         if ( exists $part->{filename} ) {
-               if ( $part->{filename} ne "" ) {
+            if ( $part->{filename} ne "" ) {
                 $part->{fh}->close;
 
                 delete @{$part}{qw[ data done fh ]};
index 75a7f89..7f2c0f5 100644 (file)
@@ -3,7 +3,7 @@
 use strict;
 use warnings;
 
-use Test::More tests => 55;
+use Test::More tests => 60;
 
 use Cwd;
 use HTTP::Body;
@@ -13,7 +13,7 @@ use YAML;
 
 my $path = catdir( getcwd(), 't', 'data', 'multipart' );
 
-for ( my $i = 1; $i <= 11; $i++ ) {
+for ( my $i = 1; $i <= 12; $i++ ) {
 
     my $test    = sprintf( "%.3d", $i );
     my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
@@ -40,5 +40,5 @@ for ( my $i = 1; $i <= 11; $i++ ) {
     is_deeply( $body->param, $results->{param}, "$test MultiPart param" );
     is_deeply( $body->upload, $results->{upload}, "$test MultiPart upload" );
     cmp_ok( $body->state, 'eq', 'done', "$test MultiPart state" );
-    cmp_ok( $body->length, '==', $headers->{'Content-Length'}, "$test MultiPart length" );
+    cmp_ok( $body->length, '==', $body->content_length, "$test MultiPart length" );
 }
index b73c51a..ef92262 100644 (file)
@@ -3,9 +3,10 @@
 use strict;
 use warnings;
 
-use Test::More tests => 10;
+use Test::More tests => 16;
 
 use Cwd;
+use Digest::MD5 qw(md5_hex);
 use HTTP::Body;
 use File::Spec::Functions;
 use IO::File;
@@ -13,7 +14,7 @@ use YAML;
 
 my $path = catdir( getcwd(), 't', 'data', 'urlencoded' );
 
-for ( my $i = 1; $i <= 2; $i++ ) {
+for ( my $i = 1; $i <= 3; $i++ ) {
 
     my $test    = sprintf( "%.3d", $i );
     my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
@@ -31,5 +32,12 @@ for ( my $i = 1; $i <= 2; $i++ ) {
     is_deeply( $body->param, $results->{param}, "$test UrlEncoded param" );
     is_deeply( $body->upload, $results->{upload}, "$test UrlEncoded upload" );
     cmp_ok( $body->state, 'eq', 'done', "$test UrlEncoded state" );
-    cmp_ok( $body->length, '==', $headers->{'Content-Length'}, "$test UrlEncoded length" );
+    cmp_ok( $body->length, '==', $body->content_length, "$test UrlEncoded length" );
+    
+    # Check trailing header on the chunked request
+    if ( $i == 3 ) {
+        my $content = IO::File->new( catfile( $path, "002-content.dat" ) );
+        $content->read( my $buf, 4096 );
+        is( $body->trailing_headers->header('Content-MD5'), md5_hex($buf), "$test trailing header ok" );
+    }
 }
index 87331a2..fde0383 100644 (file)
@@ -1,7 +1,7 @@
 use strict;
 use warnings;
 
-use Test::More tests => 8;
+use Test::More tests => 12;
 
 use Cwd;
 use HTTP::Body;
@@ -11,7 +11,7 @@ use YAML;
 
 my $path = catdir( getcwd(), 't', 'data', 'octetstream' );
 
-for ( my $i = 1 ; $i <= 2 ; $i++ ) {
+for ( my $i = 1 ; $i <= 3 ; $i++ ) {
 
     my $test = sprintf( "%.3d", $i );
     my $headers = YAML::LoadFile( catfile( $path, "$test-headers.yml" ) );
@@ -33,7 +33,7 @@ for ( my $i = 1 ; $i <= 2 ; $i++ ) {
     cmp_ok( $body->state, 'eq', 'done', "$test UrlEncoded state" );
     cmp_ok(
         $body->length, '==',
-        $headers->{'Content-Length'},
+        $body->content_length,
         "$test UrlEncoded length"
     );
 }
diff --git a/t/data/multipart/012-content.dat b/t/data/multipart/012-content.dat
new file mode 100644 (file)
index 0000000..1204041
--- /dev/null
@@ -0,0 +1,75 @@
+1ce\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="text1"\r
+\r
+Ratione accusamus aspernatur aliquam\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="text2"\r
+\r
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="select"\r
+\r
+A\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="select"\r
+\r
+B\r
+-----------------------------7d534d1d60150\r
+Co\r
+1ce\r
+ntent-Disposition: form-data; name="textarea"\r
+\r
+Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r
+\r
+Molestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa.\r
+-----------------------------7d534d1d60150\r
+Content-\r
+1ce\r
+Disposition: form-data; name="upload"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload"; filename=""\r
+Content-Type: application/octet-stream\r
+\r
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload1"; filename="C:\\r
+1ce\r
+Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150\r
+Content-Disposition: form-data; name="upload2"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"\r
+Content-Type: text/plain\r
+\r
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+print "Hello World :)\n";
+
+\r
+-----------------------------7d534d1d60150-\r
+3\r
+-\r
+\r
+0\r
diff --git a/t/data/multipart/012-headers.yml b/t/data/multipart/012-headers.yml
new file mode 100644 (file)
index 0000000..39b8281
--- /dev/null
@@ -0,0 +1,4 @@
+---
+Transfer-Encoding: chunked
+Content-Type: multipart/form-data; boundary=---------------------------7d534d1d60150
+User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
diff --git a/t/data/multipart/012-results.yml b/t/data/multipart/012-results.yml
new file mode 100644 (file)
index 0000000..28c5f03
--- /dev/null
@@ -0,0 +1,31 @@
+---
+body: ~
+param:
+  select:
+    - A
+    - B
+  text1: Ratione accusamus aspernatur aliquam
+  text2: ""
+  textarea: "Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r\n\r\nMolestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa."
+upload:
+  upload:
+    filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+    headers:
+      Content-Disposition: form-data; name="upload"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+      Content-Type: text/plain
+    name: upload
+    size: 71
+  upload1:
+    filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+    headers:
+      Content-Disposition: form-data; name="upload1"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+      Content-Type: text/plain
+    name: upload1
+    size: 71
+  upload2:
+    filename: C:\Documents and Settings\Administrator\Desktop\hello.pl
+    headers:
+      Content-Disposition: form-data; name="upload2"; filename="C:\Documents and Settings\Administrator\Desktop\hello.pl"
+      Content-Type: text/plain
+    name: upload2
+    size: 71
diff --git a/t/data/octetstream/003-content.dat b/t/data/octetstream/003-content.dat
new file mode 100644 (file)
index 0000000..d9cc7bd
--- /dev/null
@@ -0,0 +1,10 @@
+143\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+146\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+d1\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r
+a7\r
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+\r
+0\r
diff --git a/t/data/octetstream/003-headers.yml b/t/data/octetstream/003-headers.yml
new file mode 100644 (file)
index 0000000..21d05b7
--- /dev/null
@@ -0,0 +1,4 @@
+---
+Transfer-Encoding: chunked
+Content-Type: application/x.atom+xml
+User-Agent: 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312'
diff --git a/t/data/octetstream/003-results.dat b/t/data/octetstream/003-results.dat
new file mode 100644 (file)
index 0000000..3b45458
--- /dev/null
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/t/data/urlencoded/003-content.dat b/t/data/urlencoded/003-content.dat
new file mode 100644 (file)
index 0000000..802169f
--- /dev/null
@@ -0,0 +1,20 @@
+f9\r
+text1=Ratione+accusamus+aspernatur+aliquam&text2=%C3%A5%C3%A4%C3%B6%C3%A5%C3%A4%C3%B6&select=A&select=B&textarea=Voluptatem+cumque+voluptate+sit+recusandae+at.+Et+quas+facere+rerum+unde+esse.+Sit+est+et+voluptatem.+Vel+temporibus+velit+neque+odio+no\r
+7d\r
+n.%0D%0A%0D%0AMolestias+rerum+ut+sapiente+facere+repellendus+illo.+Eum+nulla+quis+aut.+Quidem+voluptas+vitae+ipsam+officia+vo\r
+3e\r
+luptatibus+eveniet.+Aspernatur+cupiditate+ratione+aliquam+quid\r
+1f\r
+em+corrupti.+Eos+sunt+rerum+non\r
+10\r
++optio+culpa.&en\r
+8\r
+coding=f\r
+4\r
+oo%3\r
+2\r
+Db\r
+2\r
+ar\r
+0\r
+Content-MD5: 7fd07d9cb09bfd205f057440be771540\r
diff --git a/t/data/urlencoded/003-headers.yml b/t/data/urlencoded/003-headers.yml
new file mode 100644 (file)
index 0000000..21e3480
--- /dev/null
@@ -0,0 +1,4 @@
+---
+Transfer-Encoding: chunked
+Content-Type: application/x-www-form-urlencoded
+User-Agent: 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.1 (KHTML, like Gecko) Safari/312'
diff --git a/t/data/urlencoded/003-results.yml b/t/data/urlencoded/003-results.yml
new file mode 100644 (file)
index 0000000..1571078
--- /dev/null
@@ -0,0 +1,11 @@
+---
+body: ~
+param:
+  select:
+    - A
+    - B
+  text1: Ratione accusamus aspernatur aliquam
+  text2: åäöåäö
+  textarea: "Voluptatem cumque voluptate sit recusandae at. Et quas facere rerum unde esse. Sit est et voluptatem. Vel temporibus velit neque odio non.\r\n\r\nMolestias rerum ut sapiente facere repellendus illo. Eum nulla quis aut. Quidem voluptas vitae ipsam officia voluptatibus eveniet. Aspernatur cupiditate ratione aliquam quidem corrupti. Eos sunt rerum non optio culpa."
+  encoding: foo=bar
+upload: {}