Skip to content

feat: Add docs for DownloadHandler #4309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: latest
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 363 additions & 0 deletions articles/flow/advanced/downloads.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
---
title: Downloads
page-title: How to download from server to browser in Vaadin
description: Download a file or arbitrary content from server to browser.
meta-description: Learn how to handle download requests on server and transfer a content to browser in Vaadin Flow applications.
order: 110
---

= Handle Downloads
:toc:

To generate content dynamically and download it, there are two options:

* You can use a [classname]`DownloadHandler`, which handles HTTP requests automatically.
* You can build a custom URL using [classname]`String` type parameters.
In this case, you need one more servlet, which handles the URL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't push people to have an extra servlet for dynamic content generation.
The custom URL feels like it shouldn't be in the downloads document (even if it was from the old documentation)
At the minimum this should have a link to == Using Custom Servlet and Request Parameters


The first option is preferable, since it doesn't require an additional servlet and allows you to use data of any type from the application state.

== Introduction

The [classname]`DownloadHandler` API provides a flexible high-level abstraction to implement file and arbitrary contents downloads from server to browser in Vaadin applications.
This API supports various download scenarios, from simple file downloads to complex streaming with progress tracking.

Downloading is supported by multiple different Vaadin components such as `Anchor`, `Image`, `IFrame`, `Avatar`, `AvatarGroup`, `SvgIcon`, `MessageListItem`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would not list any components specifically and only mention that Vaadin Components support DownloadHandler where applicable.

Note that "download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component.

Check warning on line 26 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.NoteThat] Avoid using 'note that'. Raw Output: {"message": "[Vaadin.NoteThat] Avoid using 'note that'.", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 26, "column": 1}}}, "severity": "WARNING"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Note that "download" here doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element downloads a file into the browser's cache and renders it from there, e.g. `Image` component.
[NOTE]
"download" doesn't refer to only downloading a file to the user's file system, but it also covers cases where an HTML element transfers data into the browser's cache and renders it, e.g. `Image` component.


This documentation covers the main features of the `DownloadHandler` API, including:

* Static helper methods for common download scenarios
* Download progress tracking
* Creating custom download handlers
* Low-level API features
Comment on lines +28 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be direct links to the respective parts of the document


== Common Download Scenarios

The `DownloadHandler` API provides several static helper methods to simplify common download scenarios.

=== Download A Classpath Resource

Check warning on line 39 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.HeadingCase] 'Download A Classpath Resource' should be in title case. Raw Output: {"message": "[Vaadin.HeadingCase] 'Download A Classpath Resource' should be in title case.", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 39, "column": 5}}}, "severity": "WARNING"}

Check failure on line 39 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'classpath' instead of 'Classpath'. Raw Output: {"message": "[Vale.Terms] Use 'classpath' instead of 'Classpath'.", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 39, "column": 16}}}, "severity": "ERROR"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
=== Download A Classpath Resource
=== Download a Classpath Resource

I think it doesn't want A Classpath , but a Classpath instead.


The `forClassResource` method allows you to serve resources from the classpath.
For instance, for the file [filename]`src/main/resources/com/example/ui/vaadin.jpeg` and class [classname]`com.example.ui.MainView` the code would be:

[source,java]
----
Image logoImage = new Image(DownloadHandler.forClassResource(
MainView.class, "vaadin.jpeg"), "Vaadin Logo");
----

This method is useful for serving static resources like images, CSS, or JavaScript files that are packaged with your application.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could have the note that if you start the resource name with / it will then look from /src/main/resources without the class path prepended


=== Download A File From File System

The `forFile` method allows you to serve files from the server's file system.

[source,java]
----
Anchor download = new Anchor(DownloadHandler.forFile(new File("/path/to/terms-and-conditions.md")), "Download Terms and Conditions");
----

This method is useful for serving files that are stored on the server's file system.

=== Download Content from InputStream

The `fromInputStream` method allows you to serve content from any [classname]`InputStream`.
This is the most flexible helper method as it can be used with any data source that can provide an `InputStream`.

