logo
podcast Podcast
get help Get Unstuck

How to force download with S3 temporary URLs

Useful with S3 and other cloud storage

Joel Clermont
Joel Clermont
2025-06-13

Let's say you want to make a file available for download in your app. This seems pretty simple at first glance, but I'm going to layer on some constraints to make it more interesting.

First, this file is something your browser knows how to display, like a PDF or an image. If you just create a link to it, the browser will display it instead of downloading it.

Maybe you're next thought is to add the download attribute to the link. Great idea! Except that only works for files on the same domain as your app, and this file is coming from a private S3 bucket using the temporaryUrl method from Laravel's storage API.

Digging into that method, you will see that you can pass an array of options, which will get passed along to the underlying S3 client.

If we look at the S3 docs for how to fetch an object, it lists some request parameters that let you control how it constructs it's HTTP response.

There are two headers in particular that will solve our problem.

Let's look at some code, and then I'll explain how it works:

Storage::disk('s3')->temporaryUrl(
    'your-object-key', 
    Carbon::now()->addHour(), 
    [
        'ResponseContentType' => 'application/octet-stream',
        'ResponseContentDisposition' => "attachment; filename=some-file-name"
    ]
);

The first parameter tells S3 to set a Content-Type header on the download response which will override the content type of the file to something the browser will not try to display.

The second parameter tells S3 to set a Content-Disposition header on the response, which will tell the browser to treat this response as an attachment with the desired file name, triggering the browser to download it.

It's important to note that these headers will only work over an HTTPS connection. This won't be a problem if you're using a cloud storage provider like S3, but it may be an issue for local development if you're not set up for it.

Here to help,

Joel

P.S. Would you like two seasoned Laravel experts to review your application code and architecture? Let's set up a time to talk.

Toss a coin in the jar if you found this helpful.
Want a tip like this in your inbox every weekday? Sign up below 👇🏼
email
No spam. Only real-world advice you can use.