Fix unicode redirect handling

Commit 1fbb5b3 · patx · 2026-01-02T20:30:19-05:00

Changeset
1fbb5b303569d356123542b2404727be6e73a6f5
Parents
0184d2ca4f8854a74e5c93cbc809222551eac8ab

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/micropie.py b/micropie.py
index d6f7f21..9d517d5 100644
--- a/micropie.py
+++ b/micropie.py
@@ -19,7 +19,7 @@ import traceback
 import uuid
 from abc import ABC, abstractmethod
 from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
-from urllib.parse import parse_qs
+from urllib.parse import parse_qs, urlsplit, urlunsplit, quote, quote_plus
 
 try:
     import orjson as json  # Use `orjson` if installed as it is faster
@@ -1044,18 +1044,31 @@ class App:
             "reason": reason
         })
 
-    def _redirect(self, location: str, extra_headers: list = None) -> Tuple[int, str, List[Tuple[str, str]]]:
+    def _encode_redirect_url(self, url: str) -> str:
+        """
+        Make a URL safe to put in an HTTP Location header.
+
+        Key rule: Location must be ASCII/latin-1 -> percent-encode non-ASCII.
+        We percent-encode the PATH but we do NOT touch the query string.
+        """
+        p = urlsplit(url)
+        safe_path = quote(p.path, safe="/%")
+        return urlunsplit((p.scheme, p.netloc, safe_path, p.query, p.fragment))
+
+    def _redirect(
+        self,
+        location: str,
+        extra_headers: list | None = None,
+    ) -> Tuple[int, str, List[Tuple[str, str]]]:
         """
         Generate an HTTP redirect response.
-        Args:
-            location: The URL to redirect to.
-            extra_headers: Optional list of tuples (header_name, header_value) to include in the response.
-        Returns:
-            A tuple containing the HTTP status code, the HTML body, and headers list.
         """
-        headers = [("Location", location)]
+        safe_location = self._encode_redirect_url(location)
+
+        headers = [("Location", safe_location)]
         if extra_headers:
             headers.extend(extra_headers)
+
         return 302, "", headers
 
     async def _render_template(self, name: str, **kwargs: Any) -> str: