diff --git a/VERSION b/VERSION
index 99f6ee8..4fe10f9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.2503.142359
+0.2503.150208
diff --git a/src/Dependencies.php b/src/Dependencies.php
index 7be9c1e..a935bb1 100644
--- a/src/Dependencies.php
+++ b/src/Dependencies.php
@@ -1,7 +1,7 @@
 <?php
 // Dependencies.php
 // Created: 2025-01-18
-// Updated: 2025-03-07
+// Updated: 2025-03-15
 
 namespace Index;
 
@@ -50,7 +50,7 @@ class Dependencies {
             try {
                 $typeInfo = $paramInfo->getType();
                 $allowsNull = $typeInfo === null || $typeInfo->allowsNull();
-                $value = null;
+                $value = $paramInfo->isDefaultValueAvailable() ? $paramInfo->getDefaultValue() : null;
 
                 // maybe support intersection and union someday
                 if($typeInfo instanceof ReflectionNamedType && !$typeInfo->isBuiltin()) {
diff --git a/src/Http/Content/FormContent.php b/src/Http/Content/FormContent.php
index 32fb08c..e3fbe3c 100644
--- a/src/Http/Content/FormContent.php
+++ b/src/Http/Content/FormContent.php
@@ -1,66 +1,16 @@
 <?php
 // FormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-14
+// Updated: 2025-03-15
 
 namespace Index\Http\Content;
 
 use Iterator;
+use Index\Http\HttpParameters;
 
 /**
  * Provides a common interface for application/x-www-form-urlencoded and multipart/form-data.
  *
- * @template TValue of string|\Stringable
- * @extends Iterator<string, ?list<TValue>>
+ * @extends Iterator<string, ?list<string|\Stringable>>
  */
-interface FormContent extends Content, Iterator {
-    /**
-     * Checks if a form field is present.
-     *
-     * @param string $name Name of the form field.
-     * @return bool
-     */
-    public function hasParam(string $name): bool;
-
-    /**
-     * Retrieves an HTTP request form field, or null if it is not present.
-     *
-     * @param string $name Name of the request form field.
-     * @param int $filter A PHP filter extension filter constant.
-     * @param mixed[]|int $options Options for the PHP filter.
-     * @param mixed $default Default value to fall back on.
-     * @return mixed Value of the form field, null if not present.
-     */
-    public function getParam(
-        string $name,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed;
-
-    /**
-     * Retrieves an HTTP request form field, or null if it is not present.
-     *
-     * @param string $name Name of the request form field.
-     * @param int $index Index of the parameter value.
-     * @param int $filter A PHP filter extension filter constant.
-     * @param mixed[]|int $options Options for the PHP filter.
-     * @param mixed $default Default value to fall back on.
-     * @return mixed Value of the form field, null if not present.
-     */
-    public function getParamAt(
-        string $name,
-        int $index,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed;
-
-    /**
-     * Retrieves how many parameters of the same name appear in the HTTP request query field.
-     *
-     * @param string $name Name of the request query field.
-     * @return int<0, max> Amount of values.
-     */
-    public function getParamCount(string $name): int;
-}
+interface FormContent extends Content, HttpParameters, Iterator {}
diff --git a/src/Http/Content/MultipartFormContent.php b/src/Http/Content/MultipartFormContent.php
index 8888373..fd26956 100644
--- a/src/Http/Content/MultipartFormContent.php
+++ b/src/Http/Content/MultipartFormContent.php
@@ -1,21 +1,25 @@
 <?php
 // MultipartFormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-14
+// Updated: 2025-03-15
 
 namespace Index\Http\Content;
 
+use Iterator;
 use RuntimeException;
+use Index\Http\HttpParametersCommon;
 use Index\Http\Content\Multipart\{FileMultipartFormData,MultipartFormData,ValueMultipartFormData};
 use Index\Http\Streams\{ScopedStream,Stream,StreamBuffer};
 use Psr\Http\Message\StreamInterface;
 
 /**
  * Implements multipart/form-data.
- *
- * @implements FormContent<MultipartFormData>
+
+ * @implements Iterator<string, ?list<MultipartFormData>>
  */
-class MultipartFormContent implements FormContent {
+class MultipartFormContent implements FormContent, Iterator {
+    use HttpParametersCommon;
+
     /** @var string[] */
     private array $keys;
 
@@ -59,19 +63,6 @@ class MultipartFormContent implements FormContent {
         ++$this->position;
     }
 
-    public function hasParam(string $name): bool {
-        return isset($this->params[$name]);
-    }
-
-    public function getParam(
-        string $name,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        return $this->getParamAt($name, 0, $filter, $options, $default);
-    }
-
     /**
      * Retrieves value of the form field, including additional data.
      *
@@ -82,20 +73,6 @@ class MultipartFormContent implements FormContent {
         return $this->getParamDataAt($name, 0);
     }
 
-    public function getParamAt(
-        string $name,
-        int $index,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        if(!isset($this->params[$name]) || !isset($this->params[$name][$index]))
-            return $default;
-
-        $value = filter_var($this->params[$name][$index], $filter, $options);
-        return is_scalar($value) ? $value : $default;
-    }
-
     /**
      * Retrieves value of the form field, including additional data.
      *
@@ -108,10 +85,6 @@ class MultipartFormContent implements FormContent {
             ? $this->params[$name][$index] : null;
     }
 
-    public function getParamCount(string $name): int {
-        return isset($this->params[$name]) ? count($this->params[$name]) : 0;
-    }
-
     /**
      * Parses multipart form data in a stream.
      *
diff --git a/src/Http/Content/UrlEncodedFormContent.php b/src/Http/Content/UrlEncodedFormContent.php
index 0bf2525..87aa398 100644
--- a/src/Http/Content/UrlEncodedFormContent.php
+++ b/src/Http/Content/UrlEncodedFormContent.php
@@ -1,19 +1,19 @@
 <?php
 // UrlEncodedFormContent.php
 // Created: 2025-03-12
-// Updated: 2025-03-14
+// Updated: 2025-03-15
 
 namespace Index\Http\Content;
 
-use Index\Http\HttpUri;
+use Index\Http\{HttpParametersCommon,HttpUri};
 use Psr\Http\Message\StreamInterface;
 
 /**
  * Implements application/x-www-form-urlencoded.
- *
- * @implements FormContent<string>
  */
 class UrlEncodedFormContent implements FormContent {
+    use HttpParametersCommon;
+
     /** @var string[] */
     private array $keys;
 
@@ -55,37 +55,6 @@ class UrlEncodedFormContent implements FormContent {
         ++$this->position;
     }
 
-    public function hasParam(string $name): bool {
-        return isset($this->params[$name]);
-    }
-
-    public function getParam(
-        string $name,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        return $this->getParamAt($name, 0, $filter, $options, $default);
-    }
-
-    public function getParamAt(
-        string $name,
-        int $index,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        if(!isset($this->params[$name]) || !isset($this->params[$name][$index]))
-            return $default;
-
-        $value = filter_var($this->params[$name][$index], $filter, $options);
-        return is_scalar($value) ? $value : $default;
-    }
-
-    public function getParamCount(string $name): int {
-        return isset($this->params[$name]) ? count($this->params[$name]) : 0;
-    }
-
     /**
      * Parses URL encoded form params in a stream.
      *
diff --git a/src/Http/HttpParameters.php b/src/Http/HttpParameters.php
new file mode 100644
index 0000000..76ed156
--- /dev/null
+++ b/src/Http/HttpParameters.php
@@ -0,0 +1,87 @@
+<?php
+// HttpParameters.php
+// Created: 2025-03-15
+// Updated: 2025-03-15
+
+namespace Index\Http;
+
+/**
+ * Common definition for HTTP parameters.
+ */
+interface HttpParameters {
+    /**
+     * Checks if a parameter is present.
+     *
+     * @param string $name Name of the parameter.
+     * @return bool
+     */
+    public function hasParam(string $name): bool;
+
+    /**
+     * Retrieves how many parameters of the same name appear in the HTTP request query field.
+     *
+     * @param string $name Name of the request query field.
+     * @return int<0, max> Amount of values.
+     */
+    public function getParamCount(string $name): int;
+
+    /**
+     * Retrieves a parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param ?string $default Default value to fall back on.
+     * @return ?string Value of the parameter, $default if not present.
+     */
+    public function getParam(
+        string $name,
+        ?string $default = null,
+    ): ?string;
+
+    /**
+     * Retrieves a parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $index Index of the parameter value.
+     * @param ?string $default Default value to fall back on.
+     * @return ?string Value of the parameter, null if not present.
+     */
+    public function getParamAt(
+        string $name,
+        int $index,
+        ?string $default = null,
+    ): ?string;
+
+    /**
+     * Retrieves a filtered parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $filter A PHP filter extension filter constant.
+     * @param mixed[]|int $options Options for the PHP filter.
+     * @param mixed $default Default value to fall back on.
+     * @return mixed Value of the parameter, default value if not present.
+     */
+    public function getFilteredParam(
+        string $name,
+        int $filter = FILTER_DEFAULT,
+        array|int $options = 0,
+        mixed $default = null,
+    ): mixed;
+
+    /**
+     * Retrieves a filtered parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $index Index of the parameter value.
+     * @param int $filter A PHP filter extension filter constant.
+     * @param mixed[]|int $options Options for the PHP filter.
+     * @param mixed $default Default value to fall back on.
+     * @return mixed Value of the parameter, default value if not present.
+     */
+    public function getFilteredParamAt(
+        string $name,
+        int $index,
+        int $filter = FILTER_DEFAULT,
+        array|int $options = 0,
+        mixed $default = null,
+    ): mixed;
+}
diff --git a/src/Http/HttpParametersCommon.php b/src/Http/HttpParametersCommon.php
new file mode 100644
index 0000000..ef23a7f
--- /dev/null
+++ b/src/Http/HttpParametersCommon.php
@@ -0,0 +1,102 @@
+<?php
+// HttpParametersCommon.php
+// Created: 2025-03-15
+// Updated: 2025-03-15
+
+namespace Index\Http;
+
+/**
+ * Common implementation for HTTP parameters.
+ */
+trait HttpParametersCommon {
+    /**
+     * Checks if a parameter is present.
+     *
+     * @param string $name Name of the parameter.
+     * @return bool
+     */
+    public function hasParam(string $name): bool {
+        return isset($this->params[$name]);
+    }
+
+    /**
+     * Retrieves how many parameters of the same name appear in the HTTP request query field.
+     *
+     * @param string $name Name of the request query field.
+     * @return int<0, max> Amount of values.
+     */
+    public function getParamCount(string $name): int {
+        return isset($this->params[$name]) ? count($this->params[$name]) : 0;
+    }
+
+    /**
+     * Retrieves a parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param ?string $default Default value to fall back on.
+     * @return ?string Value of the parameter, $default if not present.
+     */
+    public function getParam(
+        string $name,
+        ?string $default = null,
+    ): ?string {
+        return $this->getParamAt($name, 0, $default);
+    }
+
+    /**
+     * Retrieves a parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $index Index of the parameter value.
+     * @param ?string $default Default value to fall back on.
+     * @return ?string Value of the parameter, null if not present.
+     */
+    public function getParamAt(
+        string $name,
+        int $index,
+        ?string $default = null,
+    ): ?string {
+        return isset($this->params[$name]) && isset($this->params[$name][$index])
+            ? $this->params[$name][$index] : $default;
+    }
+
+    /**
+     * Retrieves a filtered parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $filter A PHP filter extension filter constant.
+     * @param mixed[]|int $options Options for the PHP filter.
+     * @param mixed $default Default value to fall back on.
+     * @return mixed Value of the parameter, default value if not present.
+     */
+    public function getFilteredParam(
+        string $name,
+        int $filter = FILTER_DEFAULT,
+        array|int $options = 0,
+        mixed $default = null,
+    ): mixed {
+        return $this->getFilteredParamAt($name, 0, $filter, $options, $default);
+    }
+
+    /**
+     * Retrieves a filtered parameter, or a default value if it is not present.
+     *
+     * @param string $name Name of the parameter.
+     * @param int $index Index of the parameter value.
+     * @param int $filter A PHP filter extension filter constant.
+     * @param mixed[]|int $options Options for the PHP filter.
+     * @param mixed $default Default value to fall back on.
+     * @return mixed Value of the parameter, default value if not present.
+     */
+    public function getFilteredParamAt(
+        string $name,
+        int $index,
+        int $filter = FILTER_DEFAULT,
+        array|int $options = 0,
+        mixed $default = null,
+    ): mixed {
+        return isset($this->params[$name]) && isset($this->params[$name][$index])
+            ? filter_var($this->params[$name][$index], $filter, $options)
+            : $default;
+    }
+}
diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php
index 9c1390a..33210db 100644
--- a/src/Http/HttpRequest.php
+++ b/src/Http/HttpRequest.php
@@ -1,7 +1,7 @@
 <?php
 // HttpRequest.php
 // Created: 2022-02-08
-// Updated: 2025-03-12
+// Updated: 2025-03-15
 
 namespace Index\Http;
 
@@ -9,14 +9,16 @@ use RuntimeException;
 use InvalidArgumentException;
 use Index\MediaType;
 use Index\Http\Content\{MultipartFormContent,UrlEncodedFormContent};
-use Index\Http\Streams\Stream;
+use Index\Http\Streams\{NullStream,Stream};
 use Index\Json\JsonHttpContent;
 use Psr\Http\Message\{ServerRequestInterface,StreamInterface,UploadedFileInterface,UriInterface};
 
 /**
  * Represents a HTTP request message.
  */
-class HttpRequest extends HttpMessage implements ServerRequestInterface {
+class HttpRequest extends HttpMessage implements ServerRequestInterface, HttpParameters {
+    use HttpParametersCommon;
+
     /** Name of attribute containing the country code value. */
     public const string ATTR_COUNTRY_CODE = 'countryCode';
 
@@ -277,68 +279,6 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
             : http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
     }
 
-    /**
-     * Checks if a query field is present.
-     *
-     * @param string $name Name of the query field.
-     * @return bool true if the field is present, false if not.
-     */
-    public function hasParam(string $name): bool {
-        return isset($this->params[$name]);
-    }
-
-    /**
-     * Retrieves an HTTP request query field, or null if it is not present.
-     *
-     * @param string $name Name of the request query field.
-     * @param int $filter A PHP filter extension filter constant.
-     * @param mixed[]|int $options Options for the PHP filter.
-     * @param mixed $default Default value to fall back on.
-     * @return mixed Value of the query field, null if not present.
-     */
-    public function getParam(
-        string $name,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        return $this->getParamAt($name, 0, $filter, $options, $default);
-    }
-
-    /**
-     * Retrieves an HTTP request query field, or null if it is not present.
-     *
-     * @param string $name Name of the request query field.
-     * @param int $index Index of the parameter value.
-     * @param int $filter A PHP filter extension filter constant.
-     * @param mixed[]|int $options Options for the PHP filter.
-     * @param mixed $default Default value to fall back on.
-     * @return mixed Value of the query field, null if not present.
-     */
-    public function getParamAt(
-        string $name,
-        int $index,
-        int $filter = FILTER_DEFAULT,
-        array|int $options = 0,
-        mixed $default = null,
-    ): mixed {
-        if(!isset($this->params[$name]) || !isset($this->params[$name][$index]))
-            return $default;
-
-        $value = filter_var($this->params[$name][$index], $filter, $options);
-        return is_scalar($value) ? $value : $default;
-    }
-
-    /**
-     * Retrieves how many parameters of the same name appear in the HTTP request query field.
-     *
-     * @param string $name Name of the request query field.
-     * @return int<0, max> Amount of values.
-     */
-    public function getParamCount(string $name): int {
-        return isset($this->params[$name]) ? count($this->params[$name]) : 0;
-    }
-
     /**
      * @return array<string, UploadedFileInterface[]> An array tree of UploadedFileInterface instances; an empty array MUST be returned if no data is present.
      */
@@ -390,6 +330,13 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
         return $this->with(parsedBody: $data);
     }
 
+    /**
+     * Casts a PSR-7 ServerRequestInterface to an Index HttpRequest.
+     * If $request was already HttpRequest, it will be returned verbatim.
+     *
+     * @param ServerRequestInterface $request
+     * @return HttpRequest
+     */
     public static function castRequest(ServerRequestInterface $request): HttpRequest {
         if($request instanceof HttpRequest)
             return $request;
@@ -438,6 +385,58 @@ class HttpRequest extends HttpMessage implements ServerRequestInterface {
         );
     }
 
+    /**
+     * Creates a HttpRequest with some given parameters, exists for testing.
+     *
+     * @param string $method Request method.
+     * @param string $path Request target.
+     * @param array<string, list<string>> $headers Request headers.
+     * @return HttpRequest
+     */
+    public static function createRequestWithoutBody(
+        string $method,
+        string $path,
+        array $headers = [],
+    ): HttpRequest {
+        return new HttpRequest(
+            '1.1',
+            $headers,
+            NullStream::instance(),
+            [],
+            $method,
+            HttpUri::createUri($path),
+            [],
+            []
+        );
+    }
+
+    /**
+     * Creates a HttpRequest with some given parameters, exists for testing.
+     *
+     * @param string $method Request method.
+     * @param string $path Request target.
+     * @param array<string, list<string>> $headers Request headers.
+     * @param StreamInterface $body Request body.
+     * @return HttpRequest
+     */
+    public static function createRequestWithBody(
+        string $method,
+        string $path,
+        array $headers,
+        StreamInterface $body,
+    ): HttpRequest {
+        return new HttpRequest(
+            '1.1',
+            $headers,
+            $body,
+            [],
+            $method,
+            HttpUri::createUri($path),
+            [],
+            []
+        );
+    }
+
     /**
      * Parses a Cookie header string.
      *
diff --git a/src/Http/Routing/Router.php b/src/Http/Routing/Router.php
index ad1ef60..3dae8a4 100644
--- a/src/Http/Routing/Router.php
+++ b/src/Http/Routing/Router.php
@@ -1,7 +1,7 @@
 <?php
 // Router.php
 // Created: 2024-03-28
-// Updated: 2025-03-12
+// Updated: 2025-03-15
 
 namespace Index\Http\Routing;
 
@@ -47,9 +47,15 @@ class Router implements RequestHandlerInterface {
 
     /**
      * @param ErrorHandler|'html'|'plain' $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
+     * @param bool $registerDefaultProcessors Whether the default processors should be registered.
      */
-    public function __construct(ErrorHandler|string $errorHandler = 'html') {
+    public function __construct(
+        ErrorHandler|string $errorHandler = 'html',
+        bool $registerDefaultProcessors = true
+    ) {
         $this->setErrorHandler($errorHandler);
+        if($registerDefaultProcessors)
+            $this->register(new RouterProcessors);
     }
 
     /**
diff --git a/src/Http/Routing/RouterProcessors.php b/src/Http/Routing/RouterProcessors.php
new file mode 100644
index 0000000..faadb6f
--- /dev/null
+++ b/src/Http/Routing/RouterProcessors.php
@@ -0,0 +1,68 @@
+<?php
+// RouterProcessors.php
+// Created: 2025-03-15
+// Updated: 2025-03-15
+
+namespace Index\Http\Routing;
+
+use RuntimeException;
+use Index\MediaType;
+use Index\Http\Content\{MultipartFormContent,UrlEncodedFormContent};
+use Index\Http\Routing\Processors\{Postprocessor,Preprocessor};
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Implements some standard route handlers, like parsing form data and outputting json.
+ */
+class RouterProcessors implements RouteHandler {
+    use RouteHandlerCommon;
+
+    private function handlePreInputUrlencoded(HandlerContext $context, RequestInterface $request): bool {
+        $contentType = MediaType::parse($request->getHeaderLine('Content-Type'));
+        if(!$contentType->equals('application/x-www-form-urlencoded'))
+            return false;
+
+        $context->deps->register(UrlEncodedFormContent::parseStream($request->getBody()));
+
+        return true;
+    }
+
+    #[Preprocessor('input:urlencoded')]
+    public function preInputUrlencoded(HandlerContext $context, RequestInterface $request, bool $required = true): void {
+        if(!$this->handlePreInputUrlencoded($context, $request) && $required) {
+            $context->response->statusCode = 400;
+            $context->response->reasonPhrase = '';
+            $context->halt();
+        }
+    }
+
+    private function handlePreInputMultipart(HandlerContext $context, RequestInterface $request): bool {
+        $contentType = MediaType::parse($request->getHeaderLine('Content-Type'));
+        if(!$contentType->equals('multipart/form-data'))
+            return false;
+
+        $boundary = $contentType->boundary;
+        if(empty($boundary))
+            return false;
+
+        $context->deps->register(MultipartFormContent::parseStream($request->getBody(), $boundary));
+
+        return true;
+    }
+
+    #[Preprocessor('input:multipart')]
+    public function preInputMultipart(HandlerContext $context, RequestInterface $request, bool $required = true): void {
+        try {
+            $result = $this->handlePreInputMultipart($context, $request);
+        } catch(RuntimeException $ex) {
+            $result = false;
+            $required = true;
+        }
+
+        if(!$result && $required) {
+            $context->response->statusCode = 400;
+            $context->response->reasonPhrase = '';
+            $context->halt();
+        }
+    }
+}
diff --git a/tests/HttpFormContentTest.php b/tests/HttpFormContentTest.php
index 870b004..48dda4e 100644
--- a/tests/HttpFormContentTest.php
+++ b/tests/HttpFormContentTest.php
@@ -1,7 +1,7 @@
 <?php
 // HttpFormContentTest.php
 // Created: 2025-03-12
-// Updated: 2025-03-14
+// Updated: 2025-03-15
 
 declare(strict_types=1);
 
@@ -63,7 +63,7 @@ final class HttpFormContentTest extends TestCase {
         $this->assertFalse($form->hasParam('never_has_this'));
         $this->assertEquals(0, $form->getParamCount('never_has_this_either'));
         $this->assertNull($form->getParam('this_is_not_there_either'));
-        $this->assertEquals(0401, $form->getParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
+        $this->assertEquals(0401, $form->getFilteredParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
 
         foreach($expected as $key => $values) {
             $this->assertTrue($form->hasParam($key));
@@ -90,7 +90,7 @@ final class HttpFormContentTest extends TestCase {
         $this->assertEquals(0, $form->getParamCount('never_has_this_either'));
         $this->assertNull($form->getParam('this_is_not_there_either'));
         $this->assertNull($form->getParamData('this_is_not_there_either'));
-        $this->assertEquals(0401, $form->getParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
+        $this->assertEquals(0401, $form->getFilteredParamAt('abusing_octal_notation_for_teto_reference', 3510, default: 0401));
         $this->assertEquals(null, $form->getParamDataAt('abusing_octal_notation_for_teto_reference', 3510));
 
         $this->assertTrue($form->hasParam('meow'));
diff --git a/tests/RouterTest.php b/tests/RouterTest.php
index 1bffa55..660d1d2 100644
--- a/tests/RouterTest.php
+++ b/tests/RouterTest.php
@@ -1,18 +1,19 @@
 <?php
 // RouterTest.php
 // Created: 2022-01-20
-// Updated: 2025-03-12
+// Updated: 2025-03-15
 
 declare(strict_types=1);
 
 use PHPUnit\Framework\TestCase;
 use PHPUnit\Framework\Attributes\CoversClass;
 use Index\Http\{HttpHeaders,HttpResponseBuilder,HttpRequest,HttpUri};
+use Index\Http\Content\{FormContent,MultipartFormContent,UrlEncodedFormContent};
 use Index\Http\Routing\{HandlerContext,RouteHandler,RouteHandlerCommon,Router};
 use Index\Http\Routing\Filters\{FilterInfo,PrefixFilter};
 use Index\Http\Routing\Processors\{After,Before,Postprocessor,Preprocessor,ProcessorInfo};
 use Index\Http\Routing\Routes\{ExactRoute,PatternRoute,RouteInfo};
-use Index\Http\Streams\{NullStream,Stream};
+use Index\Http\Streams\Stream;
 
 /**
  * This test isn't super representative of the current functionality
@@ -37,8 +38,8 @@ final class RouterTest extends TestCase {
         $router1->route(RouteInfo::exact('PUT', '/', fn() => 'put'));
         $router1->route(RouteInfo::exact('CUSTOM', '/', fn() => 'wacky'));
 
-        $this->assertEquals('get', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
-        $this->assertEquals('wacky', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'CUSTOM', HttpUri::createUri('/'), [], []))->getBody());
+        $this->assertEquals('get', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
+        $this->assertEquals('wacky', (string)$router1->handle(HttpRequest::createRequestWithoutBody('CUSTOM', '/'))->getBody());
 
         $router1->filter(FilterInfo::prefix('/', function() { /* this one intentionally does nothing */ }));
 
@@ -52,7 +53,7 @@ final class RouterTest extends TestCase {
         $router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)$#uD', fn(string $user) => $user));
         $router1->route(RouteInfo::pattern('GET', '#^/user/([A-Za-z0-9]+)/below$#uD', fn(string $user) => 'below ' . $user));
 
-        $this->assertEquals('warioware below static', (string)$router1->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/user/static/below'), [], []))->getBody());
+        $this->assertEquals('warioware below static', (string)$router1->handle(HttpRequest::createRequestWithoutBody('GET', '/user/static/below'))->getBody());
 
         $router2 = new Router;
         $router2->filter(FilterInfo::prefix('/', fn() => 'meow'));
@@ -60,7 +61,7 @@ final class RouterTest extends TestCase {
         $router2->route(RouteInfo::exact('GET', '/contact', fn() => 'contact page'));
         $router2->route(RouteInfo::exact('GET', '/25252', fn() => 'numeric test'));
 
-        $this->assertEquals('meow', (string)$router2->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/rules'), [], []))->getBody());
+        $this->assertEquals('meow', (string)$router2->handle(HttpRequest::createRequestWithoutBody('GET', '/rules'))->getBody());
     }
 
     public function testAttribute(): void {
@@ -116,13 +117,13 @@ final class RouterTest extends TestCase {
 
         $router->register($handler);
 
-        $this->assertEquals('index', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
-        $this->assertEquals('avatar', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/avatar'), [], []))->getBody());
-        $this->assertEquals('static', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'PUT', HttpUri::createUri('/static'), [], []))->getBody());
-        $this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/meow'), [], []))->getBody());
-        $this->assertEquals('meow', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'POST', HttpUri::createUri('/meow'), [], []))->getBody());
-        $this->assertEquals('profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile/Cool134'), [], []))->getBody());
-        $this->assertEquals('still the profile of Cool134', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/profile-but-raw/Cool134'), [], []))->getBody());
+        $this->assertEquals('index', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
+        $this->assertEquals('avatar', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/avatar'))->getBody());
+        $this->assertEquals('static', (string)$router->handle(HttpRequest::createRequestWithoutBody('PUT', '/static'))->getBody());
+        $this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/meow'))->getBody());
+        $this->assertEquals('meow', (string)$router->handle(HttpRequest::createRequestWithoutBody('POST', '/meow'))->getBody());
+        $this->assertEquals('profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile/Cool134'))->getBody());
+        $this->assertEquals('still the profile of Cool134', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/profile-but-raw/Cool134'))->getBody());
     }
 
     public function testEEPROMSituation(): void {
@@ -131,8 +132,8 @@ final class RouterTest extends TestCase {
         $router->route(RouteInfo::pattern('DELETE', '#^/uploads/([A-Za-z0-9\-_]+)$#uD', fn(string $id) => "Delete {$id}"));
 
         // make sure both GET and DELETE are able to execute with a different pattern
-        $this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'), [], []))->getBody());
-        $this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'DELETE', HttpUri::createUri('/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'), [], []))->getBody());
+        $this->assertEquals('Get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/uploads/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'))->getBody());
+        $this->assertEquals('Delete BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', (string)$router->handle(HttpRequest::createRequestWithoutBody('DELETE', '/uploads/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'))->getBody());
     }
 
     public function testFilterInterceptionOnRoot(): void {
@@ -141,9 +142,9 @@ final class RouterTest extends TestCase {
         $router->route(RouteInfo::exact('GET', '/', fn() => 'unexpected'));
         $router->route(RouteInfo::exact('GET', '/test', fn() => 'also unexpected'));
 
-        $this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/'), [], []))->getBody());
-        $this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/test'), [], []))->getBody());
-        $this->assertEquals('expected', (string)$router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/error'), [], []))->getBody());
+        $this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/'))->getBody());
+        $this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/test'))->getBody());
+        $this->assertEquals('expected', (string)$router->handle(HttpRequest::createRequestWithoutBody('GET', '/error'))->getBody());
     }
 
     public function testDefaultOptionsImplementation(): void {
@@ -152,12 +153,124 @@ final class RouterTest extends TestCase {
         $router->route(RouteInfo::exact('POST', '/test', fn() => 'post'));
         $router->route(RouteInfo::exact('PATCH', '/test', fn() => 'patch'));
 
-        $response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'OPTIONS', HttpUri::createUri('/test'), [], []));
+        $response = $router->handle(HttpRequest::createRequestWithoutBody('OPTIONS', '/test'));
         $this->assertEquals(204, $response->getStatusCode());
         $this->assertEquals('No Content', $response->getReasonPhrase());
         $this->assertEquals('GET, HEAD, OPTIONS, PATCH, POST', $response->getHeaderLine('Allow'));
     }
 
+    public function testDefaultProcessorsNoRegister(): void {
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage(sprintf('preprocessor "%s" was not found', 'input:urlencoded:required'));
+
+        $router = new Router(registerDefaultProcessors: false);
+        $router->route(RouteInfo::exact(
+            'GET', '/soap',
+            #[Before('input:urlencoded:required')]
+            function() {
+                return 'test';
+            },
+        ));
+        $router->handle(HttpRequest::createRequestWithoutBody('GET', '/soap'));
+    }
+
+    public function testDefaultProcessors(): void {
+        $router = new Router;
+        $router->register(new class implements RouteHandler {
+            use RouteHandlerCommon;
+
+            #[Before('input:urlencoded', required: false)]
+            #[Before('input:multipart', required: false)]
+            #[ExactRoute('POST', '/optional-form')]
+            public function formOptional(?FormContent $content = null): string {
+                if($content instanceof MultipartFormContent)
+                    return 'multipart:' . (string)$content->getParam('test');
+                if($content instanceof UrlEncodedFormContent)
+                    return 'urlencoded:' . (string)$content->getParam('test');
+                return 'none';
+            }
+
+            #[Before('input:multipart')]
+            #[ExactRoute('POST', '/required-multipart')]
+            public function multipartRequired(MultipartFormContent $content): string {
+                return (string)$content->getParam('test');
+            }
+
+            #[Before('input:urlencoded')]
+            #[ExactRoute('POST', '/required-urlencoded')]
+            public function urlencodedRequired(UrlEncodedFormContent $content): string {
+                return (string)$content->getParam('test');
+            }
+        });
+
+        $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/optional-form'));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('none', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', [], Stream::createStream('test=mewow')));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('none', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=mewow')));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('urlencoded:mewow', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/optional-form', ['Content-Type' => ['multipart/form-data; boundary="--soap12345"']], Stream::createStream(implode("\r\n", [
+            '----soap12345',
+            'Content-Disposition: form-data; name="test"',
+            '',
+            'wowof',
+            '----soap12345--',
+            '',
+        ]))));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('multipart:wowof', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-urlencoded'));
+        $this->assertEquals(400, $response->getStatusCode());
+
+        $response = $router->handle(HttpRequest::createRequestWithoutBody('POST', '/required-multipart'));
+        $this->assertEquals(400, $response->getStatusCode());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream('test=meow')));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('meow', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream(implode("\r\n", [
+            '----soap56789',
+            'Content-Disposition: form-data; name="test"',
+            '',
+            'woof',
+            '----soap56789--',
+            '',
+        ]))));
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->assertEquals('woof', (string)$response->getBody());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-urlencoded', ['Content-Type' => ['multipart/form-data; boundary="--soap56789"']], Stream::createStream('test=meow')));
+        $this->assertEquals(400, $response->getStatusCode());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['application/x-www-form-urlencoded']], Stream::createStream(implode("\r\n", [
+            '----soap56789',
+            'Content-Disposition: form-data; name="test"',
+            '',
+            'woof',
+            '----soap56789--',
+            '',
+        ]))));
+        $this->assertEquals(400, $response->getStatusCode());
+
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/required-multipart', ['Content-Type' => ['multipart/form-data']], Stream::createStream(implode("\r\n", [
+            '----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+            'Content-Disposition: form-data; name="test"',
+            '',
+            'the',
+            '----aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--',
+            '',
+        ]))));
+        $this->assertEquals(400, $response->getStatusCode());
+    }
+
     public function testProcessors(): void {
         $router = new Router;
 
@@ -216,15 +329,15 @@ final class RouterTest extends TestCase {
             }
         ));
 
