Django StreamingHttpResponse无法正确设置CSV文件名问题求助
问题描述
我在使用Django Rest Framework(DRF)的自定义CSVRenderer结合StreamingHttpResponse输出CSV数据时遇到了棘手问题:文件内容正常,但不同浏览器的文件名设置异常——IE下文件名为mydata.txt(文件名和扩展名均错误),Firefox下则是随机名称的.csv文件。我参考Django官方文档编写了CSVRenderer,也尝试过使用djangorestframework-csv库,但问题依旧。当前环境是Django 1.10.8、DRF 3.5.3,相关代码及settings.py中的中间件配置如下:
自定义CSVRenderer代码
import csv from rest_framework.renderers import BaseRenderer from django.http import StreamingHttpResponse class Echo(object): '''An object that implements just the write method of the file-like interface. ''' def write(self, value): '''Write the value by returning it, instead of storing in a buffer. ''' return value class CSVRenderer(BaseRenderer): '''Renderer class to generate basic csvs of the data supplied by the serializer ''' media_type = 'text/csv' format = 'csv' def render(self, data, media_type=None, renderer_context=None): rows = [] first_data_row = next(iter(data['results']), None) # Create header rows if data present if first_data_row: rows.append(list(first_data_row.keys())) # print the data to rows for row in data['results']: rows.append(list(row.values())) pseudo_buffer = Echo() writer = csv.writer(pseudo_buffer) response = StreamingHttpResponse((writer.writerow(row) for row in rows), content_type='text/csv') response['Content-Type'] = 'text/csv; name="export.csv"' response['Content-Disposition'] = 'attachment; filename="export.csv"' return response
中间件配置
MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', )
解决方案
核心问题分析
你遇到的问题根源在于违反了DRF的Renderer设计逻辑:DRF的render()方法应该返回格式对应的内容(比如字节流、字符串、生成器),而不是直接返回StreamingHttpResponse对象。当你在render()里直接返回响应对象时,DRF的响应处理链会被打乱,后续中间件或DRF自身可能修改你的响应头,导致不同浏览器解析文件名时出现异常。
另外,IE和Firefox对Content-Disposition头的解析有兼容性要求,缺少特定响应头也会导致文件名识别错误。
修正后的实现步骤
1. 修改CSVRenderer,返回内容生成器
调整CSVRenderer的逻辑,只负责生成CSV内容的迭代器,不处理响应对象:
import csv from rest_framework.renderers import BaseRenderer class Echo(object): '''An object that implements just the write method of the file-like interface. ''' def write(self, value): '''Write the value by returning it, instead of storing in a buffer. ''' return value class CSVRenderer(BaseRenderer): '''Renderer class to generate basic csvs of the data supplied by the serializer ''' media_type = 'text/csv' format = 'csv' def render(self, data, media_type=None, renderer_context=None): pseudo_buffer = Echo() writer = csv.writer(pseudo_buffer) first_data_row = next(iter(data['results']), None) # 先写入表头 if first_data_row: yield writer.writerow(list(first_data_row.keys())) # 逐个写入数据行 for row in data['results']: yield writer.writerow(list(row.values()))
2. 在视图中手动构建StreamingHttpResponse并设置兼容响应头
在API视图中,调用渲染器生成内容后,手动创建StreamingHttpResponse,并添加针对不同浏览器的兼容头:
from rest_framework.views import APIView from django.http import StreamingHttpResponse from .renderers import CSVRenderer class ExportCSVView(APIView): renderer_classes = [CSVRenderer] def get(self, request): # 替换为你实际的数据源(比如序列化后的queryset数据) data = { 'results': [ {'id': 1, 'name': 'Alice', 'age': 25}, {'id': 2, 'name': 'Bob', 'age': 30} ] } # 调用渲染器生成CSV内容迭代器 renderer = CSVRenderer() csv_content = renderer.render(data) # 构建流式响应 response = StreamingHttpResponse(csv_content, content_type='text/csv; charset=utf-8') # 设置下载文件名,兼容多浏览器 response['Content-Disposition'] = 'attachment; filename="export.csv"' # 阻止IE嗅探文件类型,避免把CSV识别为TXT response['X-Content-Type-Options'] = 'nosniff' return response
关键说明
- 不直接在Renderer中返回Response:DRF的Renderer职责是数据格式转换,而非响应对象构建,遵守这个设计逻辑可以避免响应头被意外修改。
- 添加
X-Content-Type-Options: nosniff:这是解决IE文件名错误的关键,它会强制IE使用你指定的Content-Type,而不是自行嗅探文件类型。 - 指定字符集:明确
charset=utf-8可以确保文件编码正确,同时帮助浏览器更准确识别文件类型。
如果你之前尝试过djangorestframework-csv库,也可以用类似思路:在视图中手动构建StreamingHttpResponse,并手动设置上述响应头,而非依赖库的默认响应处理。
内容的提问来源于stack exchange,提问作者DrBuck

