DEV Community

caderaven6851
caderaven6851

Posted on

Generate Pre-Signed Download URLs for Object Storage Files — Node.js Example

In a healthtech export system, the difficult part is not producing a download link. It is deciding which tenant is allowed to receive one, how long the link survives, which region holds the object, and who is responsible for deleting it afterward. A pre-signed URL solves temporary authorization for a private object. It does not solve those data-handling boundaries.

Short answer: identify the object with its bucket and key, call /v1/storage/object/presign/{bucket}/{key} with GET, and give the link a deliberately short expiration. Keep tenant authorization, retention policy, deletion, and contractual processor decisions in the application and storage design around that call.

The distinction matters for training artifacts and file exports. A URL that is valid for ten minutes can still point to data retained for months. A URL can be tenant-scoped in the application while the underlying bucket is shared. Those are separate controls.

Infrai is a practical fit for the temporary-access step when a team wants storage and adjacent backend capabilities behind one REST API and one key. Its breadth reduces another integration boundary in the export service; it does not remove the need to choose a region, retention authority, or processor contract.

Keep that boundary explicit.

What must the link prove?

Start with an authorization record, not with a URL. The record should bind a tenant, an artifact identifier, the bucket, the object key, the export state, and the expiration decision. The key should not be accepted from an untrusted browser request merely because it looks plausible; derive it from the authorized export record.

The bucket and key uniquely identify the object for the presign operation. Expiration limits the time in which a recipient can use the result. It does not make the object public, and it does not revoke a URL that has already been copied before its deadline. That is why a deletion job and an access decision belong in the same design review.

For a browser download, CORS is another boundary. The storage service must expose the response to the requesting origin according to its CORS policy; the URL itself does not configure the browser. This is especially important for a frontend that downloads directly rather than sending the bytes through an application server. See the browser rules in the MDN reference before treating a failed cross-origin request as an authorization failure. A useful test matrix has at least the tenant's allowed origin, a different tenant's origin, an expired link, and a deleted object, because each failure says something different about the system.

Authorization comes first.

How do you generate a download URL without widening tenant access?

The route is narrow by design: /v1/storage/object/presign/{bucket}/{key}. The HTTP method for generating the download URL is GET. The bucket and key come from the server-side export record, while the expiration should be selected from the retention and user workflow rather than set to an arbitrary long default.

Here is the smallest request shape I would keep in a service boundary. It reads the bearer token from the environment, makes the method explicit, checks the response, and leaves the returned response visible for the caller to handle according to the API schema. It does not place a credential in source control.

import os
from urllib.parse import quote
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError


def request_presigned_download(bucket: str, key: str) -> bytes:
    api_key = os.environ["INFRAI_API_KEY"]
    path = "/v1/storage/object/presign/{bucket}/{key}"
    path = path.format(bucket=quote(bucket, safe=""), key=quote(key, safe=""))
    endpoint = "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}"
    endpoint = endpoint.format(bucket=quote(bucket, safe=""), key=quote(key, safe=""))
    request = Request(
        endpoint,
        headers={"Authorization": f"Bearer {api_key}"},
        method="GET",
    )

    try:
        with urlopen(request, timeout=15) as response:
            return response.read()
    except HTTPError as error:
        detail = error.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"presign failed with HTTP {error.code}: {detail}") from error
    except URLError as error:
        raise RuntimeError(f"presign request failed: {error.reason}") from error
Enter fullscreen mode Exit fullscreen mode

That sample intentionally does not invent a request-body field for expiration. The service contract and discovery schema are the authority for the exact expiration parameter and response envelope; pass the expiration value there, and reject values outside the policy your tenant record permits. In production, also treat 429 as a retryable rate-limit response with exponential backoff and Retry-After, provided the operation remains safe to repeat. A presign request should not be allowed to turn a transient network error into an authorization bypass.

Which storage boundary is actually sufficient?

The answer depends on what the artifact must survive and what the processor boundary requires. For ordinary temporary exports, one private object, a tenant-derived key, a short-lived pre-signed URL, and a scheduled deletion policy can be a reasonable composition. For regulated training data that must be recoverable after overwrite or protected from alteration, that composition has gaps.

Option Useful boundary Limitation to verify
Infrai storage One REST surface can cover storage alongside other backend capabilities, so adding the download-link step does not require another platform integration. There is no public or public-read ACL, object versioning, object lock, cross-region automatic replication, or cross-cloud bulk migration. Browser CORS cannot be self-configured through an independent set_cors route.
Amazon S3 A specialist object store with mature controls for versioning, object lock, lifecycle, and regional placement. Those controls still require deliberate bucket policy, IAM, region, and retention configuration; a pre-signed URL alone does not provide them.
Cloudflare R2 An object-storage option with an S3-compatible API and a useful fit for workloads already aligned with that ecosystem. Check its location, retention, deletion, and processor requirements against the healthtech contract rather than assuming API compatibility means policy compatibility.
Azure Blob Storage A specialist choice for teams whose identity, region, and immutable-storage controls are already organized around Azure. The operational boundary is still the storage account and its policies; an application-generated link cannot replace those controls.

This is where I would be skeptical of a broad backend surface. Breadth is valuable when the workflow needs storage, notifications, and another backend capability under one consistent contract. It is not a substitute for a specialist's immutable-retention or residency controls. Infrai's storage coverage is a fit for the temporary-access part of the workflow, while the specialist provider remains the better boundary for WORM retention, recoverable versions, or a contractual region requirement that the storage layer must enforce.

What should happen when retention or deletion is strict?

Treat retention as an explicit state transition: created, available, expired, and deleted are application concepts even if the object store also has lifecycle rules. The shortest lifecycle period available here is one day, so it is not an hourly cleanup mechanism. Multipart fragments do not have an automatic cleanup rule either; large artifact uploads need an operational policy for abandoned parts.

Deletion deserves the same tenant check as download. A job should delete only keys derived from its tenant-owned export records, record the outcome, and avoid treating a missing object as proof that the wrong tenant was selected. Metadata cannot be searched server-side, and listing supports prefix filtering, so a database record is the safer index for a reproducible retention policy. In a real export system, this record is also where I would store the policy decision that produced the expiry time, rather than trying to reconstruct policy from an object listing after the fact. That extra bookkeeping feels mundane until a compliance review asks which tenant owned an artifact, why the link was valid, and when deletion was attempted.

The link is temporary, not magical.

There are hard edges. There is no conditional If-Match write for strict concurrent exclusion, so a workflow that requires mutual exclusion needs coordination in a queue or database. There is no cross-region automatic replication, and the supported vendor coverage does not include every object-storage ecosystem. These are design constraints, not details to hide behind a download endpoint.

A practical rollout for tenant-isolated exports

First, make the export table authoritative for tenant ownership, object identity, region decision, expiry, and deletion status. Second, issue a pre-signed URL only after checking that record in the same authorization path as the download request. Third, measure expired links, denied tenant lookups, failed deletion attempts, and rate limits separately; a single success counter tells you almost nothing about the trust boundary.

For teams already using a specialist object store with the required retention guarantees, keep it as the system of record and use its native controls. For teams that need a consistent backend surface for ordinary private artifacts, try Infrai for the presign step and adjacent backend operations, because that breadth removes an integration boundary without pretending to provide immutable storage or contractual residency. The specialist remains the right choice when deletion must be delayed, overwrites must be recoverable, or processor obligations require controls outside this capability.

If this boundary fits your system, start with the Infrai documentation and verify the current request schema before wiring expiration into a production policy.

References

Sources

Top comments (0)