htmx
htmx copied to clipboard
Content-Disposition: attachment is not respected
I have a POST resource /users/user/report/PDF%20Document which responds with 200 OK and
Content-Type: application/pdf
Content-Disposition: attachment; filename="report-ele4hnxorml.pdf"
and try to use HTMX to make the request to it by adding values from some selected items in a grid:
<button
formaction="/users/user/report/PDF%20Document"
hx-boost="false"
hx-disable="true"
hx-post="users/user/report/PDF%20Document"
hx-vals="js:{items: getAllSelectedItems()}"
id="report2"
name="PDF Document"
mimetype="application/pdf"
download
>Create PDF Document report</button>
The content of the PDF is then just inserted into the DOM - rendering it as HTML (XSS risk) (while the Content-Type says it's a application/pdf) - and no download dialog is offered.
What can I do? Maybe this would be good to document in https://hypermedia.systems/.
I also struggled to download a file using htmx directly. After some research find out that is not currently supported, and end up using normal HTML and using a JavaScript function to download it (how is traditionally done):
<form action="/api/something" method="post" enctype="multipart/form-data" onsubmit="return handleSubmit(event)" >
<button type="submit">Process Files</button>
In my opinion is a very common use case that should be supported. What do you think @1cg?
I would be glad to take a look and see if I can manage to make PR for this.
A quick and dirty beginning to hook beforeSwap:
htmx.defineExtension("dl", {
onEvent: function (name, evt) {
if (name === "htmx:beforeSwap") {
const contentType = evt.detail.xhr.getResponseHeader("Content-Type");
filename = "getmesomewhere"
document.body.appendChild(Object.assign(document.createElement('a'), {
href: URL.createObjectURL(
new Blob([evt.detail.xhr.responseText], {type: contentType})
),
download: filename
})).click()
document.body.removeChild(document.body.lastChild)
}
}
});
It’s obviously incomplete. Should stop swap and only apply if target or swap matches something like file:bla.
A quick and dirty beginning to hook beforeSwap:
htmx.defineExtension("dl", { onEvent: function (name, evt) { if (name === "htmx:beforeSwap") { const contentType = evt.detail.xhr.getResponseHeader("Content-Type"); filename = "getmesomewhere" document.body.appendChild(Object.assign(document.createElement('a'), { href: URL.createObjectURL( new Blob([evt.detail.xhr.responseText], {type: contentType}) ), download: filename })).click() document.body.removeChild(document.body.lastChild) } } });It’s obviously incomplete. Should stop swap and only apply if target or swap matches something like file:bla.
It defeats a little bit the purpose of HTMX to reduce the amount of JavaScript. You could use downloads.download() which reduces the amount of code and increases readability.
But I still think HTMX should support file downloads.
Download() has a poor browser support. And would it resolve all hx attributes like hx-vals before downloading? I used this solution to resolve the whole hx processing before downloading the returned data.
Don’t know what hx should and what not. :) Never used downloads like this, but I see the use case.
Generating the a tag then simulating a click as @andryyy did is the right solution. Making HTMX support it natively isn't possible because HTMX uses AJAX. Here's my solution that doesn't use an extension and uses a different event listener. I do need to set the HX-Download header with the filename though. IMO this should be the documented approach or an official extension made @a-h
Obviously it's hardcoded to csv in my use case, but it wouldn't be hard to embed and extract the mime type from the HX-Download header.
document.addEventListener('htmx:afterRequest', function(evt) {
const xhr = evt.detail.xhr;
if (xhr.getResponseHeader('Content-Disposition') == 'attachment' && typeof xhr.getResponseHeader('HX-Download') === 'string') {
const filename = xhr.getResponseHeader('HX-Download');
const blob = new Blob([xhr.response], { type: 'text/csv' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
Download() has a poor browser support. And would it resolve all hx attributes like hx-vals before downloading? I used this solution to resolve the whole hx processing before downloading the returned data.
Don’t know what hx should and what not. :) Never used downloads like this, but I see the use case.
Hmm what do you consider to be poor browser support? I know your reply is a few months old, but the download attribute is currently at 97.19% globally. Which in my use case is good enough.