[source,java]
----
var downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> {
Anchor downloadAttachment = new Anchor(DownloadHandler.fromInputStream((event) -> {

As no other place uses var, but the actual type instead

try {
Attachment attachment = attachmentsRepository.findById(attachmentId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation should be copyable, and this introduces a non existent attachmentsRepository and a non known Attrachment object that is a cause for confusion when having low knowledge about this.

Probable questions would most likely include at least:
What/where is the repo and how is it set up? How does the find generate an Attachment and where does the attachmentId come from? What is the attachmentId type?

Perhaps it would be better to have something else as a sample for input stream instead. Perhaps to show just use a String -> InputStream

String contentFromDatabase = "Content fetched from database to be sent to client";
InputStream inputStream = new ByteArrayInputStream(contentFromDatabase.getBytes(StandardCharsets.UTF_8));
int contentLength = contentFromDatabase.getBytes(StandardCharsets.UTF_8).length;
return new DownloadResponse(inputStream, "Database data", "text/plain", contentLength );

return new DownloadResponse(attachment.getData().getBinaryStream(),
attachment.getName(), attachment.getMime(), attachment.getSize());
} catch (Exception e) {
return DownloadResponse.error(500);
}
}, "attachment.txt"), "Download attachment");
----

This method is particularly useful for:

* Serving content from databases or file storages

Check failure on line 83 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'storages'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'storages'?", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 83, "column": 42}}}, "severity": "ERROR"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Serving content from databases or file storages
* Serving content from databases or file storage

* Generating dynamic content
* Streaming large files

== Download Progress Listeners

The `DownloadHandler` API provides two ways to track download progress:

1. Using the fluent API with shorthand methods
2. Implementing the [classname]`TransferProgressListener` interface

Asynchronous UI updates in progress listeners are automatically wrapped into `UI.access()` calls by Vaadin, thus you don't need to call it manually.
Vaadin `@Push` should be enabled in your application to be able to see UI updates while download is in progress.

=== Fluent API for Progress Tracking

The fluent API provides a concise way to track download progress using method chaining.

[source,java]
----
InputStreamDownloadHandler handler = DownloadHandler.fromInputStream(event ->
new DownloadResponse(getInputStream(), "download.bin",
"application/octet-stream", contentSize))
.whenStart(() -> {
Notification.show("Download started", 3000, Notification.Position.BOTTOM_START);
progressBar.setVisible(true);
})
.onProgress((transferred, total) -> progressBar.setValue((double) transferred / total))
.whenComplete(success -> {
progressBar.setVisible(false);
if (success) {
Notification.show("Download completed", 3000, Notification.Position.BOTTOM_START)
.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
} else {
Notification.show("Download failed", 3000, Notification.Position.BOTTOM_START)
.addThemeVariants(NotificationVariant.LUMO_ERROR);
}
});
----

The fluent API provides the following methods:

* `whenStart(Runnable)`: Called when the download starts
* `onProgress(BiConsumer<Long, Long>)`: Called during the download with transferred and total bytes
* `whenComplete(Consumer<Boolean>)`: Called when the download completes successfully or with a failure

=== TransferProgressListener Interface

For more control over download progress tracking, you can implement the `TransferProgressListener` interface.

[source,java]
----
InputStreamDownloadHandler handler = DownloadHandler.fromInputStream(event ->
new DownloadResponse(getInputStream(), "download.bin",
"application/octet-stream", contentSize),
"download.bin", new TransferProgressListener() {
@Override
public void onStart(TransferContext context) {
Notification.show("Download started for file " + context.fileName(),
3000, Notification.Position.BOTTOM_START);
progressBar.setVisible(true);
}

@Override
public void onProgress(TransferContext context, long transferredBytes,
long totalBytes) {
progressBar.setValue((double) transferredBytes / totalBytes);
}

@Override
public void onError(TransferContext context, IOException reason) {
progressBar.setVisible(false);
Notification.show("Download failed, reason: " + reason.getMessage(),
3000, Notification.Position.BOTTOM_START);
}

@Override
public void onComplete(TransferContext context, long transferredBytes) {
progressBar.setVisible(false);
Notification.show("Download completed, total bytes " + transferredBytes,
3000, Notification.Position.BOTTOM_START);
}

@Override
public long progressReportInterval() {
return 1024 * 1024 * 2; // 2 MB
}
});
----

The `TransferProgressListener` interface provides the following methods:

* `onStart(TransferContext)`: Called when the download starts
* `onProgress(TransferContext, long, long)`: Called during the download with transferred and total bytes
* `onError(TransferContext, IOException)`: Called when the download fails with an exception
* `onComplete(TransferContext, long)`: Called when the download completes with the total transferred bytes
* `progressReportInterval()`: Defines how often progress updates are sent (in bytes)

The [classname]`TransferContext` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session.

Check warning on line 181 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.OxfordComma] Use the Oxford comma in 'request, response and session.'. Raw Output: {"message": "[Vaadin.OxfordComma] Use the Oxford comma in 'request, response and session.'.", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 181, "column": 170}}}, "severity": "WARNING"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TransferProgressListener example for Customized DownloadHandler as the reading from stream you should do is not apparent from the text.


