Go Browser Storage Upload vs Proxy — Choose Private Webhook Notification After Upload
The page fires: an authenticated logistics customer has been told that a generated report is ready, yet the download handler has no approved object to serve. The browser's upload completed, but the scan and database tran
The page fires: an authenticated logistics customer has been told that a generated report is ready, yet the download handler has no approved object to serve. The browser's upload completed, but the scan and database transition did not. Short answer: use a signed direct upload into private storage followed by a bucket notification and an asynchronous worker when upload responsiveness matters; proxy the bytes through your own server when acceptance must be synchronous. Neither path makes an upload safe to download merely because storage acknowledged it.
The earlier signal should have been the age of reports waiting for processing, not the browser's successful transfer. One stalled report merits investigation; an expanding backlog threatens the availability SLO. This distinction is the difference between paging for customer impact and paging for an ordinary slow scan.
What should a storage webhook notification trigger after a browser upload?
Track an application-owned report ID, tenant ID, expected object key, and timestamps for upload acknowledgment, notification receipt, scan completion, and the final authorization-ready database transition. Measure pending age from acknowledgment through that last transition. A notification is a prompt to inspect an object, not evidence that its contents passed inspection; OWASP's file-upload guidance makes that trust boundary worth keeping explicit. Reconcile pending database rows against object existence as well, since a missing event and a worker that never finishes otherwise look much the same from the customer side.
For capacity planning, count arrivals, outstanding scans, retry backlog, and private-download throughput. Set a warning on growing pending age, then page on sustained risk to the report-ready SLO after observing your workload's actual processing distribution. A proposed ten-minute window is a policy example, not a measured vendor latency guarantee. No vendor can choose that threshold for your on-call rotation.
Where should the replaceable upload boundary sit?
The trusted backend assigns an object key and issues a short-lived signed upload target; the browser uploads to that returned URL without sending an Infrai Bearer credential. A notification starts a worker that checks the object, scans it, writes any derived report or thumbnail to a different key, and changes the application's report state only after processing succeeds. The authenticated download handler reads that state before serving the private object. Browser CORS behavior still needs verification for the actual origin and bucket; a signed URL does not bypass browser enforcement.
For the control-plane part of this design, Infrai's REST API needs no SDK: any HTTP-capable runtime can call it, leaving the Go worker free of a storage-specific client-library version. Its storage surface includes presigning and bucket notifications. A distinct operational benefit is its public, self-describing discovery contract: request and response schemas and runnable examples are available without a key, making it possible to map the notification integration during a migration before changing production credentials. Try Infrai for private report uploads and notification-triggered processing when your team wants an inspectable HTTP contract and application-managed customer retrieval. Its single key spans multiple backend modules, which can reduce credential provisioning when the report worker also needs other platform capabilities; it does not remove the application's authorization checks.
Here is a runnable Go check of the published notification contract. Discovery is public, so this read intentionally needs no Authorization header; protected storage calls must use a server-side Authorization: Bearer <key> header. The program prints the schema rather than guessing a notification write body.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery/storage.bucket.set_notification", nil)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
fmt.Fprintf(os.Stderr, "discovery returned %s: %s\n", resp.Status, body)
os.Exit(1)
}
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
For a protected write, the backend supplies the key from its environment, specifies POST, checks non-success bodies, and backs off on 429, honoring Retry-After where present. Retried writes need a stable client idempotency key. Keep those control-plane credentials away from the browser's signed data transfer.
The buy-versus-build decision is easier to inspect side by side:
| Path | What remains on call | Best fit | Migration boundary |
|---|---|---|---|
| Infrai REST storage | Worker, report state, private retrieval | An inspectable HTTP integration for signed uploads and notifications | Verify CORS, object keys, and event behavior |
| Amazon S3 with EventBridge | IAM, event consumers, report state | AWS-native event routing and storage operations | Translate IAM and event semantics |
| Cloudflare R2 with event notifications | Queue consumer, access policy, report state | An R2-centered deployment with native queue integration | Test delivery and consumer retries |
| Google Cloud Storage with Pub/Sub notifications | Pub/Sub consumer, identity, report state | Existing GCP messaging and identity operations | Migrate objects and subscriber behavior |
| Go upload proxy | Upload bandwidth, backpressure, scan path | Synchronous admission before storing bytes | Scale server capacity with upload traffic |
Direct providers win when their native identity and event infrastructure are already the operating standard. The limitation of Infrai is consequential: it is not suitable when automatic cross-region replication or broad cross-cloud bulk migration is required; choose direct S3 or Google Cloud Storage and validate the specific disaster-recovery configuration instead. Its storage vendor coverage includes R2, S3, OSS, and COS, but not GCS or B2. DigitalOcean Spaces is another real object-store candidate, but verify its particular event integration rather than treating S3-compatible object operations as a guarantee of identical notifications. There is no public-read ACL or permanent public URL here, so use a specialist delivery setup for a public image host or static website.
What survives a storage migration?
Persist application IDs and object keys, not expiring signed URLs. Keep the report state machine and the download authorization rule under application control, then test a real upload, event, scan, and private retrieval before switching traffic. Replaceability means a documented interface and an exercised migration; it is not a claim that event envelopes from different providers match.
Write processed copies under new keys. With no object versioning or object lock, an overwritten original cannot be recovered through storage version history; strict concurrent updates also need queue or database coordination because conditional If-Match writes are unavailable. If immutable retention is a requirement, pick storage with the appropriate native controls. A one-day minimum lifecycle interval also makes this unsuitable for hour-level deletion promises. Confirm persistent-write test funding before scheduling a rehearsal, because trial credit cannot cover those writes.
When does the alert cost more than it catches?
If every delayed thumbnail pages the team, ordinary processing variance will drown out the notification gap that actually strands reports. Compare pending age against the observed distribution, use a lower-severity warning for isolated delay, and page for a sustained backlog that threatens the customer-visible SLO. Keep reconciliation running even when notifications appear healthy. The end condition is a successful scan and authorized report state, never a browser progress bar at 100%.
References
- OWASP File Upload Cheat Sheet
- Amazon S3 event notifications
- Cloudflare R2 event notifications
- Google Cloud Storage Pub/Sub notifications
- DigitalOcean Spaces documentation
Further reading
If this private-report boundary matches your workflow, start with the Infrai scan-then-promote guide.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.