Cannot get my pdf rendered - Django, HTMX and Nginx with X-Accel-Redirect header

Hello everybody,

I have been trying around with this for days now, but no matter what I do, I just cannot get it to work.

What should happen:
On the html page we have several tables (one for each patient) with a table row for each claim. Styling os done via bootstrap. If you click on one of the rows, a preview of the claim should be presented next to the tables.

What happens:
When I click on a table row, I get a these symbols (just copied the first few, it is quite long):

%PDF-1.7 %🖤 5 0 obj <> stream x��Y[��6~ϯ�X����nw)��e��-�>�N���-���$�2rd'9]��;����曋d�����*`�:��0�~�����x�����#�c\H-� 4��pƍ5j0��qD>� NCw\���:�8��?��ۨ擸(�.x�Nx���y<�Ө���ݟ���>:<�������ۗ���Y+��p apڋF@7�x�}�ӏ�����7�\���s$K����#a�K�Wd�2Wv��`E��){7��t��W����%]+�,��J��Ճpm[��L�: ������H,�:HN�Ӹ�i<%V��p�,k��!�_���@��B#���BZ�2�{o%�b��I@��:]�d<���]]��q{^�aKNi���zi�s�5�԰�� n 3Fe�� M6N���-77`�%ɭuhIpI2�d�H�4�,M�6|�vj��He��>�{sG� �Ca$����"�W(��Q�> stream xڭ�͎�HvF���ÎF�fƃ��1��1`x! ��L/l/

I think it is safe to say, my pdf is not correctly rendered (better: not rendered at all). So far, so bad. But how to fix it?

Here is the template that calls the view via htmx (in line 30)

{% extends "base.html" %}

{% block content %}


    {% if user.is_authenticated %}
        <div class="container-xl shadow p-4 mb-5 bg-body rounded">
        
            <h4 class="text-center pt-3">Hier ist eine Übersicht Ihrer Vergütungsanträge:</h4>

            <div class="container">
                <div class="row"><div class="col col-6">
                    {% for patient in patients %}
                        <h5 class="mt-5">{{ patient.last_name }}, {{patient.first_name }} geb.{{ patient.birthday|date:'d.m.Y' }}</h5>
                        <table class="table table-hover">
                            <thead class="table-light">
                                <tr>
                                    <th scope="col">vom</th>
                                    <th scope="col">bis</th>
                                    <th scope="col">erstellt am</th>
                                    <th scope="col">Summe</th>
                                    <th scope="col"></th>
                                </tr>
                            </thead>
                            <tbody>
                                {% for claim in claims %}
                                    {% if claim.patient == patient %}
                                        <tr 
                                            hx-get="{% url "show_pdf_preview" claim.id %}"
                                            hx-target="#pdf_preview"
                                        >
                                            <td>{{ claim.start_date|date:'d.m.Y' }}</td>
                                            <td>{{ claim.end_date|date:'d.m.Y' }}</td>
                                            <td>{{ claim.creation_date|date:'d.m.Y' }}</td>
                                            <td>{{ claim.sum|floatformat:"2u" }} €</td>
                                            <td></td>
                                        </tr>
                                    {% endif %}
                                {% endfor %}
                            </tbody>
                        </table>
                    {% endfor %}
                </div>
                <div id="pdf_preview" class="col col-6">
                    Klicken Sie auf eine Abrechnung, um eine Vorschau anzuzeigen!
                </div></div>
            </div>
        </div>
        

    {% else %}
        {% load static %}

        <div class="container-xl shadow p-4 mb-5 bg-body rounded">
            <center>
                <img src="/static/buckingham_guard.png" class="mb-5">

                <h1>Sorry, Sie müssen angemeldet sein um diese Seite aufzurufen</h1>
                <br>
                <a href="{% url 'login' %}">Zur Anmeldung</a>
            </center>
        </div>

    {% endif %}

{% endblock %}

and this is the view that gets called:

def show_pdf_preview(request, claim_id):
    if request.user.is_authenticated:
        claim = Claim.objects.get(id=claim_id)
        if claim.guardian == request.user.guardian:
            response = HttpResponse('<iframe src="{{ claim.pdf.url }}" allowfullscreen frameborder="0"></iframe>')
            response["X-Accel-Redirect"] = claim.pdf.url
            response["Content-Type"] = "application/pdf"
            response["Content-Disposition"] = "inline"
            return response
        return HttpResponseForbidden

and here we have the default.conf from Nginx:

