如何在使用FastAPI时同时返回PDF文件和Jinja2模板响应?

huangapple go评论69阅读模式
英文:

How to return both a PDF file and a Jinja2 Template Response using FastAPI?

问题

My FastAPI应用在点击某个按钮时返回一个PDF文件。有没有办法同时返回FileResponse(starlette.responses)和Jinja2 TemplateResponse

英文:

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)

答案1

得分: 1

以下是翻译好的内容:

简单的回答是不行,而更详细的回答可能是“这取决于情况”。

简单回答的理由

通常,HTTP的工作方式是您发出一个请求,然后应该收到一个响应。基于这个前提,对于一个请求来说,服务器发送多个响应是不可能的。

此外,响应内容类型存在问题。您的服务器发送的每个响应都具有内容类型,以便客户端知道如何解释响应。返回文件的响应将使用适当的文件内容类型,返回HTML文档的响应将使用另一种适当的内容类型。对于所有响应,内容类型都包含在响应头中。

通过templates.TemplateResponse,您可能指的是从模板生成的HTML响应,是吗?如果是这样,那么您的响应将具有两种不同的内容类型,一种用于要返回的文件,一种用于HTML。这是行不通的,因为无论哪个响应首先发送,都会将响应内容类型头发送给客户端,客户端会假定随后的所有响应主体(数据)都是该类型的。

通常,在Web框架的请求处理实现中,即您的端点实现中,只能有一个返回点,随后的任何代码都不会被执行。

“这取决于情况”部分

您可以使用multipart content在一个响应中返回两个或更多内容类型。我找不到关于FastAPI或Starlette的多部分响应的示例,因此您需要自己花更多时间来实现它。我认为这应该是可能的,可以创建一个自定义响应来定义返回PDF文件和从模板生成的HTML或其他文档的顺序,并确保它们正确编码。

返回多部分响应的一个主要缺点是,根据您希望如何使用API,客户端应用程序需要知道如何处理不同类型的内容。实际上,这会使得消费您的API变得更加困难,因为客户端需要处理响应并选择在哪些情况下显示哪些内容,而不仅仅是显示文件或显示HTML,例如。

另一个缺点是,您需要以符合多部分规范的方式对返回的文件进行编码,这很可能会增加响应大小。这可能或可能不是一个问题,具体取决于您的情况,但这值得注意。

建议

我建议采用更简单的方法,并定义两个不同的端点,一个用于返回文件,另一个用于生成模板的文档。这有以下好处:

  • 您的端点实现会更简单,
  • FastAPI和Starlette的文档中都有执行这两种操作的示例,
  • 阅读您的API文档的任何人都会更清楚您的API提供了什么。

有时拥有接受相同输入但返回不同内容的端点是很常见的,所以我不认为这会成为一个问题。

英文:

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.

答案2

得分: 1

解决方案

将PDF文件编码为base64格式,并将其作为Jinja2Templates context字典中的一个键值对返回,类似于这个答案

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})

在前端显示或下载PDF文件的各种方式

1. 在页面加载时显示PDF文件

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>显示PDF文件</title>
   </head>
   <body>
      <h1>显示PDF文件</h1>
      <embed src="data:application/pdf;base64,{{ pdfFile | safe }}" width="800" height="600">
   </body>
</html>
2. 在按钮点击时显示PDF文件

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>显示PDF文件</title>
   </head>
   <body>
      <h1>显示PDF文件</h1>
      <button onclick="displayPDF()">显示PDF文件</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. 在按钮点击时下载PDF文件(参见这里

templates/display.html

<!DOCTYPE html>
<html>
   <head>
      <title>下载PDF文件</title>
   </head>
   <body>
      <h1>下载PDF文件</h1>
      <button onclick="downloadPDF()">下载PDF文件</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>

替代方案

有关替代方案,请参阅这个答案的最后一段,描述了将要返回给用户的文件保存到StaticFiles目录中的解决方案,因此可以在Jinja2模板中返回指向该文件的URL,用户可以用于下载/查看文件(但要注意,StaticFiles将对使用API的任何人可访问)。此外,请查看这个答案,其中提供了进一步的解决方案(其中一个类似于前面提到的解决方案,其中可以在Jinja2模板中返回文件的URL,但这次文件仅限用户请求它的用户使用,并在之后删除)。

英文:

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=&#39;templates&#39;)
pdf_file = &#39;path/to/file.pdf&#39;


@app.get(&#39;/&#39;)
async def get_report(request: Request):
    async with aiofiles.open(pdf_file, &#39;rb&#39;) as f:
        contents = await f.read()
        
    base64_string = base64.b64encode(contents).decode(&#39;utf-8&#39;)
    return templates.TemplateResponse(&#39;display.html&#39;, {&#39;request&#39;: request,  &#39;pdfFile&#39;: 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

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

templates/display.html

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

templates/display.html

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

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).

huangapple
  • 本文由 发表于 2023年5月7日 11:13:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/76192041.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定