Django+DRF实现SSE服务端推送失败,报406错误求助
问题排查与解决
核心原因
DRF的APIView默认启用内容协商机制,会校验请求的Accept头。EventSource发起请求时,Accept头为text/event-stream,但DRF默认的渲染器(如JSONRenderer)不支持该媒体类型,因此返回406错误,同时强制将响应转为JSON格式,导致Content-Type不符合SSE要求。
另外,你的SSE格式存在问题:当前用event: 开头的事件,前端onmessage只能监听未指定event字段的默认消息,无法捕获这类带自定义事件名的内容。
解决方案
方案1:改用普通Django View(推荐)
SSE流不需要DRF的序列化、内容协商等REST API特性,直接使用普通Django View可以彻底绕过DRF的限制:
修改views.py
from django.http import StreamingHttpResponse import subprocess import select def log_stream_view(request): def stream_log(): f = subprocess.Popen(['tail', '-F', rcvenot_log], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = select.poll() p.register(f.stdout) while TEST_IN_PROGRESS: if p.poll(0.1): new_line_log = f.stdout.readline().decode('utf-8').strip() for target_line in RCV_SCENARIO: if target_line in new_line_log: # 遵循SSE标准格式:data: 开头,结尾用两个换行符分隔事件 yield f"data: {new_line_log}\n\n" return StreamingHttpResponse(stream_log(), content_type='text/event-stream')
更新urls.py
path('api/log-stream/', log_stream_view, name='log_stream'),
方案2:保留DRF APIView,关闭内容协商
如果必须使用DRF的APIView,可以关闭其内容协商机制,避免媒体类型校验:
from rest_framework.views import APIView from django.http import StreamingHttpResponse import subprocess import select class LogStreamAPIView(APIView): # 关闭DRF内容协商 content_negotiation_class = None def get(self, request): def stream_log(): f = subprocess.Popen(['tail', '-F', rcvenot_log], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = select.poll() p.register(f.stdout) while TEST_IN_PROGRESS: if p.poll(0.1): new_line_log = f.stdout.readline().decode('utf-8').strip() for target_line in RCV_SCENARIO: if target_line in new_line_log: yield f"data: {new_line_log}\n\n" return StreamingHttpResponse(stream_log(), content_type='text/event-stream')
方案3:自定义SSE渲染器(兼容DRF内容协商)
添加支持text/event-stream的自定义渲染器,让DRF识别该媒体类型:
第一步:创建自定义渲染器
from rest_framework.renderers import BaseRenderer class SSERenderer(BaseRenderer): media_type = 'text/event-stream' format = 'sse' def render(self, data, media_type=None, renderer_context=None): return data
第二步:更新APIView
from rest_framework.views import APIView from django.http import StreamingHttpResponse import subprocess import select class LogStreamAPIView(APIView): renderer_classes = [SSERenderer] def get(self, request): def stream_log(): f = subprocess.Popen(['tail', '-F', rcvenot_log], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = select.poll() p.register(f.stdout) while TEST_IN_PROGRESS: if p.poll(0.1): new_line_log = f.stdout.readline().decode('utf-8').strip() for target_line in RCV_SCENARIO: if target_line in new_line_log: yield f"data: {new_line_log}\n\n" return StreamingHttpResponse(stream_log(), content_type='text/event-stream')
前端修复
你的HTML代码存在语法错误,需修正div标签的style属性,同时添加错误监听方便调试:
<div style="width: 200px; height: 200px; overflow-y: scroll;"> <ul id="logList"></ul> </div> <script> document.addEventListener("DOMContentLoaded", function() { var logList = document.getElementById('logList'); function updateLog(newLine) { var listItem = document.createElement('li'); listItem.textContent = newLine; logList.appendChild(listItem); // 自动滚动到最新日志 logList.scrollTop = logList.scrollHeight; } var source = new EventSource('/api/log-stream/'); source.onmessage = function(event) { updateLog(event.data); }; // 监听错误,方便调试 source.onerror = function(error) { console.error('EventSource error:', error); }; }); </script>
内容的提问来源于stack exchange,提问作者Tim-Tam
相关产品推荐
相关产品推荐

