diff --git a/composer.json b/composer.json
index 94ce78c..9f7aad0 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
 {
     "require": {
-        "flashwave/index": "^0.2410",
+        "flashwave/index": "^0.2503",
         "sentry/sdk": "^4.0"
     },
     "autoload": {
diff --git a/composer.lock b/composer.lock
index 66a1580..99c7ba9 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,25 +4,27 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "023574a93f076ef7a987151cdbfdb33e",
+    "content-hash": "ca6ae2c3d6229791acf93914e055bf41",
     "packages": [
         {
             "name": "flashwave/index",
-            "version": "v0.2410.830205",
+            "version": "v0.2503.201929",
             "source": {
                 "type": "git",
                 "url": "https://patchii.net/flash/index.git",
-                "reference": "416c716b2efab9619d14be02a20cd6540e18a83f"
+                "reference": "1ba9e8fa34fbd30c84c23b2a9b6beb2152ca54f0"
             },
             "require": {
                 "ext-mbstring": "*",
-                "php": ">=8.3",
-                "twig/html-extra": "^3.13",
-                "twig/twig": "^3.14"
+                "php": ">=8.4",
+                "psr/http-message": "^2.0",
+                "psr/http-server-handler": "^1.0",
+                "twig/html-extra": "^3.20",
+                "twig/twig": "^3.20"
             },
             "require-dev": {
-                "phpstan/phpstan": "^2.0",
-                "phpunit/phpunit": "^11.4"
+                "phpstan/phpstan": "^2.1",
+                "phpunit/phpunit": "^12.0"
             },
             "suggest": {
                 "ext-memcache": "Support for the Index\\Cache\\Memcached namespace (only if you can't use ext-memcached for some reason).",
@@ -59,7 +61,7 @@
             ],
             "description": "Composer package for the common library for my projects.",
             "homepage": "https://railgun.sh/index",
-            "time": "2024-12-22T02:05:46+00:00"
+            "time": "2025-03-20T19:29:51+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
@@ -179,16 +181,16 @@
         },
         {
             "name": "jean85/pretty-package-versions",
-            "version": "2.1.0",
+            "version": "2.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Jean85/pretty-package-versions.git",
-                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10"
+                "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
-                "reference": "3c4e5f62ba8d7de1734312e4fff32f67a8daaf10",
+                "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
+                "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
                 "shasum": ""
             },
             "require": {
@@ -198,8 +200,9 @@
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^3.2",
                 "jean85/composer-provided-replaced-stub-package": "^1.0",
-                "phpstan/phpstan": "^1.4",
+                "phpstan/phpstan": "^2.0",
                 "phpunit/phpunit": "^7.5|^8.5|^9.6",
+                "rector/rector": "^2.0",
                 "vimeo/psalm": "^4.3 || ^5.0"
             },
             "type": "library",
@@ -232,9 +235,9 @@
             ],
             "support": {
                 "issues": "https://github.com/Jean85/pretty-package-versions/issues",
-                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.0"
+                "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
             },
-            "time": "2024-11-18T16:19:46+00:00"
+            "time": "2025-03-19T14:43:43+00:00"
         },
         {
             "name": "psr/http-factory",
@@ -344,6 +347,62 @@
             },
             "time": "2023-04-04T09:54:51+00:00"
         },
+        {
+            "name": "psr/http-server-handler",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-server-handler.git",
+                "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
+                "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "psr/http-message": "^1.0 || ^2.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Server\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP server-side request handler",
+            "keywords": [
+                "handler",
+                "http",
+                "http-interop",
+                "psr",
+                "psr-15",
+                "psr-7",
+                "request",
+                "response",
+                "server"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
+            },
+            "time": "2023-04-10T20:06:20+00:00"
+        },
         {
             "name": "psr/log",
             "version": "3.0.2",
@@ -651,16 +710,16 @@
         },
         {
             "name": "symfony/mime",
-            "version": "v7.2.1",
+            "version": "v7.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-                "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283"
+                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283",
-                "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
+                "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
                 "shasum": ""
             },
             "require": {
@@ -715,7 +774,7 @@
                 "mime-type"
             ],
             "support": {
-                "source": "https://github.com/symfony/mime/tree/v7.2.1"
+                "source": "https://github.com/symfony/mime/tree/v7.2.4"
             },
             "funding": [
                 {
@@ -731,7 +790,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-07T08:50:44+00:00"
+            "time": "2025-02-19T08:51:20+00:00"
         },
         {
             "name": "symfony/options-resolver",
@@ -1123,98 +1182,22 @@
             ],
             "time": "2024-09-09T11:45:10+00:00"
         },
