async def decode(self, websocket: WebSocket, message: Message) -> typing.Any: if self.encoding == "text": if "text" not in message: await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) raise RuntimeError( "Expected text websocket messages, but got bytes") return message["text"] elif self.encoding == "bytes": if "bytes" not in message: await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) raise RuntimeError( "Expected bytes websocket messages, but got text") return message["bytes"] elif self.encoding == "json": if message.get("text") is not None: text = message["text"] else: text = message["bytes"].decode("utf-8") try: return json.loads(text) except json.decoder.JSONDecodeError: await websocket.close(code=status.WS_1003_UNSUPPORTED_DATA) raise RuntimeError("Malformed JSON data received.") assert (self.encoding is None), f"Unsupported 'encoding' attribute {self.encoding}" return message["text"] if message.get("text") else message["bytes"]
async def send(message: Message) -> None: nonlocal raw_kwargs, response_started, response_complete, template, context if message["type"] == "http.response.start": assert (not response_started ), 'Received multiple "http.response.start" messages.' raw_kwargs["version"] = 11 raw_kwargs["status"] = message["status"] raw_kwargs["reason"] = _get_reason_phrase(message["status"]) raw_kwargs["headers"] = [(key.decode(), value.decode()) for key, value in message["headers"]] raw_kwargs["preload_content"] = False raw_kwargs["original_response"] = _MockOriginalResponse( raw_kwargs["headers"]) response_started = True elif message["type"] == "http.response.body": assert ( response_started ), 'Received "http.response.body" without "http.response.start".' assert ( not response_complete ), 'Received "http.response.body" after response completed.' body = message.get("body", b"") more_body = message.get("more_body", False) if request.method != "HEAD": raw_kwargs["body"].write(body) if not more_body: raw_kwargs["body"].seek(0) response_complete = True elif message["type"] == "http.response.template": template = message["template"] context = message["context"]
async def send_with_gzip(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 ougoging headers correctly. self.initial_message = message elif message_type == "http.response.body" and not self.started: 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 GZip to small outgoing responses. await self.send(self.initial_message) await self.send(message) elif not more_body: # Standard GZip response. self.gzip_file.write(body) self.gzip_file.close() body = self.gzip_buffer.getvalue() headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "gzip" headers["Content-Length"] = str(len(body)) headers.add_vary_header("Accept-Encoding") message["body"] = body await self.send(self.initial_message) await self.send(message) else: # Initial body in streaming GZip response. headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "gzip" headers.add_vary_header("Accept-Encoding") del headers["Content-Length"] self.gzip_file.write(body) message["body"] = self.gzip_buffer.getvalue() self.gzip_buffer.seek(0) self.gzip_buffer.truncate() await self.send(self.initial_message) await self.send(message) elif message_type == "http.response.body": # Remaining body in streaming GZip response. body = message.get("body", b"") more_body = message.get("more_body", False) self.gzip_file.write(body) if not more_body: self.gzip_file.close() message["body"] = self.gzip_buffer.getvalue() self.gzip_buffer.seek(0) self.gzip_buffer.truncate() await self.send(message)
async def send_with_brotli(self, message: Message) -> None: """Apply compression using brotli.""" 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 elif message_type == "http.response.body" and not self.started: 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 Brotli to small outgoing responses. await self.send(self.initial_message) await self.send(message) elif not more_body: # Standard Brotli response. body = self.br_file.process(body) + self.br_file.finish() headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "br" headers["Content-Length"] = str(len(body)) headers.add_vary_header("Accept-Encoding") message["body"] = body await self.send(self.initial_message) await self.send(message) else: # Initial body in streaming Brotli response. headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Encoding"] = "br" headers.add_vary_header("Accept-Encoding") del headers["Content-Length"] self.br_buffer.write( self.br_file.process(body) + self.br_file.flush()) message["body"] = self.br_buffer.getvalue() self.br_buffer.seek(0) self.br_buffer.truncate() await self.send(self.initial_message) await self.send(message) elif message_type == "http.response.body": # Remaining body in streaming Brotli response. body = message.get("body", b"") more_body = message.get("more_body", False) self.br_buffer.write( self.br_file.process(body) + self.br_file.flush()) if not more_body: self.br_buffer.write(self.br_file.finish()) message["body"] = self.br_buffer.getvalue() self.br_buffer.close() await self.send(message) return message["body"] = self.br_buffer.getvalue() self.br_buffer.seek(0) self.br_buffer.truncate() await self.send(message)
async def _send(message: Message): try: message.get('headers', list()).append( (b'X-Flow-ID', Hub.current.scope.transaction.to_traceparent()) ) except AttributeError: pass await send(message)
async def send_with_logging(self, message: Message) -> None: if message["type"] == "http.response.start": self._response_status_code = message.get("status") headers = Headers(raw=message["headers"]) self.should_log_response_body = "application/json" in headers.get( "content-type", "") await self.send(message) elif message["type"] == "http.response.body": if not self.should_log_response_body: await self.send(message) return body: bytes = message.get("body", b"") self._response_body.extend(body) await self.send(message)
async def wrapped_send(message: Message): if message["type"] == "http.response.start": headers = message.get("headers", []) header_value = b"default-src 'self'" if self.config.report_uri: header_value = (header_value + b"; report-uri " + self.config.report_uri) headers.append([b"Content-Security-Policy", header_value]) message["headers"] = headers await send(message)
async def send_with_tracing(message: Message) -> None: span = self.tracer.current_span() if span and message.get("type") == "http.response.start": if "status" in message: status_code: int = message["status"] span.set_tag(http_tags.STATUS_CODE, str(status_code)) if "headers" in message: response_headers = Headers(raw=message["headers"]) store_response_headers(response_headers, span, config.asgi) await send(message)
async def send_with_msgpack(self, message: Message) -> None: if not self.should_encode_from_json_to_msgpack: await self.send(message) return if message["type"] == "http.response.start": headers = Headers(raw=message["headers"]) if headers["content-type"] != "application/json": # Client accepts msgpack, but the app did not send JSON data. # (Note that it may have sent msgpack-encoded data.) self.should_encode_from_json_to_msgpack = False await self.send(message) return # Don't send the initial message until we've determined how to # modify the ougoging headers correctly. self.initial_message = message elif message["type"] == "http.response.body": assert self.should_encode_from_json_to_msgpack body = message.get("body", b"") more_body = message.get("more_body", False) if more_body: # pragma: no cover raise NotImplementedError( "Streaming the response body isn't supported yet" ) body = msgpack.packb(json.loads(body)) headers = MutableHeaders(raw=self.initial_message["headers"]) headers["Content-Type"] = "application/x-msgpack" headers["Content-Length"] = str(len(body)) message["body"] = body await self.send(self.initial_message) await self.send(message)
async def send_with_caching(self, message: Message) -> None: if not self.is_response_cachable: await self.send(message) return if message["type"] == "http.response.start": # Defer sending this message until we figured out # whether the response can be cached. self.initial_message = message return assert message["type"] == "http.response.body" if message.get("more_body", False): logger.trace("response_not_cachable reason=is_streaming") self.is_response_cachable = False await self.send(self.initial_message) await self.send(message) return assert self.request is not None body = message["body"] response = Response(content=body, status_code=self.initial_message["status"]) # NOTE: be sure not to mutate the original headers directly, as another Response # object might be holding a reference to the same list. response.raw_headers = list(self.initial_message["headers"]) try: await store_in_cache(response, request=self.request, cache=self.cache) except ResponseNotCachable: self.is_response_cachable = False else: # Apply any headers added or modified by 'store_in_cache()'. self.initial_message["headers"] = list(response.raw_headers) await self.send(self.initial_message) await self.send(message)
def _raise_on_close(self, message: Message) -> None: if message["type"] == "websocket.close": raise WebSocketDisconnect(message.get("code", 1000))
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)