An agent finishes a task and produces a 180 MB archive. Returning it inside the task JSON makes the API slow, retries expensive, and browser failure states unclear.
Keep task metadata small. Deliver large artifacts through a separate, expiring download contract.
The API shape
type Artifact = {
id: string;
taskId: string;
filename: string;
contentType: string;
sizeBytes: number;
sha256: string;
state: "building" | "ready" | "expired" | "deleted";
};
type DownloadGrant = {
url: string;
expiresAt: string;
artifactId: string;
sha256: string;
};
The task endpoint returns artifact metadata. A separate authorized endpoint creates a short-lived download grant only when the artifact is ready and the current user may access that task.
GET /tasks/:id -> metadata
POST /artifacts/:id/downloads -> signed grant
GET signed object URL -> bytes
Bind the grant
A signed URL should bind at least the object key and expiration. Depending on the storage provider, also bind the response filename and content type. Never let a client submit an arbitrary object key for the server to sign.
Before issuing the grant:
- load the artifact by ID;
- authorize through its task and tenant;
- require
state === "ready"; - sign the server-owned object key;
- return the expected digest.
The digest is not authorization. It lets clients or downstream systems detect corruption after download.
Make browser states explicit
type DownloadState =
| { kind: "idle" }
| { kind: "requesting-grant" }
| { kind: "downloading"; received: number; total?: number }
| { kind: "verifying" }
| { kind: "expired" }
| { kind: "failed"; retryable: boolean }
| { kind: "complete" };
If a grant expires, request a new grant after authorization. Do not blindly retry the same URL. If the artifact itself is expired or deleted, show that terminal state instead of an endless retry button.
Test the complete path
Use a disposable object store and assert:
- another tenant cannot obtain a grant;
- changing the object path invalidates the signature;
- an expired grant fails while a newly issued grant works;
- downloaded bytes match
sha256; - deleting task access prevents future grants even if metadata was cached.
A signed URL reduces load on your application servers, but it is still a bearer capability until expiration. Keep lifetimes short, avoid logging full query strings, and do not confuse “unguessable” with “revocable.”
This separation keeps the control plane small and the data plane efficient—the task API describes what exists, while storage delivers the bytes.
Top comments (0)