Example #1
0
def patch_cache_control(headers: MutableHeaders, **kwargs: typing.Any) -> None:
    """
    Patch headers with an extended version of the initial Cache-Control header by adding
    all keyword arguments to it.
    """
    cache_control: typing.Dict[str, typing.Any] = {}
    for field in parse_http_list(headers.get("Cache-Control", "")):
        try:
            key, value = field.split("=")
        except ValueError:
            cache_control[field] = True
        else:
            cache_control[key] = value

    if "max-age" in cache_control and "max_age" in kwargs:
        kwargs["max_age"] = min(int(cache_control["max-age"]),
                                kwargs["max_age"])

    if "public" in kwargs:
        raise NotImplementedError(
            "The 'public' cache control directive isn't supported yet.")

    if "private" in kwargs:
        raise NotImplementedError(
            "The 'private' cache control directive isn't supported yet.")

    for key, value in kwargs.items():
        key = key.replace("_", "-")
        cache_control[key] = value

    directives: typing.List[str] = []
    for key, value in cache_control.items():
        if value is False:
            continue
        if value is True:
            directives.append(key)
        else:
            directives.append(f"{key}={value}")

    patched_cache_control = ", ".join(directives)

    if patched_cache_control:
        headers["Cache-Control"] = patched_cache_control
    else:
        del headers["Cache-Control"]
Example #2
0
    async def __call__(self, scope: Scope, receive: Receive,
                       send: Send) -> None:
        headers = MutableHeaders(scope=scope)
        self.should_decode_from_msgpack_to_json = ("application/x-msgpack"
                                                   in headers.get(
                                                       "content-type", ""))
        # Take an initial guess, although we eventually may not
        # be able to do the conversion.
        self.should_encode_from_json_to_msgpack = (
            "application/x-msgpack" in headers.getlist("accept"))
        self.receive = receive
        self.send = send

        if self.should_decode_from_msgpack_to_json:
            # We're going to present JSON content to the application,
            # so rewrite `Content-Type` for consistency and compliance
            # with possible downstream security checks in some frameworks.
            # See: https://github.com/florimondmanca/msgpack-asgi/issues/23
            headers["content-type"] = "application/json"

        await self.app(scope, self.receive_with_msgpack,
                       self.send_with_msgpack)
Example #3
0
    async def send_compressed(self, message: Message) -> None:
        message_type = message["type"]
        if message_type == "http.response.start":
            # Don't send the initial message until we've determined how to
            # modify the outgoing headers correctly.
            self.initial_message = message
            headers = MutableHeaders(raw=self.initial_message["headers"])
            media_type = headers.get("Content-Type")
            for encoding in self.compression_registry.encodings(media_type):
                if encoding in self.accepted:
                    file_factory = self.compression_registry.dispatch(
                        media_type=media_type, encoding=encoding)
                    self.compressed_buffer = io.BytesIO()
                    self.compressed_file = file_factory(self.compressed_buffer)
                    self.encoding = encoding
                    break
            else:
                self.encoding = None
        elif message_type == "http.response.body" and not self.started:
            headers = MutableHeaders(raw=self.initial_message["headers"])
            self.started = True
            body = message.get("body", b"")
            more_body = message.get("more_body", False)
            if len(body) < self.minimum_size and not more_body:
                # Don't apply compression to small outgoing responses.
                await self.send(self.initial_message)
                await self.send(message)
            elif not more_body:
                if self.encoding is not None:
                    # Standard (non-streaming) response.
                    t0 = time.perf_counter()
                    self.compressed_file.write(body)
                    self.compressed_file.close()
                    compression_time = time.perf_counter() - t0
                    compressed_body = self.compressed_buffer.getvalue()
                    # Check to see if the compression ratio is significant.
                    # If it isn't just send the original; the savings isn't worth the decompression time.
                    compression_ratio = len(body) / len(
                        compressed_body)  # higher is better
                    THRESHOLD = 1 / 0.9
                    if compression_ratio > THRESHOLD:
                        headers["Content-Encoding"] = self.encoding
                        headers["Content-Length"] = str(len(compressed_body))
                        headers.add_vary_header("Accept-Encoding")
                        message["body"] = compressed_body
                        # The Server-Timing middleware, which runs after this
                        # CompressionMiddleware, formats these metrics alongside
                        # others in the Server-Timing header.
                        self.scope["state"]["metrics"]["compress"] = {
                            "dur": compression_time,  # Units: seconds
                            "ratio": compression_ratio,
                        }

                await self.send(self.initial_message)
                await self.send(message)
            else:
                # Initial body in streaming response.
                if self.encoding is not None:
                    headers = MutableHeaders(
                        raw=self.initial_message["headers"])
                    headers["Content-Encoding"] = self.encoding
                    headers.add_vary_header("Accept-Encoding")
                    del headers["Content-Length"]

                    self.compressed_file.write(body)
                    message["body"] = self.compressed_buffer.getvalue()
                    self.compressed_buffer.seek(0)
                    self.compressed_buffer.truncate()

                await self.send(self.initial_message)
                await self.send(message)

        elif message_type == "http.response.body":
            # Remaining body in streaming response.
            if self.encoding is not None:
                body = message.get("body", b"")
                more_body = message.get("more_body", False)

                self.compressed_file.write(body)
                if not more_body:
                    self.compressed_file.close()

                message["body"] = self.compressed_buffer.getvalue()
                self.compressed_buffer.seek(0)
                self.compressed_buffer.truncate()

            await self.send(message)