[LiveComponent] Live Actions cannot handle file downloads
As live actions are treated as regular controller actions, they should also support file responses.
#[LiveAction]
public function submit(): BinaryFileResponse
{
$this->validate();
return $this->file($this->exporter->generate($this->items));
}
Currently, file downloads are not downloaded but parsed..?
Edit (current workaround)
public function submit(UriSigner $uriSigner): Response
{
$this->validate();
$file = $this->exporter->generate($this->items)
$url = $this->generateUrl('admin_download', ['file' => base64_encode($file)]);
return $this->redirect($uriSigner->sign($url));
}
Hello @richardhj
there is no support of File responses currently in LiveComponent
Couple of solutions:
- send a redirect response -> GET url of your response (what i'd do)
- use a standard action/URL instead of a live one (probably not the best there as it seems you need some form data)
- create a custom action in your Stimulus controller to handle the binary stream (but you may need to duplicate some code)
--
Some quotes cherry-picked from the RFC9110, in favor of the POST -> redirection -> GET solution
If one or more resources has been created on the origin server as a result of successfully processing a POST request, the origin server SHOULD send a 201 (Created) response containing a Location header field that provides an identifier for the primary resource created (Section 10.2.2) and a representation that describes the status of the request while referring to the new resource(s).
If the result of processing a POST would be equivalent to a representation of an existing resource, an origin server MAY redirect the user agent to that resource by sending a 303 (See Other) response with the existing resource's identifier in the Location field. This has the benefits of providing the user agent a resource identifier and transferring the representation via a method more amenable to shared caching
--
Now what seems weird to me is how the response seems handled and displayed in your example/capture... that may need some improvement / bug fixes :)
Hi @smnandre, thats valuale information, I implemented a redirect to a "download controller" đ đ
However, that redirect must be uri-signed, so I can make sure that only "authorized" files can be downloaded. Maybe there is a way, Live component may support content-disposition in the future? đ
same need here, so maybe this should be considered to be in core :thinking:
would be awesome if file response handling would work one day â€ïž
+1
+1
+1
Hey guys, this is not how OSS work and you are spamming other people's mailboxes.
Hi @stefpe, @x-vlad-x, @sebbemunich,
I understand your desire to see this feature implemented. However, as @richardhj mentioned, this is an open-source project, so you can either:
- Open a pull request (PR) to add the feature.
- Ask your company to sponsor someone to develop it.
- Make a polite request for someone else to consider working on it.
If you want to support @barbieswimcrew's suggestion, thumbing up the first message in the issue is the best way, as it allows us to sort by this.
I performed a brief test with this method, which could serve as an implementation idea. This is similar to how Livewire implements this feature.
Here are some considerations for the implementation:
- Handle readable streams if possible.
- Ensure files do not remain in memory or the DOM unnecessarily.
- Prevent the creation of large blobs (e.g., 1TB) in the DOM.
- Verify the origin of the files (only accept local streams/files, perhaps with a custom header).
- Enforce that this occurs only after a user action (not on load or after an event).
However, implementing this will take some time, which I currently do not have, as we all work on this in our free time.
(and I still believe that redirecting to a file is a much cleaner solution đ )
This option would really be very useful.
@smnandre, thank you for the following suggestion (and congrats for your place in the Symfony UX Core Team đ)
there is no support of File responses currently in LiveComponent
Couple of solutions:
* send a redirect response -> GET url of your response (what i'd do)
Just in case someone found this topic and is trying to implement a download from a LiveComponent using a return new RedirectResponse() with something similar to the following code :
#[LiveAction]
public function initiateDownload(
UrlGeneratorInterface $urlGenerator,
): Response
{
$url = $urlGenerator->generate('app_document_download');
return new RedirectResponse($url);
}
<div{{ attributes }}>
<button
data-action="live#action"
data-live-action-param="initiateDownload"
>
Export
</button>
</div>
If turbo is enabled (it is if you have initialized your symfony project with --webapp) there is a strange behaviour (or not ?) : the url will be called twice : first from the live component (type fetch) then by turbo (type document)
If you share data between the live component and the controller route with a session and you manipulate this data (add/remove), you will have unexpected results (eg : $session->remove('some_session_key')) because the data will be changed on the first call and not in the state you expect in the second call : the one that return the document.
Adding the attribute data-turbo="false" (turbo doc) to the root of your live component will bypass this behaviour
<div{{ attributes }}
data-turbo="false"
>
<button
data-action="live#action"
data-live-action-param="initiateDownload"
>
Export
</button>
</div>
The url of RedirectResponse() is now called only once
(on symfony 7.2.2 up to date at the time of this post)
Hi @zefyx! Thank you for sharing this feedback (and the kind words).
We are working on a proper implementation of downloads with LiveComponent on the PR here https://github.com/symfony/ux/pull/2483
But this is something that could be mentioned anyway on the documentation I think.
Probably just after we speak about file uploads, in a dedicated "Download" paragraph
Maybe just a paragraph ending with the HTML you provided here... ... would you have time to open a PR for this ?
Hi, my solution :
$tempFile = tempnam(sys_get_temp_dir(), 'export_');
$handle = fopen($tempFile, 'w');
//DO YOU THINGS
fclose($handle);
// Utiliser le nom du fichier temporaire comme token
$token = basename($tempFile);
// Générer l'URL de téléchargement via la route 'admin_download'
$url = $router->generate('backoffice_secure_download_csv', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
// Signer l'URL pour éviter toute altération
$signedUrl = $uriSigner->sign($url);
// Rediriger vers l'URL signée pour lancer le téléchargement
return new RedirectResponse($signedUrl);
And download :
#[Route('/download_csv', name: 'secure_download_csv')]
public function downloadCsv(Request $request, UriSigner $uriSigner): Response
{
// Vérifier que l'URL est correctement signée
if (!$uriSigner->check($request->getUri())) {
throw new AccessDeniedHttpException('Lien invalide ou expiré.');
}
// Récupérer le token depuis la query string
$token = $request->query->get('token');
if (!$token) {
throw new NotFoundHttpException('Aucun fichier spécifié.');
}
// Reconstruire le chemin du fichier temporaire
$tempFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $token;
if (!file_exists($tempFile)) {
throw new NotFoundHttpException('Le fichier n\'existe pas ou a expiré.');
}
// Créer une réponse pour le téléchargement
$response = new BinaryFileResponse($tempFile);
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'export.csv');
unlink($tempFile);
return $response;
}
In my case, I create an empty component dedicated solely to managing downloads. The advantage is that other components can use a different listener to update their views.
<div {{ attributes }} style="display: none"></div>
#[AsLiveComponent]
class Download extends AbstractController
{
use ComponentToolsTrait;
use DefaultActionTrait;
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
{
}
#[LiveListener('file:download')]
public function onFinalize(#[LiveArg] int $id): RedirectResponse
{
$url = $this->urlGenerator->generate('app_file_download', ['id' => $id]);
return new RedirectResponse($url);
}
}