Web Infrastructure
R2 Presigned URLs: Diagnose Expiry, CORS, and Signature Mismatches
Troubleshoot R2 presigned URLs by checking method, signed headers, expiry, and browser CORS while treating the URL as temporary access to a specific object.
In this article
A presigned URL can work in a command-line test and fail in a browser. It can also fail after someone changes a header or edits a query parameter. These symptoms are frustrating because the URL looks like an ordinary link, but it carries a specific signed request contract.
R2 presigned URL troubleshooting starts by separating authorisation from browser policy and transport. A signature mismatch, an expired grant, and a blocked cross-origin request need different fixes. Don't respond by making the whole bucket public simply to get one upload working.
Treat the URL as a temporary credential
Anyone who possesses a usable presigned URL may be able to perform its permitted operation until the relevant conditions stop it. Keep it out of public tickets, analytics, screenshots, and logs. Share diagnostic metadata instead of the full query string.
Cloudflare's R2 presigned URL documentation describes generating grants for supported S3 API operations. Review the current operation and endpoint requirements. A signed URL is not automatically interchangeable with a custom-domain URL or another storage endpoint.
Record the object key, HTTP method, intended expiry, and signed headers in an internal reference. Keep the signing credentials server-side. The browser should receive only the narrow temporary grant needed for its task, not the credential used to create grants.
Match the method and signed request details
A URL signed for one operation should be used for that operation. Confirm whether the client sends GET, PUT, or another documented method. Check the exact object path and avoid editing signed query parameters after generation.
If Content-Type was included in the signature, the client must send the matching value. A browser or upload library may select a different header automatically. Compare the request that was signed with the request actually sent.
For synthetic metadata, JSON Formatter can make method, header, and expiry fields readable. It doesn't validate a signature. Never paste a real presigned URL into a public checker or decoder merely to inspect its credentials.
Separate CORS from storage authorisation
Browsers enforce cross-origin rules that a command-line client doesn't. A valid signed operation can still fail in the browser if the bucket's CORS configuration doesn't permit the origin, method, or requested headers.
Cloudflare's R2 CORS documentation explains configuring that browser-facing policy. CORS does not replace signature validation or user authorisation. A permissive origin policy doesn't mean every visitor should receive a signed grant.
Inspect the browser's preflight and actual request in local developer tools. Determine whether the request reached storage and whether the browser can read the response. Don't assume a visible CORS error proves the signature is valid; more than one problem can exist.
Check time and expiry deliberately
Confirm the signing service's clock and the generated expiry using the storage provider's documented rules. A short expiry may be appropriate, but it must leave enough time for the intended user action and network conditions.
Distinguish creating a new grant from extending an existing one. Editing the expiry text inside a signed URL won't create a valid longer grant. If the operation needs another attempt, request a new authorised grant through your application.
For a synthetic timestamp, Unix Timestamp Converter can help explain the intended instant. Check the SDK's actual time units and format separately. A milliseconds-versus-seconds mistake can make otherwise reasonable expiry logic wrong.
Authorise before signing
Your application must check whether the current user can access the object before generating the URL. An unpredictable object key is not enough. Otherwise a valid signing service can become a way for users to request grants to other users' files.
For uploads, choose object ownership and overwrite behaviour deliberately. Don't let a caller provide an unrestricted key that replaces another user's object. Consider a controlled staging path and a separate verification step before treating uploaded content as accepted application data.
Signing a content-type header constrains that header; it doesn't inspect the file's actual content. Keep file validation and access decisions in the application workflow rather than confusing a matching signature with a safe upload.
Choose what happens if the user leaves the upload unfinished. An abandoned staging object should have a defined cleanup and retention path. It should not become publicly accessible merely because the upload was partially successful. Test the transition from uploaded object to accepted application record.
Build a small failure matrix
In staging with synthetic objects, test the right method, wrong method, mismatched header, expired grant, disallowed browser origin, and a user requesting another user's key. Record expected outcomes without publishing active URLs.
Compare sanitised configuration with JSON Diff when debugging origin and method changes. Use the provider's request identifiers and safe error categories for support. Remove credentials and private object names from shared examples.
For an application backed by Workers, see CPU versus wall-time diagnosis. Storage grants and function resource limits are separate boundaries, so test each one instead of guessing from the final browser error.
Conclusion
A presigned URL is a narrowly defined temporary grant. Match its method and headers, check expiry, configure browser CORS separately, and authorise the object before signing. Diagnose one boundary at a time and keep real signed URLs out of public troubleshooting tools.