upstream django {
    server django_gunicorn:8000;
}

server {
    listen 80;

    location / {
        proxy_pass http://django;
    }

    location /static/ {
        alias /static/;
    }

    location /protected/ {
        internal;
        root /;
    }
}

What I tried so far:

-I tried to change <iframe> against <embed>
-I tried to use <iframe> together with <object>
-I tried to respond only with the filename
-I tried to respond with render() and a small partial template containing the iframe and a context containing claim.pdf
-I tried to respond with render() and a small partial template containing the iframe and a context containing claim.pdf.url and changing the partial template accordingly
-I tried to respond with render() and the filename
-I tried to use django-sendfile2 from pypi
-I tried to use no headers or combine the above headers (in the view) in several ways.
-I tried usind claim.pdf.path, claim.pdf.url, claim.pdf in the f-strings

Either it did not work at all or I got the same symbols as shown above. I also looked on Google and Stackoverflow, but I could not find something that was not in the line of the already tried stuff from above.

After some thinking it looks to me as if the X-Accel-Redirect causes nginx to dismiss everything in the response except the file that it serves and then the browser cannot figure out that this is a pdf…?

I have no clue what to try next… Can anybody give me a hint?

<conjecture>
Just as an off-the-cuff reaction to this, I don’t think you want to add these headers to your response:

I believe you want your HTMX response to only be:

Which should then allow the iframe tag to retrieve the PDF as part of its normal processing. It’s the request generated by the iframe that needs to yield a response containing the pdf content type.

1 Like

I tried it and got this:

Here is the network view from the firefox devtools:

I think this makes sense because the whole purpose of the X-Accel-Redirect as far as I understand is to make nginx serve the pdf file from the protected folder instead of django serving it. I configured Nginx to treat /protected/ as an internal directory as this is what I read about protecting files from being downloaded directly.

If I only add in the X-Accel-Redirect header, i get the unrendered pdf again.

So… How can I make Nginx serve the file without sending the header?

Nope - completely different issue here - and I missed this at first. (Notice the request being made for the iframe)

You’re not rendering this response, so you can’t reference it as

You would need to do something like:

response = HttpResponse(f'<iframe src="{claim.pdf.url}" allowfullscreen frameborder="0"></iframe>')

to actually get the url inserted into the response.

1 Like

OK… I tried this…

response = HttpResponse(f'<iframe src="{claim.pdf.url}" allowfullscreen frameborder="0"></iframe>')
response["X-Accel-Redirect"] = claim.pdf.url

and this…

response = HttpResponse(f'<iframe src="{claim.pdf.url}" allowfullscreen frameborder="0"></iframe>')

in the view.

The first option gave me the unrendered pdf again, the second showed a “404 - Not Found” Error in the div. (no black box this time)

It would be helpful again if you showed the requests from the browser along with the corresponding nginx logs for these requests.

1 Like

Yesterday evening I thought about what you said earlier and I realized that you delivered the solution right away, I was just to stuck in my path to fully understand what it implied…

Which should then allow the iframe tag to retrieve the PDF as part of its normal processing. It’s the request generated by the iframe that needs to yield a response containing the pdf content type.

Basically, I am dealing with TWO requests here: The first request is to insert the html into the page via htmx. The inserted element (I settled for an <embed>) then makes ANOTHER request to the server to get the file defined in its src="". I tried to serve both requests at once which was obviously not a good idea.

I have now splitted this into two functions:
This one just serves the html to insert into the DOM:

def show_pdf_preview(request, claim_id):
    return render(request, "partials/pdf_preview.html", {"claim_id": claim_id})

This is the html that is being served:

<embed type="application/pdf" src="{% url 'show_pdf' claim_id %}#toolbar=1&navpanes=0&scrollbar=0" width="100%" height="1000" />

And when the second request is made by the <embed>, this function serves the file:

def show_pdf(request, claim_id):
    if request.user.is_authenticated:
        logging.info(f"Request from authenticated user.")
        claim = Claim.objects.get(id=claim_id)
        if claim.guardian == request.user.guardian:
            logging.info(f"Matching User and Guardian found. Serving claim {claim.id}.")
            response = HttpResponse(claim.pdf.url)
            response["Content-Type"] = "application/pdf"
            response["X-Accel-Redirect"] = claim.pdf.url
            return response

Maybe I should change the name to serve_pdf_file to clarify to future me what I was doing here. But for now, it works! Thanks once again for your help!