• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

django-hurricane / django-hurricane / 18352029444

08 Oct 2025 04:51PM UTC coverage: 85.397% (-0.2%) from 85.561%
18352029444

push

github

web-flow
Merge pull request #142 from django-hurricane/update-ci

chore: update ci

306 of 416 branches covered (73.56%)

Branch coverage included in aggregate %.

1384 of 1563 relevant lines covered (88.55%)

3.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.7
/hurricane/server/wsgi.py
1
from types import TracebackType
4✔
2
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
4✔
3

4
import tornado.wsgi
4✔
5
from tornado import escape, httputil
4✔
6
from tornado.ioloop import IOLoop
4✔
7

8
from hurricane.management.commands import HURRICANE_DIST_VERSION
4✔
9
from hurricane.metrics import registry
4✔
10

11

12
class HurricaneWSGIException(Exception):
4✔
13
    pass
4✔
14

15

16
class HurricaneWSGIContainer(tornado.wsgi.WSGIContainer):
4✔
17
    """
18
    Wrapper for the tornado WSGI Container, which creates a WSGI-compatible function runnable on Tornado's
19
    HTTP server. Additionally to tornado WSGI Container should be initialized with the specific handler.
20

21
    """
22

23
    def __init__(self, handler, wsgi_application, observe=True, executor=None) -> None:
4✔
24
        self.handler = handler
4✔
25
        self._observe = observe
4✔
26
        super(HurricaneWSGIContainer, self).__init__(
4✔
27
            wsgi_application, executor=executor
28
        )
29

30
    def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
4✔
31
        self.handler._status_code = status_code
4✔
32
        self.handler.application.log_request(self.handler)
4✔
33
        if self._observe:
4✔
34
            registry.get("response_time_seconds").observe(request.request_time())
4✔
35
            registry.get("path_requests_total").increment(
4✔
36
                request.method, self.handler.request.path
37
            )
38

39
    def __call__(self, request: httputil.HTTPServerRequest) -> None:
4✔
40
        IOLoop.current().spawn_callback(self.handle_request, request)
4✔
41

42
    async def handle_request(self, request: httputil.HTTPServerRequest) -> None:
4✔
43
        data: Dict[str, Any] = {}
4✔
44
        response: List[bytes] = []
4✔
45

46
        def start_response(
4✔
47
            status: str,
48
            headers: List[Tuple[str, str]],
49
            exc_info: Optional[
50
                Tuple[
51
                    "Optional[Type[BaseException]]",
52
                    Optional[BaseException],
53
                    Optional[TracebackType],
54
                ]
55
            ] = None,
56
        ) -> Callable[[bytes], Any]:
57
            data["status"] = status
4✔
58
            data["headers"] = headers
4✔
59
            return response.append
4✔
60

61
        loop = IOLoop.current()
4✔
62
        app_response = await loop.run_in_executor(
4✔
63
            self.executor,
64
            self.wsgi_application,
65
            self.environ(request),
66
            start_response,
67
        )
68
        try:
4✔
69
            app_response_iter = iter(app_response)
4✔
70

71
            def next_chunk() -> Optional[bytes]:
4✔
72
                try:
4✔
73
                    return next(app_response_iter)
4✔
74
                except StopIteration:
4✔
75
                    # StopIteration is special and is not allowed to pass through
76
                    # coroutines normally.
77
                    return None
4✔
78

79
            while True:
3✔
80
                chunk = await loop.run_in_executor(self.executor, next_chunk)
4✔
81
                if chunk is None:
4✔
82
                    break
4✔
83
                response.append(chunk)
4✔
84
        finally:
85
            if hasattr(app_response, "close"):
4✔
86
                app_response.close()  # type: ignore
4✔
87
        body = b"".join(response)
4✔
88
        if not data:
4!
89
            raise Exception("WSGI app did not call start_response")
×
90

91
        status_code_str, reason = data["status"].split(" ", 1)
4✔
92
        status_code = int(status_code_str)
4✔
93
        headers = data["headers"]  # type: List[Tuple[str, str]]
4✔
94
        header_set = set(k.lower() for (k, v) in headers)
4✔
95
        body = escape.utf8(body)
4✔
96
        if status_code != 304:
4!
97
            if "content-length" not in header_set:
4!
98
                headers.append(("Content-Length", str(len(body))))
4✔
99
            if "content-type" not in header_set:
4!
100
                headers.append(("Content-Type", "text/html; charset=UTF-8"))
×
101
        if "server" not in header_set:
4!
102
            headers.append(("Server", "Hurricane/%s" % HURRICANE_DIST_VERSION))
4✔
103

104
        start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
4✔
105
        header_obj = httputil.HTTPHeaders()
4✔
106
        for key, value in headers:
4✔
107
            sanitized_value = self._sanitize_header_value(value)
4✔
108
            if sanitized_value is None:
4!
109
                continue
×
110
            header_obj.add(key, sanitized_value)
4✔
111
        assert request.connection is not None
4✔
112
        if request.method == "HEAD":
4✔
113
            request.connection.write_headers(start_line, header_obj)
4✔
114
        else:
115
            request.connection.write_headers(start_line, header_obj, chunk=body)
4✔
116
        if self._observe:
4✔
117
            registry.metrics["response_size_bytes"].observe(len(body))
4✔
118
        request.connection.finish()
4✔
119
        self._log(status_code, request)
4✔
120

121
    @staticmethod
4✔
122
    def _sanitize_header_value(value: str) -> Optional[str]:
4✔
123
        """Normalize header values before they are passed to Tornado.
124

125
        Tornado 6.5 raises ``HTTPInputError`` for header values that contain
126
        leading whitespace or are empty. Some WSGI applications, including
127
        Django, may emit values with a leading space when removing cookies
128
        (e.g. ``" sessionid=..."``).  To keep backwards compatibility with
129
        those applications we normalise the value by stripping surrounding
130
        whitespace and dropping headers that end up empty.
131
        """
132

133
        if value is None:
4!
134
            return None
×
135
        normalized_value = value.strip()
4✔
136
        if not normalized_value:
4!
137
            return None
×
138
        return normalized_value
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc