Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Uppy S3 Multipart Upload Laravel Package

tapp/laravel-uppy-s3-multipart-upload

Laravel package that provides backend endpoints for Uppy's AWS S3 multipart uploader, handling create, sign, complete, and abort requests so large files upload directly to S3 via presigned URLs with minimal server overhead.

View on GitHub
Deep Wiki
Context7

Getting Started

This package supplies the server half of Uppy's @uppy/aws-s3-multipart plugin: the endpoint set that creates an S3 multipart upload, presigns each part, completes it, aborts it, and lists parts. If you have ever hand-rolled those five routes around S3Client::createMultipartUpload / signPart / completeMultipartUpload, this replaces that plumbing.

Minimal path:

  1. composer require tapp/laravel-uppy-s3-multipart-upload — the service provider is auto-discovered.
  2. Publish the config: php artisan vendor:publish --provider="Tapp\...ServiceProvider" (check the README for the exact tag/provider string) and inspect config/uppy-s3-multipart-upload.php. The values you care about first are the S3 disk, bucket, region, presigned-URL expiry, and the route prefix / middleware the package registers.
  3. Configure a s3 disk in config/filesystems.php (or point the package at your own disk) with AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET. For MinIO/LocalStack also set endpoint and use_path_style_endpoint => true.
  4. Confirm the routes exist: php artisan route:list | grep -i uppy. You should see something shaped like POST /uppy/s3/multipart, POST /uppy/s3/multipart/{uploadId}, .../complete, DELETE /uppy/s3/multipart/{uploadId}, GET /uppy/s3/multipart/{uploadId}.
  5. On the front end, point Uppy at that base URL:
import Uppy from '@uppy/core'
import AwsS3Multipart from '@uppy/aws-s3-multipart'

const uppy = new Uppy()
uppy.use(AwsS3Multipart, {
  endpoint: '/uppy/s3/multipart',
  headers: { 'X-CSRF-TOKEN': window.csrfToken },
  // withCredentials if you rely on the session cookie
})

First use case: a single authenticated admin form uploading a large video or archive to a private bucket. Drop Uppy's Dashboard in, submit the returned key/location with your form, and verify the object server-side before saving the model. That's the whole loop and it takes well under an hour.

Read src/Http/Controllers first — the package is small, and knowing exactly what each endpoint returns is faster than guessing when Uppy's client throws a vague error.

Implementation Patterns

Treat the route group as your own. The highest-leverage move is to take the package's default routes out of the equation: wrap them in auth/verified middleware via the config, and if you need validation, publish or subclass the controller. The defaults are deliberately permissive because the package cannot know your domain rules.

Server-side key generation, not client-side. Uppy sends a filename and metadata on create. Do not let that string become the object key verbatim. Derive the key from server context — uploads/{user_id}/{uuid}/{sanitized_filename} — so a user cannot overwrite someone else's object or escape the prefix with ../.

Match Uppy's contract precisely. The plugin expects:

  • POST {endpoint} → { uploadId, key }
  • POST {endpoint}/{uploadId} → { url } for a given partNumber
  • POST {endpoint}/{uploadId}/complete → { location }
  • POST {endpoint}/{uploadId}/batch → { presignedUrls } (used when many small parts need signing)
  • DELETE {endpoint}/{uploadId}, GET {endpoint}/{uploadId}

If any of these diverge, the upload silently stalls at "uploading" with no useful console message. Diff the package's controller against the Uppy spec when something hangs.

Persist the upload identity early. Uppy's upload-success / complete callback gives you the key; store it plus uploadId on your model (a draft/pending row) so that a page reload can resume with uppy.addFile({ ... }) combined with uppy.getPlugin('AwsS3Multipart').setOptions({ uploadId, key }). Large uploads on flaky connections are the main reason people reach for multipart at all.

Chain an authorization policy onto creation. A Gate check in a custom controller — "can this user upload, and is the requested content type/size allowed?" — is the difference between a feature and an open S3 write endpoint.

