Problem with downloading from dialog- 24.8

The following method sets up a DownloadHandler with a byte array and a filename, and programmatically triggers the download using JavaScript

private static void downloadFile(byte[] fileContent, String fileName) {

		DownloadHandler handler = (DownloadEvent event) -> {
			event.setFileName(fileName);
			event.getResponse().setHeader("Content-Type",
					"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
			event.getResponse().setHeader("Cache-Control", "public, max-age=3600");

			try (OutputStream out = event.getOutputStream()) {
				out.write(fileContent);
			}
		};

		Anchor downloadAnchor = new Anchor(handler, "Download Excel");

		UI.getCurrent().access(() -> {
			UI.getCurrent().add(downloadAnchor);
			downloadAnchor.getElement().executeJs("this.click(); $0.remove()", downloadAnchor.getElement());
		});
	}

This works reliably when called from within a normal view.

However, when called from within a dialog, it behaves inconsistently(works sometimes and sometimes does not) :

In firefox

http://localhost:8080/VAADIN/dynamic/resource/0/f87e9251-f7e3-40db-98a5-327a73268ec1/ might have a temporary problem or it could have moved.
Error code: 403 Forbidden

In brave

it tries to save a download.json file
Pasted image

Am I doing something wrong here or is it a Vaadin Issue?

Reproducible example:


@Route("")
public class MainView extends VerticalLayout {

	private static final long serialVersionUID = 1L;
	private static final Logger LOGGER = LoggerFactory.getLogger(MainView.class);

	public MainView() {
		List<Person> people1 = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
		List<Person> people2 = List.of(new Person("Charlie", 35));

		add(new H3("Grid 1 (Main View)"), createGrid(people1), createDownloadButton(people1, "grid1_main.xlsx"));
		add(new H3("Grid 2 (Main View)"), createGrid(people2), createDownloadButton(people2, "grid2_main.xlsx"));

		Dialog dialog = new Dialog();
		VerticalLayout dialogLayout = new VerticalLayout();
		dialogLayout.setWidth(600, Unit.PIXELS);

		dialogLayout.add(new H3("Grid 1 (Dialog)"), createGrid(people1),
				createDownloadButton(people1, "grid1_dialog.xlsx"));
		dialogLayout.add(new H3("Grid 2 (Dialog)"), createGrid(people2),
				createDownloadButton(people2, "grid2_dialog.xlsx"));

		dialog.add(dialogLayout);

		Button openDialogButton = new Button("Open Dialog", e -> dialog.open());
		add(openDialogButton);
	}

	private Grid<Person> createGrid(List<Person> people) {
		Grid<Person> grid = new Grid<>(Person.class, false);
		grid.setHeight(150, Unit.PIXELS);

		grid.addColumn(Person::getName).setHeader("Name");
		grid.addColumn(Person::getAge).setHeader("Age");
		grid.setItems(people);
		return grid;
	}

	private Button createDownloadButton(List<Person> data, String fileName) {
		return new Button("Download Excel", e -> {
			byte[] excel = createExcel(data);
			downloadFile(excel, fileName);
		});
	}

	private static byte[] createExcel(List<Person> data) {
		try (Workbook workbook = new XSSFWorkbook()) {
			Sheet sheet = workbook.createSheet("Data");
			Row header = sheet.createRow(0);
			header.createCell(0).setCellValue("Name");
			header.createCell(1).setCellValue("Age");

			for (int i = 0; i < data.size(); i++) {
				Person person = data.get(i);
				Row row = sheet.createRow(i + 1);
				row.createCell(0).setCellValue(person.getName());
				row.createCell(1).setCellValue(person.getAge());
			}

			ByteArrayOutputStream out = new ByteArrayOutputStream();
			workbook.write(out);
			return out.toByteArray();
		} catch (Exception e) {
			throw new RuntimeException("Failed to create Excel file", e);
		}
	}

	private static void downloadFile(byte[] fileContent, String fileName) {

		DownloadHandler handler = (DownloadEvent event) -> {
			event.setFileName(fileName);
			event.getResponse().setHeader("Content-Type",
					"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
			event.getResponse().setHeader("Cache-Control", "public, max-age=3600");

			try (OutputStream out = event.getOutputStream()) {
				out.write(fileContent);
			}
		};

		Anchor downloadAnchor = new Anchor(handler, "Download Excel");

		UI.getCurrent().access(() -> {
			UI.getCurrent().add(downloadAnchor);
			downloadAnchor.getElement().executeJs("this.click(); $0.remove()", downloadAnchor.getElement());
		});
	}

	public static class Person {
		private String name;
		private int age;

		public Person() {
		}

		public Person(String name, int age) {
			this.name = name;
			this.age = age;
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public int getAge() {
			return age;
		}

		public void setAge(int age) {
			this.age = age;
		}
	}
}

You have weird looking code that attaches the anchor and then automatically initiates the download. Is there some specific reason to do it this way, instead of letting user initiate the download?

The button is clicked by the user and it initiates everything else right? Am I missing something?

What you see is most likely caused by a security feature in Vaadin: when a modal dialog is open, then everything behind that dialog is disabled. This includes the request handling logic that serves the file content.

You can bypass this in multiple ways:

  • Add the anchor to the dialog instead of to the UI
  • Use with a custom DownloadHandler implementation where isAllowInert is overridden to return true
  • Make the dialog non-modal
  • Disable modality protection for the dialog by running ui.setChildComponentModal(dialog, false);

At the same time, I agree with Matti. It’s better for users if there’s a link that they can click on because they they also e.g. have the optoin to download the file again and so on.

Chose this as the solution as it’s the most feasible for our use case.

Do get the benefit of having a persistent link in case users want to download the file again. However, since in our case as grid data often changes, users are unlikely to need the same file more than once—unless the download fails.

Ah, so it is mainly the Button you want to be the UI component that triggers the download. I suggest you to implement it this way instead: Put a dummy Button inside the Anchor. This approach now probably solves those timing issues, looks less hacky and is tiny bit more efficient on the server side (file contents streamed directly to users instead of saving to server memory).


package org.test.views;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;

@Route("")
public class MainView extends VerticalLayout {

    private static final long serialVersionUID = 1L;
    private static final Logger LOGGER = LoggerFactory.getLogger(MainView.class);

    public MainView() {
        List<Person> people1 = Arrays.asList(new Person("Alice", 30), new Person("Bob", 25));
        List<Person> people2 = List.of(new Person("Charlie", 35));

        add(new H3("Grid 1 (Main View)"), createGrid(people1), createDownloadButton(people1, "grid1_main.xlsx"));
        add(new H3("Grid 2 (Main View)"), createGrid(people2), createDownloadButton(people2, "grid2_main.xlsx"));

        Dialog dialog = new Dialog();
        VerticalLayout dialogLayout = new VerticalLayout();
        dialogLayout.setWidth(600, Unit.PIXELS);

        dialogLayout.add(new H3("Grid 1 (Dialog)"), createGrid(people1),
                createDownloadButton(people1, "grid1_dialog.xlsx"));
        dialogLayout.add(new H3("Grid 2 (Dialog)"), createGrid(people2),
                createDownloadButton(people2, "grid2_dialog.xlsx"));

        dialog.add(dialogLayout);

        Button openDialogButton = new Button("Open Dialog", e -> dialog.open());
        add(openDialogButton);
    }

    private Grid<Person> createGrid(List<Person> people) {
        Grid<Person> grid = new Grid<>(Person.class, false);
        grid.setHeight(150, Unit.PIXELS);

        grid.addColumn(Person::getName).setHeader("Name");
        grid.addColumn(Person::getAge).setHeader("Age");
        grid.setItems(people);
        return grid;
    }

    private Component createDownloadButton(List<Person> data, String fileName) {
        Anchor downloadAnchor = new Anchor();
        downloadAnchor.setHref(event -> {
            event.setFileName(fileName);
            event.getResponse().setHeader("Content-Type",
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            event.getResponse().setHeader("Cache-Control", "public, max-age=3600");

            event.getOutputStream().write(createExcel(data));
        });
        downloadAnchor.add(new Button("Download Excel")); // Dummy button, NOP
        return downloadAnchor;
    }

    private static byte[] createExcel(List<Person> data) {
        return "dummycontent".getBytes();
    }

    public static class Person {
        private String name;
        private int age;

        public Person() {
        }

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

Rolf would probably throw stones at you ;) Hopefully those kind of hacks are not necessary once a DownloadButton is published by Vaadin nagging

I built a prototype of a download button. That was based on the previous “stream resource” concept rather than the new DownloadHandler but they both work the same way in the sense that you just set the handler as an element attribute and then that resolves into the URL generated for that download. Support triggering a file download when clicking a button · Issue #7203 · vaadin/flow-components · GitHub

Thanks for the suggestion! Our setup is a bit different, though:

We have multiple dynamic grids, each with its own “Download” option. The files aren’t pre-generated, since not all users need them, and user interactions can change the data at any time like selecting a row in one may update another grid. Because of this, we only generate the file on demand, when the user clicks “Download.”

A shared helper handles the file generation and programmatically triggers the anchor click.

So given how dynamic and interaction-driven our data is, I’m not sure the anchor-wrapping approach would work in our case.

The example I posted above indeeded generates the file content dynamically. The createExcell method (which I replaced with dummy implementation) is called (via the callback) only after the user clicks the download button (or technically the anchor wrapping it).

1 Like

As the option how to disable Dialog server side module is not obvious and not well documented I did PR to docs to improve: Add note and code example of disabling server modality by TatuLund · Pull Request #4404 · vaadin/docs · GitHub

Agree it could be better documented. I ran into the same issue a few years ago while implementing support for Stripe and Square card readers. Vaadin’s expert on demand helped with a solution, but I was quite stuck for a bit trying to figure out why the client request handlers stopped working.

Would your example also work for download of large files from another server? The download should be directly streamed to the download view of the browser.

You can still wrap a button in an anchor even if you have an external resource. The only difference is that you pass the URL of that resource as a String to setHref instead of the stream resource lambda overload that is used in the original example.