PUT Strategy
WIPSingle-request presigned HTTP PUT — the S3 putObject / signed-PUT path.
PutStrategy sends the whole file as the body of one presigned HTTP PUT request. It is the
most common direct-to-storage pattern — S3 putObject, GCS and Azure signed PUT — and the
lightest way to hand bytes to object storage. It is not resumable: a failure restarts from
zero (but transient network errors are retried automatically).
import { PutStrategy } from '@gentleduck/upload/strategies'
strategies.set(PutStrategy({ allowedHosts: ['uploads.example.com'] }))
Configuration
PutStrategy({
allowedHosts?: string[] // lock the PUT host (recommended)
allowPrivateHosts?: boolean // allow loopback/RFC1918 hosts (default false)
maxRetries?: number // transient-failure retries (default 3)
})
When allowedHosts is omitted the strategy warns once and every host is allowed. Set it to
your storage host(s) to close the SSRF surface — see Security.
Intent shape
Your createIntent returns PutStrategy.Intent:
type Intent = {
strategy: 'put'
fileId: string
url: string // presigned PUT URL
headers?: Record<string, string> // signature-required headers (e.g. Content-Type)
expiresAt?: string // optional expiry hint
}
Any headers your signature covers (commonly Content-Type) go in headers and are sent
verbatim on the PUT.
Cursor shape
Nothing to resume, so the cursor is empty:
type Cursor = Record<string, never>
type Cursors = { put?: PutStrategy.Cursor }
How it runs
start() validates the URL, then PUTs the file body through ctx.transport.put, streaming
progress. The call is wrapped in the shared retry helper, so transient network failures
(timeouts, 5xx, ECONNRESET) back off and retry up to maxRetries:
async start(ctx) {
await ctx.transport.put({
url: ctx.intent.url,
body: ctx.file,
headers: ctx.intent.headers ?? {},
signal: ctx.signal,
onProgress: (uploadedBytes, totalBytes) => ctx.reportProgress({ uploadedBytes, totalBytes }),
})
}
If the intent is missing its url, the strategy throws a validation error.
Security
The presigned URL is backend-supplied and flows straight to the transport, so a compromised
backend or a MITM could return a file:, javascript:, or private-network URL. Every URL runs
through the shared validateUploadUrl SSRF guard before any byte leaves the client. Lock it
down with allowedHosts, and keep allowPrivateHosts at its default false to block
loopback, link-local, RFC1918, and cloud-metadata addresses.
When to use
- Direct-to-storage uploads where the backend can presign a single PUT.
- Small to medium files that don't need part-level resume.
- The simplest S3 / GCS / Azure object-put path.
For large files or flaky connections, reach for the
Multipart Strategy (concurrent parts, per-part resume) or
the tus Strategy (offset-based resume). A common pattern is to
register several and let createIntent pick per file by size.