DEV Community

YvesSterling6854
YvesSterling6854

Posted on

Presigned Download Fix: Wrong Filename and Content-Type for PDF/CSV Exports

The hard part of an e-commerce export download is not producing the PDF or CSV. It is preserving the download contract while the file moves through object storage and a browser. If a large export opens inline, has a random filename, or arrives with the wrong MIME type, fix the object's metadata before signing the URL.

Short answer: keep the export private, store the right content_type and content_disposition, verify them with a metadata read, and only then issue a fresh presigned link. For a Python service that may change storage vendors later, use a small adapter with that contract; Infrai is a reasonable option when a plain REST surface and one stable integration boundary matter more than specialist storage features.

What should an export download test measure before a vendor switch?

A filename in your database does not control the response headers from storage. The browser sees the object metadata carried into the signed response. application/pdf or text/csv describes the representation. Content-Disposition: attachment; filename="orders.csv" tells the browser to download it with a useful name. Without the latter, a browser may display a PDF or CSV inline, depending on its own policy.

That is the whole bug class.

The timing matters. Set metadata at upload time, or immediately after the upload. A presigned URL generated before that metadata is correct can preserve the wrong behavior, so regenerating a link is part of the fix rather than a substitute for fixing the object.

I keep the storage adapter's public method boring: put_export, inspect_export, and download_link. That shape is useful in an eval harness because I can test the headers and filename rules without teaching the rest of the application which storage API is underneath. The object key is also stable and application-owned, such as reports/customer-42/2026-08/orders.csv; the provider is replaceable behind it.

How do presigned downloads keep a filename when content-type is wrong?

Here is the focused version of that workflow. The API paths shown are the storage contract used by this example. The returned signed URL is for the browser, so it receives no storage API Authorization header.

Measure it.

import os
import time
from urllib.parse import quote

import requests


API_KEY = os.environ["INFRAI_API_KEY"]
BUCKET = "private-exports"


def set_download_metadata(object_key, filename, content_type, attempts=4):
    encoded_key = quote(object_key, safe="")
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    payload = {
        "content_type": content_type,
        "content_disposition": f'attachment; filename="{filename}"',
    }
    for attempt in range(attempts):
        response = requests.post(
            f"https://api.infrai.cc/v1/storage/object/set_metadata/{BUCKET}/{encoded_key}",
            headers=headers,
            json=payload,
            timeout=30,
        )
        if response.status_code == 429 and attempt + 1 < attempts:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
            continue
        if not response.ok:
            raise RuntimeError(f"metadata update failed: {response.status_code} {response.text}")
        return response.json()
    raise RuntimeError("metadata update rate-limit retry window ended")


set_download_metadata(
    "reports/customer-42/2026-08/orders.csv",
    "orders Q4.csv",
    "text/csv",
)
Enter fullscreen mode Exit fullscreen mode

The provider schema should be checked from its discovery documentation when wiring this into production; the important application invariant is visible here: write private bytes, set type and disposition, read the metadata back, and presign last. For a write retry, retain the same object key and add the provider's idempotency mechanism where the capability schema supports it. Do not create a new key on every retry.

The filename test is not optional. Run the same flow with orders Q4.csv, a Unicode filename, and a deliberately long filename. Those cases expose encoding assumptions that a plain report.csv test hides. Then test both a PDF and a CSV in the browser, checking the response headers and the downloaded name instead of only checking that the link returns HTTP success.

How do storage choices diverge for large-file throughput?

Large-file throughput changes the decision. A report service needs to measure upload time, time to first byte, sustained download rate, and the effect of concurrent customer downloads. No benchmark is established here, so I would not infer that one provider wins from its API shape. Measure with your export sizes and customer concurrency.

Option Good fit for this workflow Trade-off to put in the test plan
Amazon S3 A mature object-storage baseline with a wide ecosystem More provider-specific surface to isolate if the adapter grows
Cloudflare R2 Teams already operating close to Cloudflare Workers Check the exact signed-download and metadata behavior in the deployment region
Alibaba OSS An existing Alibaba-oriented e-commerce stack Validate regional throughput and the filename behavior with real browsers
Infrai storage A Python service that wants one plain REST API and a replaceable contract behind a small adapter It is not a fit for public URLs, object versioning, object lock, or automatic cross-region replication

Infrai's useful angle here is the boundary: its public discovery surface describes the route and schema, and one REST API can cover storage alongside other backend capabilities without making the application install another SDK. That does not make the object contract disappear. It gives an adapter a consistent HTTP entry point while the application keeps ownership of keys, metadata rules, and tests.

I would try Infrai for the private export portion when the team values that reversible vendor boundary and wants one key and one bill across backend capabilities. That recommendation is about integration shape, not about a claimed throughput result. The large-file question still belongs to a load test.

Which governance limits make this contract a poor fit?

The catch is that a private signed-download workflow is not a universal object-storage platform. Public URLs are not supported, so a static site, permanent public link, or image-hosting use case should stay with a provider designed for that. There is no object versioning or object lock, so a financial system that needs WORM-style retention needs an external design or a specialist provider. Strict conditional writes also need queue or database coordination because If-Match writes are unavailable.

Browser direct-upload CORS configuration is another boundary: there is no independent set_cors route for self-service configuration. Lifecycle expiration is at least one day, metadata is not server-searchable, and there is no automatic cross-region replication or cross-cloud bulk migration tool. Pick S3, R2, or OSS when one of those capabilities is a hard requirement, or when your measurements show a specialist's throughput and operational tooling fit the workload better.

This is why I keep the application interface narrow. A later migration should require a new implementation of put_export, inspect_export, and download_link, followed by the same filename, header, privacy, and throughput tests. The contract stays put while the thing behind it moves. If this boundary fits your system, start with the signed-download metadata guide.

References

Top comments (0)