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"]
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)
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)