From 43c057a21446b00bac4329f21f0e126fa15bbe5e Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 28 Jun 2026 09:18:16 +0530 Subject: [PATCH 1/2] feat: add Content-Disposition header for raw paste download --- backend/app/api/subroutes/pastes.py | 35 ++++++++++++++++++++++++-- backend/tests/api/test_paste_routes.py | 7 ++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/backend/app/api/subroutes/pastes.py b/backend/app/api/subroutes/pastes.py index bcc0810..ede28ba 100644 --- a/backend/app/api/subroutes/pastes.py +++ b/backend/app/api/subroutes/pastes.py @@ -1,5 +1,6 @@ import logging from typing import TYPE_CHECKING +import re from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends @@ -161,9 +162,23 @@ async def get_paste_raw( cached_content = await cache.get(cache_key) if cached_content: cache_operations.labels(operation="get", result="hit").inc() + # Build a sanitized filename from the paste_id for cached responses + def _sanitize_filename(source: str) -> str: + name = re.sub(r"[^A-Za-z0-9 \-_.]", "", source) + name = re.sub(r"\s+", "_", name) + name = name.strip("_.") + name = name[:30] + if not name: + return str(paste_id) + return name + + filename = f"{_sanitize_filename(str(paste_id))}.txt" return PlainTextResponse( content=cached_content, - headers={"Cache-Control": f"public, max-age={config.CACHE_TTL}"}, + headers={ + "Cache-Control": f"public, max-age={config.CACHE_TTL}", + "Content-Disposition": f'attachment; filename="{filename}"', + }, ) cache_operations.labels(operation="get", result="miss").inc() @@ -177,9 +192,25 @@ async def get_paste_raw( await cache.set(cache_key, content, ttl=config.CACHE_TTL) cache_operations.labels(operation="set", result="success").inc() + # Sanitize filename from title (fallback to id) + def _sanitize_filename(source: str) -> str: + name = re.sub(r"[^A-Za-z0-9 \-_.]", "", source) + name = re.sub(r"\s+", "_", name) + name = name.strip("_.") + name = name[:30] + if not name: + return str(paste_id) + return name + + filename_source = paste_result.title if paste_result.title else str(paste_result.id) + filename = f"{_sanitize_filename(str(filename_source))}.txt" + return PlainTextResponse( content=content, - headers={"Cache-Control": f"public, max-age={config.CACHE_TTL}"}, + headers={ + "Cache-Control": f"public, max-age={config.CACHE_TTL}", + "Content-Disposition": f'attachment; filename="{filename}"', + }, ) diff --git a/backend/tests/api/test_paste_routes.py b/backend/tests/api/test_paste_routes.py index 3c56d90..65bd51d 100644 --- a/backend/tests/api/test_paste_routes.py +++ b/backend/tests/api/test_paste_routes.py @@ -279,6 +279,11 @@ async def test_get_raw_paste_returns_plain_text( assert response.status_code == 200 assert response.headers["content-type"] == "text/plain; charset=utf-8" assert response.text == "This is test content" + # New: ensure download header present + assert "content-disposition" in response.headers + cd = response.headers["content-disposition"] + assert "attachment" in cd + assert ".txt" in cd async def test_get_raw_paste_returns_404_for_nonexistent(self, test_client: AsyncClient, bypass_headers): """GET /pastes/{id}/raw should return 404 for non-existent paste.""" @@ -317,6 +322,8 @@ async def test_get_raw_paste_with_unicode_content(self, test_client: AsyncClient assert response.status_code == 200 assert response.text == paste_data["content"] + # Ensure download header exists for unicode titles as well + assert "content-disposition" in response.headers @pytest.mark.asyncio From 3be401572e0f72a711ea756e5b308d1869b110d3 Mon Sep 17 00:00:00 2001 From: Tushar Date: Sun, 28 Jun 2026 09:37:56 +0530 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- backend/app/api/subroutes/pastes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/subroutes/pastes.py b/backend/app/api/subroutes/pastes.py index ede28ba..378c7df 100644 --- a/backend/app/api/subroutes/pastes.py +++ b/backend/app/api/subroutes/pastes.py @@ -1,6 +1,6 @@ import logging -from typing import TYPE_CHECKING import re +from typing import TYPE_CHECKING from dependency_injector.wiring import Provide, inject from fastapi import APIRouter, Depends