Forcing a browser to download a file (HTML and server side)
Tags: programming, webdev, html, php
Published: 2022-08-26 15:21:00

When a user clicks a normal HTML link to a file:
```
<a href="path/to/file.ext">
```

Browsers will usually try to open the file directly (inline) instead of opening the file download dialog. 

You can use HTML5 `download` attribute to tell modern browsers to open the download dialog instead:
```
<a href="path/to/file.ext" download>
```

You can also specify the suggested filename the user will see in the download dialog:
```
<a href="path/to/file.ext" download="myfile.ext">
```

If you specify just the file name (without the extension), the extension will usually be inferred from the original filename (this example will still suggest "myfile.ext" in most browsers):
```
<a href="path/to/file.ext" download="myfile">
```

## Limitations and workarounds

Nowadays, `download` attribute is supported in all mainstream browsers, but there's an important limitation: this attribute only works for same-origin URLs (protocol, port, and host have to match), and the `blob:` and `data:` schemes.

Besides the `download` attribute, there's not much else you can do to force this behavior from the client side, but you can on the server side.

Including the `Content-Disposition` header in the HTTP response will have a similar effect to `download` attribute (but without this limitation):
```
Content-Disposition: attachment; filename=file.ext
```

You'll also need to send the `Content-type` header and the actual file contents. 

In PHP, that would look something like this:
```php
header('Content-Disposition: attachment; filename="downloaded.pdf"');
header('Content-type: application/pdf');
readfile('test.pdf');
```

But to avoid the performance overhead, I recommend doing that on the webserver level instead of the app level. 

### nginx

For `nginx` this could be as simple as:
```
add_header Content-Disposition 'attachment; filename="file.ext"';
```

Note: nginx will usually send the correct `Content-type` header automatically, but you can override that if needed:
```
add_header Content-Type 'application/pdf'
```

For a more general case, you could send a generic `application/octet-stream` header and `Content-Disposition` without a filename:
```
location /myloc {
        if ($request_filename ~* ^.*?\.(pdf|zip|docx)$) {
            add_header Content-Disposition attachment;
            add_header Content-Type application/octet-stream;
        }
    }
```

## Combination of download attribute and Content-Disposition header

If both the download attribute and Content-Disposition header are present and both define the filename, the filename defined in the header will have priority.

In the case of `Content-Disposition: inline` header (which tells the browser to display the contents `inline` - as a part of the page), the download attribute will have the priority (for the same-origin URLs).

<hr><small>
Note: This is a snapshot of the page from the <a href="https://betterways.dev">BetterWays.dev</a> wiki, you can find the latest (better formatted) version here: <a href="https://betterways.dev/forcing-a-browser-to-download-a-file">betterways.dev/forcing-a-browser-to-download-a-file</a>.
</small>