Django视图返回HttpResponse却提示返回None的问题排查
解决Django视图返回None而非HttpResponse的问题
我在Django视图中设置了按钮,触发compileUpdate函数后返回文件作为响应。该功能之前正常运行,但现在出现错误:ValueError: The view didn't return an HttpResponse object. It returned None instead.
代码逻辑为:获取对应Campaign、格式化待编译文件路径、检查指定目录(不存在则创建)、调用compileUpdate函数、返回文件作为HttpResponse。
无法正常工作的文件创建与响应代码
if req == "Bulk Update": cmp_id = request.headers.get('cmp') campaign = Campaign.objects.get(id=cmp_id) parent_dir = os.path.normpath(os.getcwd() + os.sep + os.pardir) submittedFolder = os.path.join(parent_dir, "SubmittedReviews", "", str(cmp_id) + "-" + campaign.system.name, "") cmp_files = glob.glob(os.path.join(submittedFolder,'*')) archive = os.path.join(parent_dir, "Archive", "", str(campaign.id) + "-" + campaign.system.name, "") if os.path.exists(archive) == False: os.mkdir(archive) bulk_update = os.path.join(archive, str(campaign.id) + "-" + campaign.system.name + "_bulk_update.csv") print(bulk_update) with open(bulk_update, 'w') as bulk_out: writer = csv.writer(bulk_out) compileUpdate(cmp_files, campaign, writer) bulk_out.close() time.sleep(2) file = str(bulk_update).split("/")[-1] with open(bulk_update, 'rb') as fh: response = HttpResponse(fh.read()) response['Content-Type'] = 'application/vnd.ms-excel' response['Content-Disposition'] = 'inline; filename="{}"'.format(os.path.basename(bulk_update)) response['X-Accel-Redirect'] = f'/archive/{file}' return response
正常工作的对比代码
另一个视图中类似的文件返回代码可正常运行:
if file_type == "responses": data = request.GET.getlist("campaigns")[0].split(",") campaigns = [] for item in data: cmp_id = item.split(' ')[0] campaigns.append(Campaign.objects.get(id=cmp_id)) files = compileResponses(campaigns) files = joinFiles(files) file = files[0] f = str(file).split("/")[-1] time.sleep(2) with open(file, 'rb') as fh: response = HttpResponse(fh.read()) response['Content-Type'] = 'application/vnd.ms-excel' response['Content-Disposition'] = 'inline; filename="{}"'.format(os.path.basename(file)) response['X-Accel-Redirect'] = f'/archive/{f}' return response
打印第一个响应得到<HttpResponse status_code=200, "application/vnd.ms-excel">,说明确实生成了HttpResponse对象。
前端触发脚本
触发该功能的模板脚本:
<script> function exportData(event) { var btn btn = event.target value = $(btn).parent().parent().parent().find('.export-select').val() cmp = $(btn).parent().parent().parent().parent().find('td')[0].textContent dict = {} if (value == "Bulk Update") { // Create the Post request, pass the csrf_token in the header of the request $.ajax({ url: '/campaignmanager/download/' + cmp + '/', type: 'GET', headers: {'X-CSRFtoken': '{{ csrf_token }}', 'data': 'Bulk Update', 'cmp': cmp}, data: dict, dataType: 'json' }); } else if (value == "Bulk Delete") { // Create the Post request, pass the csrf_token in the header of the request $.ajax({ url: '/campaignmanager/download/' + cmp + '/', type: 'GET', headers: {'X-CSRFtoken': '{{ csrf_token }}', 'data': 'Bulk Delete', 'cmp': cmp}, data: dict, dataType: 'json' }) } else { return false; } window.location = "/campaignmanager/download/" + cmp + "/" return false; } </script>
完整视图代码
def campaignManager(request, **kwargs): elevated = checkPerms(request) sysAdmin = checkAdmin(request) if not request.user.is_authenticated: return HttpResponseRedirect('/login') elif elevated != True and sysAdmin != True and request.user.is_superuser == False: return HttpResponseRedirect('/login') if "download" in str(request.path): req = request.headers.get('data') if req == "Bulk Update": cmp_id = request.headers.get('cmp') campaign = Campaign.objects.get(id=cmp_id) parent_dir = os.path.normpath(os.getcwd() + os.sep + os.pardir) submittedFolder = os.path.join(parent_dir, "SubmittedReviews", "", str(cmp_id) + "-" + campaign.system.name, "") cmp_files = glob.glob(os.path.join(submittedFolder,'*')) archive = os.path.join(parent_dir, "Archive", "", str(campaign.id) + "-" + campaign.system.name, "") if os.path.exists(archive) == False: os.mkdir(archive) bulk_update = os.path.join(archive, str(campaign.id) + "-" + campaign.system.name + "_bulk_update.csv") print(bulk_update) with open(bulk_update, 'w') as bulk_out: writer = csv.writer(bulk_out) compileUpdate(cmp_files, campaign, writer) bulk_out.close() with open(bulk_update, 'r') as bulk: reader = csv.DictReader(bulk) for row in reader: print(row) bulk.close() time.sleep(2) file = str(bulk_update).split("/")[-1] with open(bulk_update, 'rb') as fh: response = HttpResponse(fh.read()) response['Content-Type'] = 'application/vnd.ms-excel' response['Content-Disposition'] = 'inline; filename="{}"'.format(os.path.basename(bulk_update)) response['X-Accel-Redirect'] = f'/archive/{file}' return response elif req == "Bulk Delete": cmp_id = request.headers.get('cmp') campaign = Campaign.objects.get(id=cmp_id) pass else: # Uses campaign manager template template = loader.get_template('AccessReview/campaignManager.html') # Create empty dict for context context = {} context['user'] = employee.objects.get(pid__iexact=request.user.username) if sysAdmin == True: context["parent_template"] = "AccessReview/sysAdminBase.html" elif request.user.is_superuser == True: context["parent_template"] = "AccessReview/adminbase.html" elif elevated == True: context["parent_template"] = "AccessReview/elevatedBase.html" # Pass campaign and review instances to campaign management template context['Campaigns'] = Campaign.objects.all() context['Reviews'] = review.objects.all() context['Systems'] = System.objects.all() # Handle campaign creations through POST request if request.method == "POST": data = request.POST req = request.headers.get('data') if req == "campaign": allowed = checkPerms(request) if allowed == True: date = str(data['expectedEndDate']).split("/")[2] + "-" + str(data['expectedEndDate']).split("/")[0] + "-" + str(data['expectedEndDate']).split("/")[1] systemName = data['system'] reviewLevel = data['reviewLevel'] # Create campaign with the one required value campaign = Campaign.createCampaign(expectedEndDate=date, system=systemName, reviewLevel=reviewLevel) management.debug("%s created a campaign for %s expected to be completed by %s", request.user, systemName, date) time.sleep(1) return redirect('/campaignmanager/') else: return redirect('/campaignmanager/') return HttpResponse(template.render(context, request))
问题分析与修复方案
核心问题
前端脚本执行了两次请求:
- 先通过AJAX发送带
data和cmp头的GET请求(实际正常返回了HttpResponse,但因为设置dataType: 'json',文件流解析失败,用户无感知) - 紧接着执行
window.location跳转,发送第二次不带自定义头的GET请求
第二次请求进入视图后,req = request.headers.get('data')返回None,既不匹配Bulk Update也不匹配Bulk Delete,视图没有返回任何HttpResponse,最终返回None触发报错。
修复步骤
- 修改前端脚本:移除多余的
window.location跳转,调整AJAX配置以正确处理文件下载:
<script> function exportData(event) { event.preventDefault(); var btn = event.target; var value = $(btn).parent().parent().parent().find('.export-select').val(); var cmp = $(btn).parent().parent().parent().parent().find('td')[0].textContent; if (value == "Bulk Update" || value == "Bulk Delete") { $.ajax({ url: '/campaignmanager/download/' + cmp + '/', type: 'GET', headers: { 'X-CSRFtoken': '{{ csrf_token }}', 'data': value, 'cmp': cmp }, xhrFields: { responseType: 'blob' }, success: function(blob) { var url = window.URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = value === "Bulk Update" ? `${cmp}-bulk_update.csv` : `${cmp}-bulk_delete.csv`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } }); } return false; } </script>
- 完善视图分支返回:在
download路径逻辑中添加默认返回,避免无匹配分支时返回None:
if "download" in str(request.path): req = request.headers.get('data') if req == "Bulk Update": # 原有逻辑... return response elif req == "Bulk Delete": # 补充Bulk Delete的实现逻辑,或临时返回占位响应 return HttpResponse("Bulk Delete功能待实现", status=200) else: # 处理无效请求类型 return HttpResponseBadRequest("无效的请求类型")
- 清理冗余代码:删除
with open(...)块后的手动close()调用(with语句会自动管理文件句柄)。
内容的提问来源于stack exchange,提问作者Sogetsue
相关产品推荐
相关产品推荐

