Django attempts SSE log push and analysis of AssertionError: Hop-by-hop headers not allowed exception

Situation description

In the near future, I plan to test Django’s support for log printing, which is usually done through websocket. I want to test the server-side push of SSE (Server-sent events), which is more concise. I found that there are errors during the process:

Traceback (most recent call last):
  File "D:\Software\Anaconda3\lib\wsgiref\handlers.py", line 137, in run
    self.result = application(self.environ, self.start_response)
  File "D:\IDE Projects\Music\venv\lib\site-packages\django\contrib\staticfiles\handlers.py", line 76, in __call__
    return self.application(environ, start_response)
  File "D:\IDE Projects\Music\venv\lib\site-packages\django\core\handlers\wsgi.py", line 142, in __call__
    start_response(status, response_headers)
  File "D:\Software\Anaconda3\lib\wsgiref\handlers.py", line 249, in start_response
    assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"
AssertionError: Hop-by-hop headers not allowed

Part of the code

 response = HttpResponse(content_type='text/event-stream')
    response['Cache-Control'] = 'no-cache'
    # AssertionError: Hop-by-hop headers not allowed
    response['Connection'] = 'keep-alive'
    response['Transfer-Encoding'] = 'chunked'

Preliminary analysis

1) The error report generally means Hop-by-hop headers not allowed, which means HTTP1.0 does not support this header.

HTTP header fields will define the behavior of caching proxies and non-caching proxies, divided into 2 types.

End-to-end Header
Headers classified in this category will be forwarded to the final recipient of the request/response and must be saved in the response generated by the cache. In addition, it must be forwarded.

Hop-by-hop Header
The headers classified in this category are only valid for a single forwarding and will no longer be forwarded due to passing through cache or proxy. In HTTP/1.1 and later versions, if you want to use the hop-by-hop header, you need to provide the Connection header field.

2) Based on the stack information, find the corresponding judgment function is_hop_by_hop(name). As long as the following set elements exist in the head, it will be judged as hop_by_hop

# wsgiref\handlers.py
        if __debug__:
            for name, val in headers:
                name = self._convert_string_type(name, "Header name")
                val = self._convert_string_type(val, "Header value")
                assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed"

# handlers\wsgi.py
_hoppish = {<!-- -->
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1
}.__contains__

def is_hop_by_hop(header_name):
    """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header"""
    return _hoppish(header_name.lower())

3) So, I commented out Connection and Transfer-Encoding, and it was normal.

How to use SSE in django

(1) Front-end HTML code

<p id="info">Test</p>
    <script>
        var need_close = false;

        var eventSource = new EventSource('/sse');

        info_elm = document.getElementById("info");
// Enable connection monitoring
// Solve repeated requests (infinite reconnection by default). You can also decide whether to close based on the backend content.
        eventSource.onopen = function(){<!-- -->
            if (need_close){<!-- -->
                eventSource.close();
            }
            need_close = true;
        };

// Receive event monitoring
        eventSource.onmessage = function(event) {<!-- -->
            // Process the received event data
            info_elm.innerText = info_elm.innerText + event.data + '\\
';
        };
    </script>

(2) django

###views.py
from django.http import StreamingHttpResponse

def sse(request):
def read_file(file_name, interval=0.5):
"""
Used to read file content in real time. When there is no content, control the access frequency through interval. Do not access large files.
:param file_name: the file name to be read
:param interval: If no content is read, the dwell time interval
:return: Returns an iterator, which can be traversed to retrieve the results
"""
with open(file_name, 'r') as file:
while True:
line = file.readline()
if line:
yield line
else:
time.sleep(interval)

    def event_stream():
    #Test reading the current file
        with open('appname/views.py', encoding='utf-8') as file:
            for line in file:
            # time.sleep(1)
                yield f'data: {<!-- -->line}\\
\\
'
    return StreamingHttpResponse(event_stream(), content_type='text/event-stream', headers={<!-- -->'Cache-Control':'no-cache'})

def hello(request):
    return render(request, 'sse.html' )


### urls.py
urlpatterns = [
    path('sse', views.sse),
    path('hello', views.hello),
]

(3) Results

http://127.0.0.1:8000/sse

You can see that there is only one request, and the content is gradually refreshed.

http://127.0.0.1:8000/hello

You can see that there are three requests, the second one is an SSE request. After the end, EventSource automatically pulls up the third request. Since the variable control is configured in onopen, there will be no new pushes in the future that will cause the data to be repeatedly loaded into the front end.

(4) Unfeasible plan
The following solution is mainly because response.flush() cannot refresh the data to the client, so this is not much different from directly returning the results. It may also be that my current usage method is wrong or my understanding is not in place.

Official documentation: response.flush() This method makes an HttpResponse instance a file-like object

def sse(request):

    response = HttpResponse( content_type='text/event-stream')
    response['Cache-Control'] = 'no-cache'

    for i in range(5):
        response.write(f'data:{<!-- -->i} \\
\\
')
        response.write('id: {i} ')
        response.write(f'message: {<!-- -->datetime.datetime.now()}')
        response.write('event: send data')
        # Send data to client
        response.flush()
        time.sleep(1)
    return response

Summary

1. I found that many of the codes provided on the Internet cannot run, or are not as expected. After all, I still have to find the official website.
2. Try to look at the source code. You may be able to find the cause of the problem and some new writing methods.
3. You can use while True, then read and write the data into the loop, and then reduce the push by controlling the time interval.

Reference links:
Detailed introduction to HTTP header fields
Usage of Python __debug__ built-in variables
Introduction to SSE
W3shcool SSE
Questions about SSE shutdown
EventSource Topic