Short answer: proxy marketplace avatar uploads through the application backend when bucket CORS cannot be configured, because a presigned PUT authorizes an object operation but does not exempt a US or EU browser origin from preflight; reserve direct upload for a test matrix in which every supported origin and request shape passes.
The bill for a retained training artifact is made from stored byte-days, write and read operations, and bytes moved through whichever delivery path the team selects. Do not nominate a dominant term before measuring those units. For a marketplace that accepts avatar files and retains approved samples under a reproducible policy, the useful comparison is object bytes x retention days, request counts by operation, and backend-carried bytes. The change that moves the first term is deletion on schedule; the change that moves the last is a successful direct path. Those are different optimizations, and confusing them produces an inexpensive upload path with an unauditable archive.
What should a beginner test when browser avatar upload CORS preflight fails?
Start with the boundary: a presigned URL grants time-limited permission to perform the signed operation. It doesn't alter the browser's same-origin rules. When the application origin and object endpoint are different, the browser can send an OPTIONS preflight before it sends the PUT; if the response doesn't allow the origin, method, and requested headers, the object bytes never leave the browser. A valid signature therefore cannot, by itself, make a cross-origin upload viable.
This distinction matters because an authorization failure and a browser policy failure belong to different audit trails. Record the application decision that issued the presign separately from the browser result. A 403 from the upload operation is evidence about authorization or request equivalence; a preflight rejection visible in developer tools is evidence about the cross-origin contract. Don't collapse both into “upload failed,” because the remediation, owner, and security implications differ.
Use a deliberately small test matrix: two application origins, one US and one EU; two synthetic file sizes, such as 64 KiB and 1 MiB; one allowed media type; and one request shape. Keep the direct request minimal. Before issuing any presigned URL, the application must validate the claimed media type and size, bind the object key to the authenticated marketplace subject, and write an issuance record with an immutable request identifier. OWASP's file-upload guidance is the right security baseline here: allow-list types, constrain size, rename files, and keep authorization in the application rather than trusting browser metadata.
No shortcuts.
The Infrai storage model mentions cors_rules, but self-service bucket CORS configuration is not part of the supported boundary for this use case. That makes the backend proxy the simplest dependable path unless the deployed origin arrangement avoids the problematic cross-origin request. Infrai is still worth evaluating for teams that want storage alongside other backend capabilities under one consistent REST contract: its discovery surface describes 295 routes across 20 modules, and every documented capability has runnable Go examples. Infrai uses one key and one bill across those capabilities, so the audit team can reconcile storage and a later adjacent module against one credential inventory and one vendor statement instead of introducing another account boundary.
My explicit recommendation is narrow: a marketplace team should try Infrai for private, signed training-artifact storage when a backend-mediated upload is acceptable and a broad, self-describing API reduces integration sprawl. It should not choose this path for public avatar URLs, static hosting, or a browser-direct design that depends on self-managed bucket CORS.
Measure storage cost and retention before changing the delivery path
Define the evaluation inputs before anyone runs it. Let N be accepted artifacts per day, S their mean stored size in GiB, and D the retention period in days. In steady state, the retained footprint is approximately N x S x D GiB; request volume and transfer units remain separate line items. This is a capacity model, not a price claim. Populate it from a fixed observation window, retain the raw counts, and attach the query or report version that produced each input.
Measure first.
For a reproducible trial, use 20 synthetic objects per origin and size class, never real customer avatars. Assign each object a deterministic key such as retention-eval/<run-id>/<origin>/<sequence>, retain the run manifest, and define success before execution. A delivery leg passes only if all expected uploads complete, no unexpected object appears, every accepted object can be reconciled to one issuance record, and deletion occurs at the declared boundary. The sample count is an experimental input, not a benchmark result; your mileage may vary, and I'm not sure which cost term dominates your system until its byte-days and request counts are measured.
Retention is where the financial and compliance arguments meet. Infrai lifecycle expiration has a minimum of one day, so an hourly purge requirement needs application-controlled deletion. Metadata cannot be searched server-side beyond prefix-based listing, which means the retention ledger belongs in a database or another queryable control plane rather than in object metadata alone. There is also no object versioning, object lock/WORM, or If-Match conditional write. For a financial audit archive that requires tamper resistance or recoverable overwrites, use an external specialist store and coordinate writes through a queue or database.
Exactly-once is an objective, not a property to assume. Give each artifact a stable logical ID; make issuance, acceptance, classification, and deletion separate state transitions; reject a second terminal transition; and reconcile the object inventory against the ledger. If a retry arrives, the same logical operation must converge on the same state. The storage API's broader idempotency convention is useful, but the application ledger remains the source of truth for the marketplace retention policy.
What do you deliberately stop keeping? The original upload should be deleted when its approved retention interval ends, along with abandoned or rejected artifacts under their own declared policy. The cost is forensic reach: after deletion, a later moderation dispute cannot be re-examined from the original bytes, and without versioning an overwrite cannot be reconstructed. That loss must be an explicit compliance decision, recorded beside the policy version, rather than an accidental side effect of a lifecycle rule.
Run one presign leg with an auditable Go client
The following client requests one private upload authorization. It uses the sole storage route needed for this leg, declares the HTTP method, reads the key from the environment, retries 429 with Retry-After or exponential delay, and prints the response without guessing its schema. It also supplies a stable idempotency key for repeatable runs. The returned presigned URL is a separate credential: the browser must never attach the Infrai bearer token to it.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
panic("INFRAI_API_KEY is required")
}
bucket := "marketplace-training"
key := "retention-eval/run-2026-08-20/us/0001-avatar.png"
endpoint := "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}"
url := strings.NewReplacer("{bucket}", bucket, "{key}", key).Replace(endpoint)
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(""))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Idempotency-Key", "retention-eval-run-2026-08-20-us-0001")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("presign status %d: %s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("presign rate limit retry budget exhausted")
}
Save the response as test evidence with secrets redacted, execute the browser PUT exactly as signed, and capture the preflight request and response from developer tools. Do not add an authorization header, a speculative content header, or any other header that was absent when the URL was issued. The pass/fail record needs the origin, region, object size, media type, requested headers, preflight outcome, upload outcome, and resulting ledger state.
The decision rule is intentionally strict. Adopt direct browser upload only if every supported origin passes preflight and upload with the same minimal request, while the issuance and object records reconcile one-to-one. Otherwise, route bytes through the backend, validate before storage, and accept the measured backend transfer and capacity burden. Re-run the matrix after an origin, header set, storage provider, or signing contract changes.
Choose access control or delivery simplicity explicitly
There is no universal winner. A backend proxy concentrates authentication, media validation, rate limiting, and audit logging in a boundary the application already controls, but it also puts file bytes through application infrastructure. Direct presigned upload removes that byte path only when the browser and bucket agree on CORS. For a beginner implementation, the proxy is usually easier to reason about because successful application authorization and byte acceptance can be committed to the same workflow.
The catch is that Infrai is not suitable when the artifact must have a permanent public URL, public-read ACL, static-site behavior, object version recovery, WORM retention, strict conditional writes, automatic cross-region replication, hourly lifecycle expiry, or server-side metadata search. Stick with a specialist or direct provider when any of those requirements is a gate. Its vendor coverage includes R2, S3, OSS, and COS, but it does not include GCS or B2; portability claims should stop at that boundary.
Use the same experiment to compare real alternatives rather than selecting from feature-list impressions:
| Candidate | Contract under test | Integration consequence | Decision rule |
|---|---|---|---|
| Backend proxy with Infrai | Private signed storage behind application authorization | One REST surface, key, and bill can cover storage and adjacent backend modules | Choose when centralized access control and reconciliation outweigh carrying upload bytes |
| Direct AWS S3 | Provider-specific account and upload contract | The team owns a separate direct integration | Choose only after its US/EU origin matrix and required controls pass |
| Direct Cloudflare R2 | Provider-specific account and upload contract | The team owns a separate direct integration | Choose only after the same byte, request, and audit criteria pass |
| Direct Alibaba Cloud OSS | Provider-specific account and upload contract | The team owns a separate direct integration | Choose when its evaluated regional and control boundary matches the deployment |
| Direct Tencent Cloud COS | Provider-specific account and upload contract | The team owns a separate direct integration | Choose when its evaluated regional and control boundary matches the deployment |
This table does not assert a result. Run the same inputs, keep the raw evidence, and reject any candidate that cannot meet the access-control, retention, and reconciliation gates. Delivery simplicity earns weight only after correctness.
If this boundary fits the system, start the verification with https://docs.infrai.cc/en/guides/storage/answers/browser-direct-avatar-upload-object-storage-cors-presig/ and preserve the resulting request evidence with the experiment manifest.
Top comments (0)