1

My FastAPI application returns a PDF file when a certain button is clicked. Is there a way to return both a FileResponse (starlette.responses) and a Jinja2 TemplateResponse at the same time?

def generate_report(request: Request, start_date: date = Form(...), end_date: Optional[date] = Form(None)):
    start_date = datetime(start_date.year, start_date.month, start_date.day)
    end_date = datetime(end_date.year, end_date.month, end_date.day)
    attendance = filter_by_date(zk, user_list, start_date, end_date)
    users_history = attendance_to_dict(attendance)
    worked = count_days(users_history, 0)
    pdf = create_pdf(users_history, worked, user_list, start_date, end_date)
    pdf_temp = "attendance.pdf"
    pdf.output(pdf_temp)
    name = "report.pdf"

    return FileResponse(pdf_temp, media_type="application/pdf", filename=name)

Chris
  • 18,724
  • 6
  • 46
  • 80
Ukhu
  • 19
  • 3
  • Sorry, for not answering yet, I'm away and will test it at night. I wasn't my intention to ignore it – Ukhu May 07 '23 at 21:50

2 Answers2

1

The simple answer is no while the more elaborate one could be it depends.

The simple answer rationale

In general, HTTP works so that you make one request for which you should get one response. Based on that premise, it would be impossible for your server to send multiple responses for one request.

Also, there is an issue with response content types. Every response your server sends has a content type so that the client knows how to interpret the response. A response that returns a file will use an appropriate file content type and a response that returns an HTML document uses the another appropriate content type. For all responses, the content type is sent in the response headers.

By templates.TemplateResponse, you probably mean an HTML response generated from a template, for example? If so, your response would have two different content types, one for the file you want to return and one for the HTML. That would not work as whichever response is sent first sends the response content type header to the client and the client assumes all of the following response body (data) is of that type.

Normally, you can only have one return point in the request handling implementation of a web framework, i.e. your endpoint implementation, and any code following the first one will not be executed.

The it depends part

You could return two or more content types in one response using multipart content. I couldn't find examples of multipart responses from neither FastAPI nor Starlette so you would have to spend more time on your own to implement it. I think this should be possible creating a custom response to define in which order you return the PDF file and the HTML or other document generated from the template and making sure they are encoded correctly.

One main downside of returning a multipart response is that, depending on how you expect your API to be used, the client applications would need to know what to do with the different types of content. In practice, this makes consuming your API harder as the clients need to process the response and choose which content to show in which cases instead of just showing the file or showing the HTML, for example.

Another downside is that you need to encode the returned file in a way that conforms the multipart specification, which would most likely increase the response size. This might or might not be an issue depending on your situation, but it's good to bear in mind.

Suggestions

I would go with a simpler approach and define two different endpoints, one for returning a file and another for the template-generated document. This has, among others, the following benefits:

  • your endpoint implementation will be simpler,
  • there are examples to do both in the documentation of FastAPI and Starlette, and
  • anyone reading your API documentation will have a better idea what your API offers as it is more clear.

It is not uncommon to have endpoints that take the same input but return different content, so I wouldn't think of that as an issue.

Jarno Lamberg
  • 1,530
  • 12
  • 12
  • Thanks a lot, it is extremely clear. I wanted to send both a HTML and a FileResponse. I wanted this because the output was in different formats but still essentially the same output. The first was a plot image showing a heatmap, and the second was a more detailed depiction of the heatmap on a pdf file. – Ukhu May 08 '23 at 00:18
1

Solution

Encode the PDF file into base64 format and return it as one of the key-value pairs in the Jinja2Templates context dictionary, similar to this answer.

app.py

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
import aiofiles
import base64

app = FastAPI()
templates = Jinja2Templates(directory='templates')
pdf_file = 'path/to/file.pdf'


@app.get('/')
async def get_report(request: Request):
    async with aiofiles.open(pdf_file, 'rb') as f:
        contents = await f.read()
        
    base64_string = base64.b64encode(contents).decode('utf-8')
    return templates.TemplateResponse('display.html', {'request': request,  'pdfFile': base64_string})

Various ways to display or download the PDF file in the frontend

1. Display the PDF file on page loading

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>Display PDF file</title>
   </head>
   <body>
      <h1>Display PDF file</h1>
      <embed src="data:application/pdf;base64,{{ pdfFile | safe }}" width="800" height="600">
   </body>
</html>
2. Display the PDF file on button click

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>Display PDF file</title>
   </head>
   <body>
      <h1>Display PDF file</h1>
      <button onclick="displayPDF()">Display PDF file</button>
      <div id="container"></div>
      <script>  
         function displayPDF() {
            var embedObj = document.createElement("embed");
            var container = document.getElementById('container');
            embedObj.src = "data:application/pdf;base64,{{ pdfFile | safe }}";
            embedObj.width = 800;
            embedObj.height = 600;
            container.append(embedObj);
         }
      </script>
   </body>
</html>
3. Download the PDF file on button click (see here)

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>Download PDF file</title>
   </head>
   <body>
      <h1>Download PDF file</h1>
      <button onclick="downloadPDF()">Download PDF file</button>
      <script>  
         function downloadPDF() {
            const a = document.createElement('a');
            a.href = "data:application/pdf;base64,{{ pdfFile | safe }}";
            a.download = 'report.pdf';
            document.body.appendChild(a);
            a.click();
            a.remove();
         }
      </script>
   </body>
</html>

Alternative solutions

For alternative solutions, please have a look at the last paragraph of this answer, which describes a solution where the file that needs to be returned to the user is saved to a StaticFiles directory, and hence, a URL pointing to that file can be returned inside the Jinja2 template, which can be used by the user to download/view the file (however, be warned that StaticFiles would be accessible by anyone using the API). Additionally, see this answer, which provides further solutions (one of which is similar to the one mentioned earlier, where you could return a URL of the file in the Jinja2 template, but, this time, the file would be accessible only from the user requesting it, and which gets deleted afterwards).

Chris
  • 18,724
  • 6
  • 46
  • 80
  • Thanks, I think I will end up doing this. Showing the pdf image instead of downloading the file. – Ukhu May 08 '23 at 00:48