== Custom Download Handlers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would probably be better before progress listeners.


For more complex download scenarios, you can create custom download handlers by implementing the `DownloadHandler` interface or extending existing implementations.

=== Implementing DownloadHandler Interface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
=== Implementing DownloadHandler Interface
=== Implementing DownloadHandler Interface Using Lambda Expression


You can implement the `DownloadHandler` interface to create a custom download handler or use a lambda.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [interfacename]DownloadHandler is a [annotationname]FunctionalInterface and can be created using a lambda expression or by creating an implementation.
Creating an implementation is needed only when overriding some of the default methods from the interface, getUrlPostfix, allowInert or getDisabledUpdateMode.


[source,java]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Samples for using a lambda expression and also for writing to the response using both event.getWriter() and event.getOutputStream() and noting that you can not use both for the same response.

----
Anchor downloadLink = new Anchor(new DownloadHandler() {
@Override
public void handleDownloadRequest(DownloadEvent event) {
// Custom download handling logic
}

@Override
public String getUrlPostfix() {
return "custom-download.txt";
}
}, "Download me!");
----

=== Custom Download Handler Example

Here's an example of a custom download handler that adds a checksum header, updates the UI and tracks the number of downloads per session:

[source,java]
----
LinkWithM5Validation link = new LinkWithM5Validation(event -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LinkWithM5Validation link = new LinkWithM5Validation(event -> {
LinkWithMD5Validation link = new LinkWithMD5Validation(event -> {

try {
var data = loadFileFromS3(event.getFileName(), event.getContentType());
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] digest = md5.digest(data);
String base64Md5 = Base64.getEncoder().encodeToString(digest);
event.getResponse().setHeader("Content-MD5", base64Md5);
Comment on lines +215 to +218
Copy link
Contributor Author

@mshabarov mshabarov May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example with calculating MD5 checksum and checking it on the client isn't perhaps a realistic case. I'd think of a better case for when setting a header is useful in download handler, feel free to share a better example if you have any.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this gives the feeling that we should perhaps have a DownloadEvent.addHeader(String, String) that would set a header to the response.

event.getResponse().getOutputStream().write(data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
event.getResponse().getOutputStream().write(data);
event.getOutputStream().write(data);

We should use the DownloadEvent api and not the response api.

event.getUI().access(() -> Notification.show(
"Download completed, number of downloads: " +
numberOfDownloads.incrementAndGet()));
event.getSession().lock();
try {
event.getSession().setAttribute("downloads-number-" + event.getFileName(),
numberOfDownloads.get());
} finally {
event.getSession().unlock();
}
} catch (NoSuchAlgorithmException | IOException e) {
event.getResponse().setStatus(500);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a DownloadEvent.setStatus(int) and DownloadEvent.setStatus(HttpStatusCode) so there would be no need to do a getResponse

}
}, "Download from S3");

private byte[] loadFileFromS3(String fileName, String contentType) {
byte[] bytes = new byte[1024 * 1024 * 10]; // 10 MB buffer
// load from file storage by file name and content type
return bytes;
}

private static class LinkWithM5Validation extends Anchor {
// JS customizations in <a> for checksum checking on the client-side
}
----

This example shows how to:

* Get file meta-data from [classname]`DownloadEvent` to load data from an external source (S3)
* Set the MD5 checksum header to the response
* Write data directly to the response output stream
* Update the UI after the download completes
* Store download statistics in the session

Note that `UI.access` is needed for updating the UI and also session locking if you want to access session.

Check warning on line 254 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.NoteThat] Avoid using 'note that'. Raw Output: {"message": "[Vaadin.NoteThat] Avoid using 'note that'.", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 254, "column": 1}}}, "severity": "WARNING"}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Note that `UI.access` is needed for updating the UI and also session locking if you want to access session.
[NOTE]
`UI.access` is needed for updating the UI and also session locking if you want access to the session.


