Skip to content

Upload with a presigned POST so that S3 enforces the maximum size - #222

Draft
mnapoli wants to merge 1 commit into
masterfrom
claude/laravel-bridge-presigned-post-aj0afp
Draft

mnapoli wants to merge 1 commit into
masterfrom
claude/laravel-bridge-presigned-post-aj0afp

Conversation

@mnapoli

@mnapoli mnapoli commented Nov 4, 2026

Copy link
Copy Markdown
Member

Draft: it needs a decision on backward compatibility first (see below).

Fixes the upload feature's main gap: a presigned PUT URL accepts a file of any size, only the UploadedToS3 rule checked the size after the upload. The route now returns a presigned POST form (Aws\S3\PostObjectV4) and bref-upload.js sends a multipart/form-data POST, so S3 itself rejects:

  • files larger than bref.uploads.max_size (content-length-range, 400 EntityTooLarge),
  • another content type or another key than the signed ones (403).

Checked on a real bucket (sandbox) from Chromium with bref-upload.js: 500 B and 1024 B uploads pass with a 1024 B limit, 1025 B and 5 MB fail with UploadError status 400 (not a network error), a tampered Content-Type or key gets 403, the session token of role credentials is handled.

Decisions made

  • Max size: reuses the existing bref.uploads.max_size (default 50 MB, null = no limit), now enforced by S3 too. No new config key. A per-rule UploadedToS3(maxSize:) larger than it has no effect anymore (documented).
  • Content type: exact condition on the type sent to the route (default application/octet-stream), like the agents did.
  • flysystem dependency: not changed. The signing uses the disk's S3 client (AwsS3V3Adapter::getClient()), so it still needs league/flysystem-aws-s3-v3, but so do UploadedToS3 and Storage::copy(), and the docs already install it first. Building our own S3Client would duplicate Laravel's disk config handling for no gain. A non-S3 upload disk now fails with a clear LogicException.
  • Response: fields (form fields) replaces headers, url is the bucket URL. The JS HttpClient type only needs post now.

Backward compatibility: needs your call

This is a breaking change for existing 3.1 users, more than expected:

  1. CORS: Lift's cors: <origin> shorthand (what our docs use) only allows GET, PUT, DELETE. Every bucket set up from the docs, or by Vapor, rejects the browser's POST until POST is added to its CORS rules. This hits users who import the JS from vendor/ too, i.e. uploads break on composer update.
  2. Copies made with vendor:publish --tag=bref-upload still PUT to url, which now fails.
  3. Custom clients reading headers break.

Options:

  • A. Ship as is in a minor (this PR), with the upgrade steps in the CHANGELOG. Simple, but breaks production uploads on update until CORS is changed.
  • B. Opt-in in 3.x (recommended): a config value, e.g. 'uploads.method' => 'put' by default, 'post' enables the S3 size limit; the JS POSTs when the response has fields, PUTs otherwise. Docs recommend post for new apps, v4 makes it the default and removes PUT. More code until v4, nothing breaks.
  • C. Either one, plus a Lift release adding POST to the cors shorthand, so that the documented config works after a Lift update.

The docs (file-storage.mdx, migrating-from-vapor.mdx: CORS config, "same url and headers fields as Vapor") will be updated in a bref PR once the option is chosen.

A presigned PUT URL accepts a file of any size: only the UploadedToS3 rule
checked the size, after the upload. A presigned POST with a
content-length-range condition (bref.uploads.max_size) and an exact
Content-Type condition lets S3 reject any other upload.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant