Skip to content

Commit

Permalink
Preserve query string on redirect
Browse files Browse the repository at this point in the history
  • Loading branch information
c-w committed Sep 30, 2024
1 parent 9d904e4 commit c92e241
Show file tree
Hide file tree
Showing 6 changed files with 46 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Using the following categories, list your changes in this order:
### Added

- Support Python 3.13.
- Django `settings.py:SERVESTATIC_PRESERVE_QUERY_STRING_ON_REDIRECT` will forward the request query string when issuing redirects.

## [2.0.1] - 2024-09-13

Expand Down
10 changes: 10 additions & 0 deletions docs/src/django-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ This setting is only effective if the `ServeStatic` storage backend is being use

---

## `SERVESTATIC_PRESERVE_QUERY_STRING_ON_REDIRECT`

**Default:** `False`

Set to `True` to preserve the request query string when issuing redirects.

By default, ServeStatic will strip the query string when issuing a redirect. If this setting is enabled, the query string will instead be forwarded to the redirect location.

---

## `SERVESTATIC_MANIFEST_STRICT`

**Default:** `True`
Expand Down
8 changes: 7 additions & 1 deletion src/servestatic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def __init__(
add_headers_function: Callable[[Headers, str, str], None] | None = None,
index_file: str | bool | None = None,
immutable_file_test: Callable | str | None = None,
preserve_query_string_on_redirect: bool | None = None,
):
self.autorefresh = autorefresh
self.max_age = max_age
Expand All @@ -60,6 +61,7 @@ def __init__(
self.application = application
self.files = {}
self.directories = []
self.preserve_query_string_on_redirect = preserve_query_string_on_redirect

if index_file is True:
self.index_file: str | None = "index.html"
Expand Down Expand Up @@ -236,4 +238,8 @@ def redirect(self, from_url, to_url):
msg = f"Cannot handle redirect: {from_url} > {to_url}"
raise ValueError(msg)
headers = {"Cache-Control": f"max-age={self.max_age}, public"} if self.max_age is not None else {}
return Redirect(relative_url, headers=headers)
return Redirect(
relative_url,
headers=headers,
preserve_query_string=self.preserve_query_string_on_redirect,
)
6 changes: 6 additions & 0 deletions src/servestatic/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def __init__(self, get_response=None, settings=django_settings):
force_script_name = getattr(settings, "FORCE_SCRIPT_NAME", None)
static_url = getattr(settings, "STATIC_URL", None)
root = getattr(settings, "SERVESTATIC_ROOT", None)
preserve_query_string_on_redirect = getattr(
django_settings,
"SERVESTATIC_PRESERVE_QUERY_STRING_ON_REDIRECT",
False,
)

super().__init__(
application=None,
Expand All @@ -84,6 +89,7 @@ def __init__(self, get_response=None, settings=django_settings):
add_headers_function=add_headers_function,
index_file=self.index_file,
immutable_file_test=immutable_file_test,
preserve_query_string_on_redirect=preserve_query_string_on_redirect,
)

if self.static_prefix is None:
Expand Down
10 changes: 9 additions & 1 deletion src/servestatic/responders.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,12 +315,20 @@ def get_path_and_headers(self, request_headers):


class Redirect:
def __init__(self, location, headers=None):
def __init__(self, location, headers=None, preserve_query_string=False):
headers = list(headers.items()) if headers else []
headers.append(("Location", quote(location.encode("utf8"))))
self.response = Response(HTTPStatus.FOUND, headers, None)
self.preserve_query_string = preserve_query_string

def get_response(self, method, request_headers):
query_string = request_headers.get("QUERY_STRING")
if query_string and self.preserve_query_string:
headers = list(self.response.headers)
name, value = headers[-1]
value = "{}?{}".format(value, query_string)
headers[-1] = (name, value)
return Response(self.response.status, headers, None)
return self.response

async def aget_response(self, method, request_headers):
Expand Down
14 changes: 13 additions & 1 deletion tests/test_servestatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pytest

from servestatic import ServeStatic
from servestatic.responders import StaticFile
from servestatic.responders import Redirect, StaticFile

from .utils import AppServer, Files

Expand Down Expand Up @@ -376,3 +376,15 @@ def test_chunked_file_size_matches_range_with_range_header():
while response.file.read(1):
file_size += 1
assert file_size == 14


def test_redirect_strips_query_string_by_default():
responder = Redirect("/redirect/to/here/")
response = responder.get_response("GET", {"QUERY_STRING": "foo=1&bar=2"})
assert response.headers[0] == ("Location", "/redirect/to/here/")


def test_redirect_preserves_query_string_if_configured():
responder = Redirect("/redirect/to/here/", preserve_query_string=True)
response = responder.get_response("GET", {"QUERY_STRING": "foo=1&bar=2"})
assert response.headers[0] == ("Location", "/redirect/to/here/?foo=1&bar=2")

0 comments on commit c92e241

Please sign in to comment.