Hilla Endpoint with custom headers (e.g. for file download)

Hi there,

using a Hilla endpoint, can i adapt the used HTTP headers so i can create a file download? I’d like my endpoint to create a CSV dynamically upon requests, so in plain Spring i’d write something like:

ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
        .contentType(MediaType.parseMediaType("application/csv"))
        .body(inputStreamResource)

But with a hilla endpoint, i guess this won’t work. What is the preferred way to achieve e.g. such a file download scenario with a hilla endpoint?

Edit: I have just found File download endpoint, is the answer still valid that hilla does not provide a solution and one needs to fall back to a Spring REST controller? If so, can i add the download method using a Spring @GetMapping inside my Hilla endpoint class (which has other hilla endpoint methods) - so can i mix these or does the RestController need to reside in a separate class?

Thanks in advance,
Alex

Hi @alxflam,
I think the answer is still valid. Hilla does not handle file uploads and downloads in its own endpoints (BrowserCallables).
I think it is a good idea to handle file upload and download separately from Hilla endpoints. The Hilla backend is a regular Spring Boot App, which means you can easily add a common Controller (e.g. @Controller and @RequestMapping("/api")) with a method (e.g. @GetMapping("/files/{filename:.+}")) to handle the download. In the frontend, you can add a Link (<a href={/api/files/${filename}} target="_blank">Download</a>) to download the file.

2 Likes

Hi @alxflam,
I created a small example and wrote a little blog post about it: https://www.rene-wilby.de/en/blog/hilla-file-upload-download/. In case your are curious, you could have a look ;)

1 Like

Thanks a lot!
My use case is to download the data of an AutoGrid/Grid component. As that cannot be easily done by the client side (e.g. pagination, not all to be exported data may be loaded yet), there needs to be a download/export endpoint which receives the Filter parameter of the Grid and then produces the file. (nevermind for now further things to consider like which columns should be exported, e.g. the ones visible in the UI or all columns of the underlying JPA entity/properties of the DTO)

Any idea how i can get the current filter of the AutoGrid (also inside an AutoCrud)? I tried using a ref but i do not spot a property for the filter, is there any?
I could imagine such download actions for Grids to be a common use case in many applications.

I agree, I think this is very likely a common requirement. At the moment, I’m not aware how to implement this. The AutoGrid components manages the applied filters in an internal state, but I’m not sure if there is a way to access this information.

1 Like

I have opened an issue for the filter retrieval: The filter of a Grid/AutoGrid should be accessible from outside the Grid/AutoGrid · Issue #2350 · vaadin/hilla · GitHub

You could observe the last used filter by implementing a custom service that extends from the service that you’re currently using in AutoGrid, and then override the list function to store the filter somewhere. For example, assuming you have an OrderService you could do this:

import {OrderService} from 'Frontend/generated/endpoints';
import Pageable from "Frontend/generated/com/vaadin/hilla/mappedtypes/Pageable";
import Filter from "Frontend/generated/com/vaadin/hilla/crud/filter/Filter";
import {EndpointRequestInit} from "@vaadin/hilla-frontend/Connect.js";

const ObservableOrderService = {
    ...OrderService,
    list(pageable: Pageable, filter: Filter | undefined, init?: EndpointRequestInit) {
        console.log('list', pageable, filter, init);
        return OrderService.list(pageable, filter, init);
    },
}

<AutoGrid service={ObservableOrderService} />
3 Likes

I created a small example based on the suggestion of @sissbruecker. You can have a look at GitHub - rbrki07/hilla-autogrid-csv-export.

The example contains a regular BrowserCallable for orders.

These orders are displayed in an AutoGrid component in src/main/frontend/views/orders/@index.tsx.

The AutoGrid component uses the suggested ObservableOrderBrowserCallable and tracks the current filter in a local state.

In addition there is a download button, that sends the current filter to a Spring Rest-Controller in the backend.

        <Button theme='primary' onClick={async () => {
          if (filter) {
            const response = await fetch('/api/export', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json'
              },
              body: JSON.stringify(filter)
            })
            if (response.ok) {
              response.blob().then((blob) => {
                const exportFile = new File([blob], 'export.csv', { type: 'text/csv' })
                const url = window.URL.createObjectURL(exportFile);
                window.open(url, '_blank');
                URL.revokeObjectURL(url);
              })
            }
          }

The response from the Spring Rest Controller is converted to a file and opened to download in tab/window.

3 Likes

Thanks a lot @rene.wilby and @sissbruecker!

My solution is along the same lines, i only use Reacts useRef instead of useState (i think adapting the state will trigger an unnecessary re-render).

When i try testing in my Hilla 2.5.7 app with a @EnableWebSecurity configuration extending VaadinWebSecurity (and hence CSRF configuration), the POST request results in a 403.
Adding the “X-XSRF-TOKEN” to the headers of the fetch call solves that.
But the next problem is that now i get a 503, because the Filter is not resulting in the correct target class instance:

java.lang.IllegalArgumentException: Unknown filter type dev.hilla.crud.filter.Filter
dev.hilla.crud.JpaFilterConverter.toSpec(JpaFilterConverter.java:65)

So it’s not an And/OrFilter, but the super-class Filter. But the POST-body with the serialized Filter looks fine, indeed identical at first glance with what i get with your example.

I guess this issue is no longer existing with the latest development version, as your sample works just fine :slight_smile:

Regarding the XSRF-TOKEN: I’m getting the value from the XSRF-TOKEN cookie set by Hilla. I guess that’s the intended way, or does hilla already provide an API for the retrieval?

Great to hear, that you’ve made progress.

You are right, using useState will result in a re-rendering every time the state has changed. Could you share you solution based on useRef?

Using the XSRF-TOKEN cookie is an interesting approach. I don’t think there is an API to get it - the browser is the API in this case ;)

Following the separation of concern philosophy by handling Hilla’s BrowserCallables separately from the Spring Rest Controllers you should think of another way to secure the endpoint provided by the Spring Rest Controller. Instead of re-using the XSRF-TOKEN you could probably change to a JWT based authentication as it is described here: Stateless Authentication | Security | Guides | React | Hilla Docs. I think using JWT is more suitable here.

1 Like

It’s more or less the same, only with useRef:

  const filterRef = useRef<Filter | undefined>(undefined);

  const ObservableService = {
    ...MyService,
    list(
      pageable: Pageable,
      filter: Filter | undefined,
      init?: EndpointRequestInit
    ) {
      filterRef.current = filter;
      return MyService.list(pageable, filter, init);
    },
  };

And the body of the POST-request is then simply:

JSON.stringify(filterRef.current)

I already thought that reusing the token might not be really the optimal solution. I’ll have to revisit the docs and see how i can best combine secured hilla endpoints with secured Spring Rest Controllers. I’ll check the JWT doc :+1:

edit: receiving the Filter in the Rest Controller now works, i forgot to annotate the parameter with @RequestBody :roll_eyes:

1 Like