Private buckets + temporary URLs. Complete the upload with ACL: private, then serve reads through Storage::disk('s3')->temporaryUrl($key, now()->addMinutes(10)). Do not expose the bucket; the presigned part URLs are already a bearer capability, and only for the duration of the upload.

Testing. Storage::fake('s3') will not exercise multipart signing. Run LocalStack or MinIO in CI and hit the package routes with $this->postJson(...), asserting on the shape (uploadId, url containing partNumber and X-Amz-Signature). Add one end-to-end test with two parts under 5 MB to catch the minimum-part-size trap.

Extension points worth knowing: the config file (disk, expiry, middleware, prefix), the S3 client binding (swap in a custom S3Client for SSE-KMS, storage class, or object lock), and the controller itself. There is no event system to speak of, so completion hooks belong in your own controller or in the form-submit handler that follows the upload.

Gotchas and Tips

Bucket CORS is the number-one failure. The browser PUTs directly to S3, bypassing your app. Configure CORS on the bucket to allow PUT (and POST/HEAD for some flows) from your origin, allow the headers Uppy sends, and — critically — ExposeHeaders: ["ETag"]. Without ETag exposed, the part upload "succeeds" but completion fails with a missing-parts error.

CSRF vs. session auth. If the routes sit behind the web middleware group, Uppy's POST/DELETE will 419 unless you send X-CSRF-TOKEN. Either add the token to Uppy's headers and include the routes in VerifyCsrfToken::$except, or register the group under api. Pick one deliberately — doing both is a common source of confusing 419s after deploy.

The default routes may be unauthenticated. Verify with route:list -v and check the middleware column. An open create endpoint lets anyone mint presigned URLs into your bucket. This is the first thing to lock down.

Presigned expiry vs. upload duration. A 30-minute expiry is fine for a 200 MB file on fiber and fatal for a 5 GB file on a train. Presign parts with a generous window (1–6 hours) and rely on part-level retries rather than shortening the TTL.

The 5 MB minimum part size. Every part except the last must be ≥5 MB. If you let users configure Uppy's chunk size and they set 1 MB, the upload dies at completion with EntityTooSmall. Also cap file size or raise the chunk size to stay under the 10,000-part limit — 5 GB at 5 MB parts is already 1,000 parts.

Newer AWS SDK checksums break S3-compatible stores. The PHP SDK now attaches CRC32 checksums by default, which some MinIO/Cloudflare R2 versions reject on presigned part URLs. If you see signature or checksum errors against a non-AWS endpoint, set request_checksum_calculation => 'when_required' on the client.

Self-hosted S3 needs a public endpoint. MinIO inside Docker resolves as http://minio:9000; the presigned URL returned to the browser contains that host and will not resolve. Set a separate public endpoint in the client config, or the browser will fail with an opaque network error while your Laravel logs look clean.

Orphaned multipart uploads cost real money. Aborted tabs leave incomplete uploads behind, and S3 bills for the parts. Add a bucket lifecycle rule: AbortIncompleteMultipartUpload after 1–7 days. Uppy's abort on uppy.close() helps, but beforeunload is not reliable.

Key sanitization. basename() plus a whitelist of characters, plus a UUID prefix. S3 keys are permissive, which is exactly why a client-supplied ../../other-user/file can be a real problem if the key is used unchecked.

Content-Type is set at create time. If you need the object to serve as video/mp4, it must be passed into the create-multipart call and preserved through completion. Passing it only from the browser and hoping the package forwards it is a common source of octet-stream objects.

Region mismatch yields AuthorizationHeaderMalformed. The bucket region must match what the SDK signs with; set it explicitly rather than relying on AWS_DEFAULT_REGION.

Debugging order: browser Network tab (which request actually failed, and is it a CORS preflight?), then storage/logs/laravel.log (the controller's exception), then aws s3api list-multipart-uploads (did the upload even get created?). That triage catches 95% of issues.

Maintenance reality check. 83 stars, zero dependents, active but sparse releases — this is a small package maintained by one agency. Pin the version in composer.json, read the controller source once, and keep your customization in a subclass rather than editing vendor/. It's a good accelerator; it is not a library you want to depend on blind across a major Laravel upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel