You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django PDF下载功能异常:点击链接报404,Content-Disposition未生效

问题:Django+Gunicorn+Nginx环境下PDF下载链接404且Content-Disposition不生效

我使用Django、Gunicorn和Nginx开发功能:用户提交邮箱后,系统发送自定义PDF下载链接,点击链接应触发PDF下载。但目前点击链接持续出现「页面未找到(404)」错误,且Content-Disposition未按预期生效。


相关代码及配置

models.py

from django.db import models
import string
import random

class BrochureToken(models.Model):
    email = models.EmailField(blank=True)  
    token = models.CharField(max_length=20, unique=True)  # 随机token
    is_used = models.BooleanField(default=False)  # 标记token是否已使用

    def generate_token(self):
        length = 20
        chars = string.ascii_letters + string.digits
        self.token = ''.join(random.choice(chars) for _ in range(length))

    def save(self, *args, **kwargs):
        # 保存前如果没有token则生成
        if not self.token:
            self.generate_token()
        super().save(*args, **kwargs)

    def __str__(self):
        return self.email

views.py

class BrochreRequest(generics.CreateAPIView):
    queryset = BrochureToken.objects.all()
    serializer_class = BrochureSerializers
    permission_classes = (permissions.AllowAny,)

    def post(self, request, format=None):
        serializer = BrochureSerializers(data=request.data)
        if serializer.is_valid():
            print("Serializer is valid")
            serializer.save()
            email = request.data.get("email")
            token, created = BrochureToken.objects.get_or_create(defaults={'email': email})
            download_link = f'https://api.mysite.com/api/filedownload/brochure/{token.token}'
          
            send_mail(
                '下载OIC手册',
                f'点击链接下载手册:{download_link}',
                settings.EMAIL_HOST_USER,
                [email],
                fail_silently=False
            )
            return Response(serializer.data, status=status.HTTP_200_OK)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


def brochure_download_view(request, token):
    download_token = get_object_or_404(BrochureToken, token=token)
    if not download_token.is_used:
        download_token.is_used = True
        download_token.save()

        # 设置PDF实际路径
        file_path = 'https://api.mysite.com/media/pdfs/oic_brochure.pdf'
    
        try:
            with open(file_path, 'rb') as f:
                response = FileResponse(f, content_type='application/pdf')
                response['Content-Disposition'] = f'attachment; filename=oic_brochure.pdf'
                return response
        except FileNotFoundError:
            raise Http404("文件未找到")
        except SuspiciousFileOperation as e:
            raise PermissionError(f"打开文件错误:{e}")
        except Exception as e:
            # 记录异常用于调试
            logger.error(f"文件下载时出现意外错误:{e}")
            return HttpResponse("内部服务器错误", status=status.HTTP_500_INTERNAL_SERVER_ERROR)

应用级urls.py

from django.urls import path
from .views import brochure_download_view, BrochreRequest

app_name = 'filedownloads'

urlpatterns = [
    path('brochure/<str:token>', brochure_download_view, name="brochure"),
    path('brochure/', BrochreRequest.as_view()),
]

项目级urls.py

urlpatterns = [
    # 文件下载
    path('api/filedownload/', include('filedownloads.urls')),
]

Nginx配置(针对mysite.com)

server {
    server_name api.example.com;
    location = /favicon.ico { access_log off; log_not_found off; }
    location /statickfiles/ {
        root /var/www/oicnm-backend;
    }
    location /media/ {
        alias /var/www/api/media/;
        autoindex off;  # 禁止目录浏览
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        add_header Pragma "no-cache";
        add_header Expires 0;
        
        }

    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;

        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, PUT, OPTIONS, POST, DELETE';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Amz-Date';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'application/json';
            add_header 'Content-Length' 0;
            return 204;
        }

        if ($request_method = 'POST') {
           add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
           add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
           add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
           add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
           add_header 'Content-Type' 'application/json';
        }

       if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
     }
}

问题修复方案

1. 修复URL路径匹配问题

应用级urls.py中,brochure/<str:token>路径缺少末尾斜杠,与生成的下载链接路由规则不匹配。修改应用级urls.py:

urlpatterns = [
    path('brochure/<str:token>/', brochure_download_view, name="brochure"),
    path('brochure/', BrochreRequest.as_view()),
]

同步更新views.py中生成下载链接的代码,确保末尾带斜杠:

download_link = f'https://api.mysite.com/api/filedownload/brochure/{token.token}/'

2. 修复PDF文件读取错误

open()函数只能读取本地文件路径,无法直接读取HTTPS URL。改用本地文件路径或重定向到媒体文件:

方案一:使用本地文件路径

如果PDF存储在本地/var/www/api/media/pdfs/oic_brochure.pdf,修改file_path:

import os
from django.conf import settings

file_path = os.path.join(settings.MEDIA_ROOT, 'pdfs', 'oic_brochure.pdf')

方案二:重定向到媒体文件并设置下载头

from django.http import HttpResponseRedirect

def brochure_download_view(request, token):
    download_token = get_object_or_404(BrochureToken, token=token)
    if not download_token.is_used:
        download_token.is_used = True
        download_token.save()
        response = HttpResponseRedirect('/media/pdfs/oic_brochure.pdf')
        response['Content-Disposition'] = 'attachment; filename=oic_brochure.pdf'
        return response
    else:
        return HttpResponse("链接已失效", status=status.HTTP_410_GONE)

3. 修复Nginx的响应头暴露配置

要让前端获取到Content-Disposition头,需在Nginx的GET请求配置中添加该头:

if ($request_method = 'GET') {
    add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range,Content-Disposition' always;
}

4. 修复token创建逻辑

get_or_create调用缺少查询条件,导致无法正确根据邮箱获取或创建token,修改为:

token, created = BrochureToken.objects.get_or_create(email=email)

内容的提问来源于stack exchange,提问作者DevolamiTech

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:05:55