-        {
-            "name": "symfony/polyfill-php81",
-            "version": "v1.31.0",
-            "source": {
-                "type": "git",
-                "url": "https://github.com/symfony/polyfill-php81.git",
-                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
-            },
-            "dist": {
-                "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
-                "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
-                "shasum": ""
-            },
-            "require": {
-                "php": ">=7.2"
-            },
-            "type": "library",
-            "extra": {
-                "thanks": {
-                    "url": "https://github.com/symfony/polyfill",
-                    "name": "symfony/polyfill"
-                }
-            },
-            "autoload": {
-                "files": [
-                    "bootstrap.php"
-                ],
-                "psr-4": {
-                    "Symfony\\Polyfill\\Php81\\": ""
-                },
-                "classmap": [
-                    "Resources/stubs"
-                ]
-            },
-            "notification-url": "https://packagist.org/downloads/",
-            "license": [
-                "MIT"
-            ],
-            "authors": [
-                {
-                    "name": "Nicolas Grekas",
-                    "email": "p@tchwork.com"
-                },
-                {
-                    "name": "Symfony Community",
-                    "homepage": "https://symfony.com/contributors"
-                }
-            ],
-            "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
-            "homepage": "https://symfony.com",
-            "keywords": [
-                "compatibility",
-                "polyfill",
-                "portable",
-                "shim"
-            ],
-            "support": {
-                "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
-            },
-            "funding": [
-                {
-                    "url": "https://symfony.com/sponsor",
-                    "type": "custom"
-                },
-                {
-                    "url": "https://github.com/fabpot",
-                    "type": "github"
-                },
-                {
-                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
-                    "type": "tidelift"
-                }
-            ],
-            "time": "2024-09-09T11:45:10+00:00"
-        },
         {
             "name": "twig/html-extra",
-            "version": "v3.18.0",
+            "version": "v3.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/html-extra.git",
-                "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a"
+                "reference": "f7d54d4de1b64182af745cfb66777f699b599734"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/c63b28e192c1b7c15bb60f81d2e48b140846239a",
-                "reference": "c63b28e192c1b7c15bb60f81d2e48b140846239a",
+                "url": "https://api.github.com/repos/twigphp/html-extra/zipball/f7d54d4de1b64182af745cfb66777f699b599734",
+                "reference": "f7d54d4de1b64182af745cfb66777f699b599734",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.0.2",
+                "php": ">=8.1.0",
                 "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/mime": "^5.4|^6.4|^7.0",
                 "twig/twig": "^3.13|^4.0"
@@ -1253,7 +1236,7 @@
                 "twig"
             ],
             "support": {
-                "source": "https://github.com/twigphp/html-extra/tree/v3.18.0"
+                "source": "https://github.com/twigphp/html-extra/tree/v3.20.0"
             },
             "funding": [
                 {
@@ -1265,28 +1248,27 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-29T10:29:59+00:00"
+            "time": "2025-01-31T20:45:36+00:00"
         },
         {
             "name": "twig/twig",
-            "version": "v3.18.0",
+            "version": "v3.20.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/twigphp/Twig.git",
-                "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50"
+                "reference": "3468920399451a384bef53cf7996965f7cd40183"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/twigphp/Twig/zipball/acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
-                "reference": "acffa88cc2b40dbe42eaf3a5025d6c0d4600cc50",
+                "url": "https://api.github.com/repos/twigphp/Twig/zipball/3468920399451a384bef53cf7996965f7cd40183",
+                "reference": "3468920399451a384bef53cf7996965f7cd40183",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.0.2",
+                "php": ">=8.1.0",
                 "symfony/deprecation-contracts": "^2.5|^3",
                 "symfony/polyfill-ctype": "^1.8",
-                "symfony/polyfill-mbstring": "^1.3",
-                "symfony/polyfill-php81": "^1.29"
+                "symfony/polyfill-mbstring": "^1.3"
             },
             "require-dev": {
                 "phpstan/phpstan": "^2.0",
@@ -1333,7 +1315,7 @@
             ],
             "support": {
                 "issues": "https://github.com/twigphp/Twig/issues",
-                "source": "https://github.com/twigphp/Twig/tree/v3.18.0"
+                "source": "https://github.com/twigphp/Twig/tree/v3.20.0"
             },
             "funding": [
                 {
@@ -1345,7 +1327,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-12-29T10:51:50+00:00"
+            "time": "2025-02-13T08:34:43+00:00"
         }
     ],
     "packages-dev": [],
diff --git a/src/Apis/v1_0.php b/src/Apis/v1_0.php
index bf75193..6e0ab52 100644
--- a/src/Apis/v1_0.php
+++ b/src/Apis/v1_0.php
@@ -17,11 +17,15 @@ use Uiharu\Lookup\NicoNicoLookupResult;
 use Index\MediaType;
 use Index\Cache\CacheProvider;
 use Index\Colour\{Colour,ColourRgb};
-use Index\Http\Routing\{HttpGet,HttpPost,RouteHandlerTrait};
+use Index\Http\{HttpRequest,HttpResponseBuilder};
+use Index\Http\Routing\RouteHandlerCommon;
+use Index\Http\Routing\AccessControl\AccessControl;
+use Index\Http\Routing\Routes\ExactRoute;
+use Index\Http\Streams\Stream;
 use Index\Performance\Stopwatch;
 
 final class v1_0 implements \Uiharu\IApi {
-    use RouteHandlerTrait;
+    use RouteHandlerCommon;
 
     private UihContext $ctx;
     private CacheProvider $cache;
@@ -35,8 +39,9 @@ final class v1_0 implements \Uiharu\IApi {
         return !str_starts_with($url, '/v');
     }
 
-    #[HttpGet('/metadata/thumb/audio')]
-    public function getThumbAudio($response, $request) {
+    #[AccessControl]
+    #[ExactRoute('GET', '/metadata/thumb/audio')]
+    public function getThumbAudio(HttpResponseBuilder $response, HttpRequest $request) {
         $targetUrl = (string)$request->getParam('url');
 
         if(empty($targetUrl))
@@ -53,11 +58,12 @@ final class v1_0 implements \Uiharu\IApi {
 
         $response->setContentType('image/png');
         $response->setCacheControl('public', 'max-age=31536000', 'immutable');
-        $response->setContent(FFMPEG::grabFirstAudioCover($parsedUrl));
+        $response->body = Stream::createStream(FFMPEG::grabFirstAudioCover($parsedUrl));
     }
 
-    #[HttpGet('/metadata/thumb/video')]
-    public function getThumbVideo($response, $request) {
+    #[AccessControl]
+    #[ExactRoute('GET', '/metadata/thumb/video')]
+    public function getThumbVideo(HttpResponseBuilder $response, HttpRequest $request) {
         $targetUrl = (string)$request->getParam('url');
 
         if(empty($targetUrl))
@@ -74,55 +80,37 @@ final class v1_0 implements \Uiharu\IApi {
 
         $response->setContentType('image/png');
         $response->setCacheControl('public', 'max-age=31536000', 'immutable');
-        $response->setContent(FFMPEG::grabFirstVideoFrame($parsedUrl));
+        $response->body = Stream::createStream(FFMPEG::grabFirstVideoFrame($parsedUrl));
     }
 
-    #[HttpGet('/metadata')]
-    public function getMetadata($response, $request) {
-        if($request->getMethod() === 'HEAD') {
-            $response->setTypeJson();
-            return;
-        }
-
+    #[AccessControl]
+    #[ExactRoute('GET', '/metadata')]
+    public function getMetadata(HttpResponseBuilder $response, HttpRequest $request) {
         return $this->handleMetadata(
             $response, $request,
             (string)$request->getParam('url')
         );
     }
 
-    #[HttpPost('/metadata')]
-    public function postMetadata($response, $request) {
-        if(!$request->isStringContent())
-            return 400;
-
+    #[AccessControl]
+    #[ExactRoute('POST', '/metadata')]
+    public function postMetadata(HttpResponseBuilder $response, HttpRequest $request) {
         return $this->handleMetadata(
             $response, $request,
-            (string)$request->getContent()
+            (string)$request->getBody()
         );
     }
 
-    #[HttpGet('/metadata/batch')]
-    public function getMetadataBatch($response, $request) {
-        if($request->getMethod() === 'HEAD') {
-            $response->setTypeJson();
-            return;
-        }
-
-        return $this->handleMetadataBatch(
-            $response, $request,
-            $request->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
-        );
+    #[AccessControl]
+    #[ExactRoute('GET', '/metadata/batch')]
+    public function getMetadataBatch(): int {
+        return 410;
     }
 
-    #[HttpPost('/metadata/batch')]
-    public function postMetadataBatch($response, $request) {
-        if(!$request->isFormContent())
-            return 400;
-
-        return $this->handleMetadataBatch(
-            $response, $request,
-            $request->getContent()->getParam('url', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY)
-        );
+    #[AccessControl]
+    #[ExactRoute('POST', '/metadata/batch')]
+    public function postMetadataBatch(): int {
+        return 410;
     }
 
     private function metadataLookup(string $targetUrl) {
@@ -172,11 +160,11 @@ final class v1_0 implements \Uiharu\IApi {
                         $resp->content_type = MediaTypeExts::toV1($result->getMediaType());
                     if($result->hasColour()) {
                         $colour = $result->getColour();
-                        if($colour->getAlpha() < 1.0)
+                        if($colour->alpha < 1.0)
                             $colour = new ColourRgb(
-                                $colour->getRed(),
-                                $colour->getGreen(),
-                                $colour->getBlue()
+                                $colour->red,
+                                $colour->green,
+                                $colour->blue,
                             );
 
                         $resp->color = (string)$colour;
@@ -278,7 +266,7 @@ final class v1_0 implements \Uiharu\IApi {
                 }
 
             $sw->stop();
-            $resp->took = $sw->getElapsedTime() / 1000;
+            $resp->took = $sw->elapsedTime / 1000;
             $respJson = json_encode($resp);
 
             $this->cache->set($cacheKey, $respJson, 10 * 60);
@@ -290,7 +278,7 @@ final class v1_0 implements \Uiharu\IApi {
         return $resp;
     }
 
-    private function handleMetadata($response, $request, string $targetUrl) {
+    private function handleMetadata(HttpResponseBuilder $response, HttpRequest $request, string $targetUrl) {
         $result = $this->metadataLookup($targetUrl);
         if(is_int($result))
             return $result;
@@ -305,7 +293,7 @@ final class v1_0 implements \Uiharu\IApi {
         return $result;
     }
 
-    private function handleMetadataBatch($response, $request, array $urls) {
+    private function handleMetadataBatch(HttpResponseBuilder $response, HttpRequest $request, array $urls) {
         $sw = Stopwatch::startNew();
 
         if(count($urls) > 20)
diff --git a/src/MediaTypeExts.php b/src/MediaTypeExts.php
index 8322bb8..a132e71 100644
--- a/src/MediaTypeExts.php
+++ b/src/MediaTypeExts.php
@@ -7,14 +7,14 @@ final class MediaTypeExts {
     public static function toV1(MediaType $mediaType): array {
         $parts = [
             'string' => (string)$mediaType,
-            'type' => $mediaType->getCategory(),
-            'subtype' => $mediaType->getKind(),
+            'type' => $mediaType->category,
+            'subtype' => $mediaType->kind,
         ];
 
-        if(!empty($suffix = $mediaType->getSuffix()))
+        if(!empty($suffix = $mediaType->suffix))
             $parts['suffix'] = $suffix;
 
-        if(!empty($params = $mediaType->getParams()))
+        if(!empty($params = $mediaType->params))
             $parts['params'] = $params;
 
         return $parts;
diff --git a/src/UihContext.php b/src/UihContext.php
index 1d50af5..c1e108a 100644
--- a/src/UihContext.php
+++ b/src/UihContext.php
@@ -3,12 +3,15 @@ namespace Uiharu;
 
 use Index\Cache\CacheProvider;
 use Index\Config\Config;
-use Index\Http\Routing\HttpRouter;
+use Index\Http\HttpResponseBuilder;
+use Index\Http\Routing\{Router,RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\Filters\PrefixFilter;
+use Index\Http\Routing\Routes\ExactRoute;
 
 final class UihContext {
     private CacheProvider $cache;
     private Config $config;
-    private HttpRouter $router;
+    private Router $router;
     private array $apis = [];
     private array $lookups = [];
 
@@ -21,52 +24,28 @@ final class UihContext {
         return $this->cache;
     }
 
-    public function getRouter(): HttpRouter {
+    public function getRouter(): Router {
         return $this->router;
     }
 
-    public function isOriginAllowed(string $origin): bool {
-        $origin = mb_strtolower(parse_url($origin, PHP_URL_HOST));
-
-        if($origin === $_SERVER['HTTP_HOST'])
-            return true;
-
-        $allowed = $this->config->getArray('cors:origins');
-        if(empty($allowed))
-            return true;
-
-        return in_array($origin, $allowed);
-    }
-
     public function setupHttp(): void {
-        $this->router = new HttpRouter;
-        $this->router->use('/', function($response) {
-            $response->setPoweredBy('Uiharu');
-        });
+        $this->router = new Router(
+            accessControlHandler: new UiharuAccessControlHandler($this->config->getArray('cors:origins')),
+        );
+        $this->router->register(new class implements RouteHandler {
+            use RouteHandlerCommon;
 
-        $this->router->use('/', function($response, $request) {
-            $origin = $request->getHeaderLine('Origin');
-
-            if(!empty($origin)) {
-                if(!$this->isOriginAllowed($origin))
-                    return 403;
-
-                $response->setHeader('Access-Control-Allow-Origin', $origin);
-                $response->setHeader('Vary', 'Origin');
+            #[PrefixFilter('/')]
+            public function filterPoweredBy(HttpResponseBuilder $response): void {
+                $response->setPoweredBy('Uiharu');
             }
-        });
 
-        $this->router->use('/', function($response, $request) {
-            if($request->getMethod() === 'OPTIONS') {
-                $response->setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET, POST');
-                return 204;
+            #[ExactRoute('GET', '/')]
+            public function getIndex(HttpResponseBuilder $response): void {
+                $response->accelRedirect('/index.html');
+                $response->setContentType('text/html; charset=utf-8');
             }
         });
-
-        $this->router->get('/', function($response) {
-            $response->accelRedirect('/index.html');
-            $response->setContentType('text/html; charset=utf-8');
-        });
     }
 
     public function dispatchHttp(): void {
diff --git a/src/UiharuAccessControlHandler.php b/src/UiharuAccessControlHandler.php
new file mode 100644
index 0000000..ceda55f
--- /dev/null
+++ b/src/UiharuAccessControlHandler.php
@@ -0,0 +1,26 @@
+<?php
+namespace Uiharu;
+
+use Index\Http\HttpUri;
+use Index\Http\Routing\HandlerContext;
+use Index\Http\Routing\Routes\RouteInfo;
+use Index\Http\Routing\AccessControl\{AccessControl,SimpleAccessControlHandler};
+
+class UiharuAccessControlHandler extends SimpleAccessControlHandler {
+    public function __construct(
+        private array $origins,
+    ) {}
+
+    #[\Override]
+    public function checkAccess(
+        HandlerContext $context,
+        AccessControl $accessControl,
+        HttpUri $origin,
+        ?RouteInfo $routeInfo = null,
+    ): string|bool {
+        if(!in_array($origin->host, $this->origins))
+            return false;
+
+        return $accessControl->credentials ? (string)$origin : true;
+    }
+}