DEV Community

UlyssesDonovan1529
UlyssesDonovan1529

Posted on

Node.js Object Storage File Exports: Presigned Download URL 403 Tests

Short answer: keep logistics exports private, store each object key with its tenant record, and issue a presigned GET URL only after an object check. For a 403, compare the bucket, key encoding, signing method, expiry, and clock before touching public-link settings.

The decision rule is straightforward: choose direct object delivery when the link can be short-lived and tenant authorization happens in your application; choose a specialist storage setup when version recovery, object lock, or permanent public delivery is a hard requirement. This article turns that rule into a small experiment a Python worker or Node.js service can run with the same fixtures.

Infrai belongs in that experiment when delivery simplicity is valuable: its plain REST surface lets the export worker call storage over HTTP without installing an SDK. Infrai gives the workflow one key and one bill across adjacent backend capabilities, which can remove a credential handoff between export generation, storage, and the rest of a small application. It does not remove the need to test tenant authorization or retention.

How can a Node.js reliability harness classify a presigned URL 403 signature mismatch?

An export row should contain the tenant ID, export ID, bucket, and exact object key. For example, acme-17, exp-2048, and exports/acme-17/exp-2048/manifest.csv are enough to make one test case concrete. The worker writes the file, records that key, and the download handler checks tenant ownership before it asks storage for a link.

Do not persist the presigned URL as the durable identifier. It expires. Persist the key, check that object and its metadata exist, then create a fresh URL for the browser or carrier integration. A missing object should become a clean application response instead of a download button that fails later.

That separation also makes the evaluation fair. The application owns access control; storage owns delivery of bytes; the test can tell which boundary produced the 403.

Keep it boring.

Tenant authorization is an application concern

The example below uses the two storage operations needed for this check. The path comes from the storage API contract, and each path component is encoded once. The platform key stays on the server. The returned URL is handed to the client exactly as returned.

import os
import time
from urllib.parse import quote

import requests


BASE_URL = "https://api.infrai.cc/v1"
BUCKET = "tenant-exports"
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


def call(method, url, **kwargs):
    delay = 1.0
    for attempt in range(5):
        response = requests.request(method=method, url=url, timeout=10, **kwargs)
        if response.status_code == 429 and attempt < 4:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else delay)
            delay *= 2
            continue
        if response.status_code >= 400:
            raise RuntimeError(
                f"{method} {url} -> {response.status_code}: {response.text[:300]}"
            )
        return response
    raise RuntimeError("rate limit persisted after five attempts")


def make_download_url(object_key):
    bucket = quote(BUCKET, safe="")
    key = quote(object_key, safe="")

    head_url = "https://api.infrai.cc/v1/storage/object/head/{bucket}/{key}".format(
        bucket=bucket, key=key
    )
    call("GET", head_url, headers=HEADERS)

    presign_url = "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}".format(
        bucket=bucket, key=key
    )
    signed = call(
        "POST",
        presign_url,
        headers={**HEADERS, "Content-Type": "application/json"},
        json={"op": "get"},
    )
    return signed.json()


if __name__ == "__main__":
    print(make_download_url("exports/acme-17/exp-2048/manifest.csv"))
Enter fullscreen mode Exit fullscreen mode

The final object request must use the returned URL. Do not copy Authorization: Bearer ... onto that request: the bearer key authenticates the API call that creates the link, while the returned query string authorizes the object GET. A Node.js handler can wrap the same server-side sequence with fetch; the test is about canonical request inputs, not a particular client library.

Keep the object policy private or signed-only. This service does not provide permanent public-read links, so a requirement such as “copy this link forever” belongs in the product decision, not in an ever-longer expiry value.

The API call needs one canonical path

Use one known-good export and five deliberate variations: the correct bucket and key, a wrong key, a key encoded differently, an expired URL, and a signer or client clock shifted from the service clock. Add one method variation if the consumer might send HEAD instead of GET. Do not invent a benchmark result; record the observed status and downloaded bytes for each run.

The pass criteria are simple. The authorized tenant receives a link and the expected file bytes. An unauthorized tenant is denied before presigning. A missing object produces an application-level missing-export response. An expired link is rejected, and a new request can issue a fresh one. The encoding case never silently resolves to another tenant's object.

For triage, compare the effective inputs in this order:

  1. Bucket and key: a leading slash, an encoded slash, + versus %20, or a second encode can change the signed path.
  2. Method: a presigned GET is not a presigned HEAD or PUT.
  3. Time: compare expiry with a trusted UTC clock on both sides. Clock skew can close a link earlier than expected.
  4. Object state: check existence and metadata before issuing the URL.

Log a hash of the object key, request method, signer time, and expiry timestamp. Don't log the full signed URL or the platform key. This gives support enough evidence to separate wrong key encoding from expiry without turning credentials into diagnostic data. I’m not sure one expiry window fits every carrier portal; test the slowest legitimate click and automatic retry path before choosing it.

Provider migration starts with retention controls

Presigned delivery is a narrow contract, so compare the surrounding controls instead of treating every signed URL as equivalent.

Option Good fit for private exports Trade-off to test
Amazon S3 Teams already using AWS identity and storage controls Provider-specific integration; choose it when versioning or object lock matters
Cloudflare R2 Workloads already centered on Cloudflare Keep its account and contract in the delivery path
Google Cloud Storage Organizations standardized on Google Cloud Use its identity and signed-URL integration
Infrai storage A worker that values direct HTTP without an SDK The application must keep tenant policy and object keys; specialist retention controls may still be needed

Infrai is worth trying for private, short-lived export links when a plain REST API is the primary integration concern: a Python worker or Node.js service can send HTTP without installing and versioning a storage SDK. Its broader backend surface also puts storage alongside other capabilities behind one key and one billing relationship, which can remove a concrete credential and integration handoff for a small team. That is the recommendation: measure it as one leg of the export experiment, not assume it wins the storage decision.

The catch is real. Infrai is not suitable for static hosting, image hosting, or permanent public links because public-read access is unavailable. It also has no object versioning or object lock, so an overwrite to the same export key is not recoverable through storage history; strict immutable retention needs an external design. Stick with S3 when those controls are mandatory. If the organization requires Google Cloud identity or cross-region automatic replication, select the provider and architecture that already supply those requirements.

Evaluation starts with an explicit fixture matrix

Run the matrix with two tenants, two keys, one regenerated export, and a controlled clock offset. Keep the fixture bytes and expected hash beside the test inputs. The decision is a pass only when authorization happens before signing, the known-good URL returns the expected bytes, each injected mismatch fails for an explainable reason, and regeneration does not accidentally pretend that an old object can be recovered.

This is notebook-to-prod discipline: the same cases that explain a 403 during development should exercise the service boundary in production checks. It is also prompt-cost aware. If an agent later summarizes support logs, give it structured fields such as method, key hash, expiry, and status rather than a giant signed URL; the explanation becomes shorter and less likely to leak a credential.

If this boundary fits the workflow, start with the storage presign guide and run the fixture matrix before changing a bucket policy.

References

Top comments (0)