The [classname]`DownloadEvent` provides information about the download, such as the file name, content length (if known), a reference to an owner component and Vaadin request, response and session.

== Low-Level API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
== Low-Level API
== Low-Level DownloadHandler API


The `DownloadHandler` API provides several low-level features for advanced use cases.

=== Inert Property

The `inert` property controls whether the download should be handled when the owner component is in an inert state, e.g. when a modal dialog is opened while the owner component is on the underlined page.
See the <<../advanced/server-side-modality.adoc#,Server-Side Modality>> for details.

[classname]`DownloadHandler` allows to handle download request from inert component by overriding the `allowInert()` method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[classname]`DownloadHandler` allows to handle download request from inert component by overriding the `allowInert()` method.
[classname]`DownloadHandler` allows to handle download request from inert component by overriding the [methodname]`allowInert()` method.


=== Disabled Update Mode

The [classname]`DisabledUpdateMode` controls whether downloads are allowed when the owner component is disabled.

The available modes are:

* `ONLY_WHEN_ENABLED`: Download handling is rejected when the owner component is disabled (default)
* `ALWAYS`: Download handling is allowed even when the owner component is disabled

[classname]`DownloadHandler` allows to override this mode by overriding the `getDisabledUpdateMode()` method.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[classname]`DownloadHandler` allows to override this mode by overriding the `getDisabledUpdateMode()` method.
[classname]`DownloadHandler` allows to override this mode by overriding the [methodname]`getDisabledUpdateMode()` method.


=== URL Postfix

The `getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL.
The [methodname]`getUrlPostfix()` method allows you to specify an optional URL postfix that appends application-controlled string, e.g. the logical name of the target file, to the end of the otherwise random-looking download URL.

If defined, requests that would otherwise be servable are still rejected if the postfix is missing or invalid.

Check failure on line 283 in articles/flow/advanced/downloads.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'servable'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'servable'?", "location": {"path": "articles/flow/advanced/downloads.adoc", "range": {"start": {"line": 283, "column": 46}}}, "severity": "ERROR"}

This is useful for:

* Providing a meaningful filename into the download handler callback
* Making the download request URL look more user-friendly as otherwise it is a random-looking URL

[source,java]
----
Anchor downloadLink = new Anchor(new DownloadHandler() {
@Override
public void handleDownloadRequest(DownloadEvent event) {
// download handling...
}

@Override
public boolean allowInert() {
return true; // default is false
}

@Override
public DisabledUpdateMode getDisabledUpdateMode() {
return DisabledUpdateMode.ALWAYS; // the default is ONLY_WHEN_ENABLED
}

@Override
public String getUrlPostfix() {
return "meeting-notes.txt";
}
}, "Download meeting notes");
----

== Using Custom Servlet and Request Parameters

You can create a custom servlet which handles "image" as a relative URL:

[source,java]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could have a sample with the new DownloadHandler for the exact same case to see difference between the approaches.

----
@WebServlet(urlPatterns = "/image", name = "DynamicContentServlet")
public class DynamicContentServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("image/svg+xml");
String name = req.getParameter("name");
if (name == null) {
name = "";
}
String svg = "<?xml version='1.0' encoding='UTF-8' standalone='no'?>"
+ "<svg xmlns='http://www.w3.org/2000/svg' "
+ "xmlns:xlink='http://www.w3.org/1999/xlink'>"
+ "<rect x='10' y='10' height='100' width='100' "
+ "style=' fill: #90C3D4'/><text x='30' y='30' fill='red'>"
+ name + "</text>" + "</svg>";
resp.getWriter().write(svg);
}
}
----
Comment on lines +315 to +341
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from the old article, I'd keep it just in case if someone is interested in a low level servlet way of downloading a content.


The following code should be used in the application (which has its own servlet).
It generates the resource URL on the fly, based on the current application state.
The property value of the input component is used here as a state:

[source,java]
----
Input name = new Input();

Element image = new Element("object");
image.setAttribute("type", "image/svg+xml");
image.getStyle().set("display", "block");

NativeButton button = new NativeButton("Generate Image");
button.addClickListener(event -> {
String url = "image?name=" + name.getValue();
image.setAttribute("data", url);
});

UI.getCurrent().getElement().appendChild(name.getElement(), image,
button.getElement());
----
Loading