-        $response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('mewow')), [], 'POST', HttpUri::createUri('/test'), [], []));
+        $response = $router->handle(HttpRequest::createRequestWithBody('POST', '/test', [], Stream::createStream(base64_encode('mewow'))));
         $this->assertEquals(200, $response->getStatusCode());
         $this->assertEquals("{\n    \"data\": \"mewow\"\n}", (string)$response->getBody());
 
-        $response = $router->handle(new HttpRequest('1.1', [], Stream::createStream(base64_encode('soap')), [], 'PUT', HttpUri::createUri('/alternate'), [], []));
+        $response = $router->handle(HttpRequest::createRequestWithBody('PUT', '/alternate', [], Stream::createStream(base64_encode('soap'))));
         $this->assertEquals(200, $response->getStatusCode());
         $this->assertEquals("{\n    \"path\": \"/alternate\",\n    \"data\": \"soap\"\n}", (string)$response->getBody());
 
-        $response = $router->handle(new HttpRequest('1.1', [], NullStream::instance(), [], 'GET', HttpUri::createUri('/filtered'), [], []));
+        $response = $router->handle(HttpRequest::createRequestWithoutBody('GET', '/filtered'));
         $this->assertEquals(403, $response->getStatusCode());
         $this->assertEquals('it can work like a filter too', (string)$response->getBody());
     }