Skip to content

HTTP/1.1

palfrey.protocols.http

HTTP/1.1 request parsing and response encoding with multiple backend support.

This module implements HTTP/1.1 protocol handling including request parsing via httptools (C-based, preferred) or h11 (pure Python fallback), ASGI scope construction, request body streaming, and response encoding with keep-alive decision logic. The module provides building blocks for keep-alive detection, 100-continue handling, chunked transfer encoding, and content-length calculations required for standards-compliant HTTP/1.1 message framing.

Key Design Decisions: - Dual-backend parser selection: httptools by default (fast C library) falls back to h11 for compatibility or when C extensions are unavailable. - Request/Response dataclasses normalize heterogeneous wire formats into consistent, Python-friendly structures (bytes for headers, chunks for body). - Keep-alive is determined by HTTP version and Connection header, not by response body completion—allowing pipelined requests to be queued immediately. - 100-continue handling is async-callback-driven for proper backpressure integration.

Key Functions
  • build_http_scope: Constructs ASGI scope dict from HTTPRequest and metadata.
  • run_http_asgi: Main coroutine cycling through request/response pairs.
  • encode_http_response: Serializes HTTPResponse to wire bytes with chunking.
  • read_http_request: Parses wire bytes into HTTPRequest dataclass.
  • should_keep_alive: Determines if connection should remain open after response.

HTTPBodyStream dataclass

Deferred reader for a fixed-size HTTP request body.

The server uses this when an application may respond before consuming the body. It keeps the public read_http_request() behavior buffered by default while allowing the connection loop to preserve request framing.

Source code in palfrey/protocols/http.py
@dataclass(slots=True)
class HTTPBodyStream:
    """
    Deferred reader for a fixed-size HTTP request body.

    The server uses this when an application may respond before consuming the
    body. It keeps the public read_http_request() behavior buffered by default
    while allowing the connection loop to preserve request framing.
    """

    reader: asyncio.StreamReader
    content_length: int
    body_limit: int
    bytes_read: int = 0
    complete: bool = False
    disconnected: bool = False
    _complete_event: asyncio.Event = field(default_factory=asyncio.Event, init=False, repr=False)

    async def receive(self) -> tuple[bytes, bool]:
        """Read the next request-body chunk and report whether more data remains."""
        if self.complete:
            return b"", False

        remaining = self.content_length - self.bytes_read
        if remaining <= 0:
            self._mark_complete()
            return b"", False

        try:
            chunk = await self.reader.readexactly(min(65_536, remaining))
        except asyncio.IncompleteReadError as exc:
            chunk = exc.partial
            self.bytes_read += len(chunk)
            self.disconnected = True
            self._mark_complete()
            return chunk, bool(chunk)

        self.bytes_read += len(chunk)
        if self.bytes_read >= self.content_length:
            self._mark_complete()
        return chunk, not self.complete

    async def drain(self) -> None:
        """Consume unread body bytes so the connection can parse the next request."""
        while not self.complete:
            await self.receive()

    async def wait_complete(self) -> None:
        """Wait until the request body has been consumed or drained."""
        await self._complete_event.wait()

    def _mark_complete(self) -> None:
        self.complete = True
        self._complete_event.set()

receive() async

Read the next request-body chunk and report whether more data remains.

Source code in palfrey/protocols/http.py
async def receive(self) -> tuple[bytes, bool]:
    """Read the next request-body chunk and report whether more data remains."""
    if self.complete:
        return b"", False

    remaining = self.content_length - self.bytes_read
    if remaining <= 0:
        self._mark_complete()
        return b"", False

    try:
        chunk = await self.reader.readexactly(min(65_536, remaining))
    except asyncio.IncompleteReadError as exc:
        chunk = exc.partial
        self.bytes_read += len(chunk)
        self.disconnected = True
        self._mark_complete()
        return chunk, bool(chunk)

    self.bytes_read += len(chunk)
    if self.bytes_read >= self.content_length:
        self._mark_complete()
    return chunk, not self.complete

drain() async

Consume unread body bytes so the connection can parse the next request.

Source code in palfrey/protocols/http.py
async def drain(self) -> None:
    """Consume unread body bytes so the connection can parse the next request."""
    while not self.complete:
        await self.receive()

wait_complete() async

Wait until the request body has been consumed or drained.

Source code in palfrey/protocols/http.py
async def wait_complete(self) -> None:
    """Wait until the request body has been consumed or drained."""
    await self._complete_event.wait()

HTTPRequest dataclass

Data container for a parsed HTTP request, including headers and body content.

This class normalizes the body representation into both a contiguous byte string and a list of chunks, facilitating easier processing for both synchronous logic and asynchronous streaming.

ATTRIBUTE DESCRIPTION
method

The HTTP method (e.g., 'GET', 'POST').

TYPE: str

target

The full request URI or path.

TYPE: str

http_version

The protocol version (e.g., 'HTTP/1.1').

TYPE: str

headers

Header pairs.

TYPE: list[tuple[str, str] | tuple[bytes, bytes]]

body

The complete request body as a single byte string.

TYPE: bytes

body_chunks

The body split into chunks, as received from the wire.

TYPE: list[bytes]

Source code in palfrey/protocols/http.py
@dataclass(slots=True)
class HTTPRequest:
    """
    Data container for a parsed HTTP request, including headers and body content.

    This class normalizes the body representation into both a contiguous byte
    string and a list of chunks, facilitating easier processing for both
    synchronous logic and asynchronous streaming.

    Attributes:
        method (str): The HTTP method (e.g., 'GET', 'POST').
        target (str): The full request URI or path.
        http_version (str): The protocol version (e.g., 'HTTP/1.1').
        headers (list[tuple[str, str] | tuple[bytes, bytes]]): Header pairs.
        body (bytes): The complete request body as a single byte string.
        body_chunks (list[bytes]): The body split into chunks, as received from the wire.
    """

    method: str
    target: str
    http_version: str
    headers: Sequence[tuple[str, str] | tuple[bytes, bytes]]
    body: bytes
    body_chunks: list[bytes] = field(default_factory=list)
    body_stream: HTTPBodyStream | None = None
    headers_normalized: bool = False
    connection_header: bytes = b""
    expect_header: bytes = b""
    is_websocket_upgrade: bool = False

    def __post_init__(self) -> None:
        """Synchronizes the body and body_chunks attributes after initialization.

        Ensures that if body_chunks is provided, the body attribute reflects the
        concatenated content. If only body is provided, body_chunks is populated
        with a single-element list containing that body.
        """
        if self.body_stream is not None:
            if self.body_chunks:
                self.body = b"".join(self.body_chunks)
            elif self.body:
                self.body_chunks = [self.body]
            return
        if self.body_chunks:
            self.body = b"".join(self.body_chunks)
            return
        self.body_chunks = [self.body] if self.body else [b""]

__post_init__()

Synchronizes the body and body_chunks attributes after initialization.

Ensures that if body_chunks is provided, the body attribute reflects the concatenated content. If only body is provided, body_chunks is populated with a single-element list containing that body.

Source code in palfrey/protocols/http.py
def __post_init__(self) -> None:
    """Synchronizes the body and body_chunks attributes after initialization.

    Ensures that if body_chunks is provided, the body attribute reflects the
    concatenated content. If only body is provided, body_chunks is populated
    with a single-element list containing that body.
    """
    if self.body_stream is not None:
        if self.body_chunks:
            self.body = b"".join(self.body_chunks)
        elif self.body:
            self.body_chunks = [self.body]
        return
    if self.body_chunks:
        self.body = b"".join(self.body_chunks)
        return
    self.body_chunks = [self.body] if self.body else [b""]

HTTPResponse dataclass

Representation of an outgoing HTTP response constructed from ASGI events.

This object acts as a collector for headers and body chunks sent by the application before they are serialized to the network socket.

ATTRIBUTE DESCRIPTION
status

HTTP status code (default 500).

TYPE: int

headers

List of header tuples in bytes.

TYPE: list[tuple[bytes, bytes]]

body_chunks

Fragments of the response body.

TYPE: list[bytes]

chunked_encoding

Whether the response uses Transfer-Encoding: chunked.

TYPE: bool

suppress_body

If True, the body is omitted (e.g., for HEAD requests).

TYPE: bool

Source code in palfrey/protocols/http.py
@dataclass(slots=True)
class HTTPResponse:
    """
    Representation of an outgoing HTTP response constructed from ASGI events.

    This object acts as a collector for headers and body chunks sent by the
    application before they are serialized to the network socket.

    Attributes:
        status (int): HTTP status code (default 500).
        headers (list[tuple[bytes, bytes]]): List of header tuples in bytes.
        body_chunks (list[bytes]): Fragments of the response body.
        chunked_encoding (bool): Whether the response uses Transfer-Encoding: chunked.
        suppress_body (bool): If True, the body is omitted (e.g., for HEAD requests).
    """

    status: int = 500
    headers: list[tuple[bytes, bytes]] = field(default_factory=list)
    body_chunks: list[bytes] = field(default_factory=list)
    chunked_encoding: bool = False
    suppress_body: bool = False
    streamed: bool = False
    close_after_response: bool = False
    connection_keep_alive: bool | None = None
    headers_metadata_trusted: bool = False
    has_content_length: bool = False
    has_transfer_encoding: bool = False
    has_connection_header: bool = False
    connection_header: bytes = b""

HTTPResponseStartedError

Bases: RuntimeError

Raised when an ASGI error occurs after response start.

Source code in palfrey/protocols/http.py
class HTTPResponseStartedError(RuntimeError):
    """Raised when an ASGI error occurs after response start."""

    def __init__(self, message: str, response: HTTPResponse) -> None:
        super().__init__(message)
        self.response = response

read_http_request(reader, *, max_head_size=1048576, body_limit=4194304, parser_mode='auto', stream_body=False) async

Reads and parses a full HTTP request (head and body) from the reader.

PARAMETER DESCRIPTION
reader

The client stream reader.

TYPE: StreamReader

max_head_size

Max allowed bytes for the request line and headers.

TYPE: int DEFAULT: 1048576

body_limit

Max allowed bytes for the body.

TYPE: int DEFAULT: 4194304

parser_mode

Choice of parser ('httptools', 'h11', or 'auto').

TYPE: str DEFAULT: 'auto'

RETURNS DESCRIPTION
HTTPRequest | None

HTTPRequest | None: The parsed request, or None if the client disconnected.

Source code in palfrey/protocols/http.py
async def read_http_request(
    reader: asyncio.StreamReader,
    *,
    max_head_size: int = 1_048_576,
    body_limit: int = 4_194_304,
    parser_mode: str = "auto",
    stream_body: bool = False,
) -> HTTPRequest | None:
    """
    Reads and parses a full HTTP request (head and body) from the reader.

    Args:
        reader (asyncio.StreamReader): The client stream reader.
        max_head_size (int): Max allowed bytes for the request line and headers.
        body_limit (int): Max allowed bytes for the body.
        parser_mode (str): Choice of parser ('httptools', 'h11', or 'auto').

    Returns:
        HTTPRequest | None: The parsed request, or None if the client disconnected.
    """
    try:
        head = await reader.readuntil(b"\r\n\r\n")
    except asyncio.LimitOverrunError as exc:
        raise ValueError("HTTP head exceeds configured limit") from exc
    except asyncio.IncompleteReadError:
        return None

    if len(head) > max_head_size:
        raise ValueError("HTTP head exceeds configured limit")

    if parser_mode == "auto":
        try:
            (
                method_raw,
                target_raw,
                version_raw,
                headers,
                content_length_raw,
                transfer_encoding,
                connection_header,
                expect_header,
                websocket_upgrade,
            ) = parse_request_head_normalized(head)
            method = method_raw.decode("latin-1")
            target = target_raw.decode("latin-1")
            version = version_raw.decode("latin-1")
        except ValueError:
            parsed_with_fallback = False
            with suppress(ValueError):
                method, target, version, raw_headers = _parse_request_head_httptools(head)
                (
                    headers,
                    content_length_raw,
                    transfer_encoding,
                    connection_header,
                    expect_header,
                    websocket_upgrade,
                ) = _prepare_request_headers(raw_headers, already_normalized=True)
                parsed_with_fallback = True
            if not parsed_with_fallback:
                method, target, version, raw_headers = _parse_request_head_h11(head)
                (
                    headers,
                    content_length_raw,
                    transfer_encoding,
                    connection_header,
                    expect_header,
                    websocket_upgrade,
                ) = _prepare_request_headers(raw_headers, already_normalized=True)
    else:
        method, target, version, raw_headers = _parse_request_head(head, parser_mode)
        (
            headers,
            content_length_raw,
            transfer_encoding,
            connection_header,
            expect_header,
            websocket_upgrade,
        ) = _prepare_request_headers(
            raw_headers,
            already_normalized=parser_mode in {"h11", "httptools"},
        )

    content_length = 0
    if content_length_raw is not None:
        try:
            content_length = int(content_length_raw)
        except ValueError as exc:
            raise ValueError("Invalid Content-Length header") from exc
        if content_length < 0:
            raise ValueError("Invalid Content-Length header")

    if content_length > body_limit:
        raise ValueError("HTTP body exceeds configured limit")

    body = b""
    body_chunks: list[bytes] = []
    body_stream: HTTPBodyStream | None = None
    if b"chunked" in transfer_encoding:
        body_chunks = await _read_chunked_body_chunks(reader, body_limit)
        body = body_chunks[0] if len(body_chunks) == 1 else b"".join(body_chunks)
    elif stream_body and content_length > 0:
        body_stream = HTTPBodyStream(reader, content_length, body_limit)
    elif content_length > 0:
        body_chunks = await _read_content_length_body_chunks(reader, content_length, body_limit)
        body = body_chunks[0] if len(body_chunks) == 1 else b"".join(body_chunks)

    if websocket_upgrade:
        request_headers: list[tuple[str, str] | tuple[bytes, bytes]] = [
            (name.decode("latin-1"), value.decode("latin-1")) for name, value in headers
        ]
        headers_normalized = False
    else:
        request_headers = cast(list[tuple[str, str] | tuple[bytes, bytes]], headers)
        headers_normalized = True

    return HTTPRequest(
        method=method,
        target=target,
        http_version=version,
        headers=request_headers,
        body=body,
        body_chunks=body_chunks,
        body_stream=body_stream,
        headers_normalized=headers_normalized,
        connection_header=connection_header,
        expect_header=expect_header,
        is_websocket_upgrade=websocket_upgrade,
    )

build_http_scope(request, *, client, server, root_path, is_tls, app_state=None, asgi_version='3.0')

Converts an internal HTTPRequest into an ASGI 3.0 scope dictionary.

PARAMETER DESCRIPTION
request

Parsed request data.

TYPE: HTTPRequest

client

IP and port of the client.

TYPE: ClientAddress

server

IP and port of the server.

TYPE: ServerAddress

root_path

The mounting point of the application.

TYPE: str

is_tls

True if connection is encrypted.

TYPE: bool

app_state

Lifespan state to shallow-copy into the per-request scope.

TYPE: dict[str, Any] | None DEFAULT: None

asgi_version

ASGI callable version reported in the scope.

TYPE: str DEFAULT: '3.0'

RETURNS DESCRIPTION
Scope

A dictionary conforming to the ASGI HTTP specification.

TYPE: Scope

Source code in palfrey/protocols/http.py
def build_http_scope(
    request: HTTPRequest,
    *,
    client: ClientAddress,
    server: ServerAddress,
    root_path: str,
    is_tls: bool,
    app_state: dict[str, Any] | None = None,
    asgi_version: str = "3.0",
) -> Scope:
    """
    Converts an internal HTTPRequest into an ASGI 3.0 scope dictionary.

    Args:
        request (HTTPRequest): Parsed request data.
        client (ClientAddress): IP and port of the client.
        server (ServerAddress): IP and port of the server.
        root_path (str): The mounting point of the application.
        is_tls (bool): True if connection is encrypted.
        app_state (dict[str, Any] | None): Lifespan state to shallow-copy into
            the per-request scope.
        asgi_version (str): ASGI callable version reported in the scope.

    Returns:
        Scope: A dictionary conforming to the ASGI HTTP specification.
    """
    path, _, query = request.target.partition("?")
    decoded_path = unquote(path) if "%" in path else path
    raw_path = path.encode("latin-1")
    if root_path:
        root_path_bytes = root_path.encode("latin-1")
        full_path = root_path + decoded_path
        full_raw_path = root_path_bytes + raw_path
    else:
        full_path = decoded_path
        full_raw_path = raw_path

    headers = request.headers
    scope_headers = (
        cast(list[tuple[bytes, bytes]], headers)
        if request.headers_normalized
        or all(
            isinstance(name, bytes) and isinstance(value, bytes) and name == name.lower()
            for name, value in headers
        )
        else [
            (_coerce_header_bytes(name).lower(), _coerce_header_bytes(value))
            for name, value in headers
        ]
    )

    return {
        "type": "http",
        "asgi": {"version": asgi_version, "spec_version": "2.3"},
        "http_version": request.http_version.removeprefix("HTTP/"),
        "method": request.method,
        "scheme": "https" if is_tls else "http",
        "path": full_path,
        "raw_path": full_raw_path,
        "query_string": query.encode("latin-1"),
        "root_path": root_path,
        "headers": scope_headers,
        "client": client,
        "server": server,
        "state": app_state.copy() if app_state else {},
    }

run_http_asgi(app, scope, request_body, *, expect_100_continue=False, on_100_continue=None, on_response_start=None, on_response_body=None) async

Orchestrates the ASGI request/response lifecycle with high-performance formatting.

This function manages the strict state machine required by the ASGI HTTP specification. It incorporates several critical performance optimizations:

  1. Single-Chunk Optimization: Defers finalizing headers until the first body chunk arrives. If the app yields a single chunk (more_body=False) without a Content-Length, it calculates it instantly to avoid the overhead of HTTP chunked transfer framing.
  2. In-Flight Chunk Framing: If Transfer-Encoding: chunked is actually required, it formats the hex framing continuously during the app's send() loop rather than post-processing.
PARAMETER DESCRIPTION
app

The user-provided ASGI application callable.

TYPE: ASGIApplication

scope

The ASGI connection scope dictionary.

TYPE: Scope

request_body

The pre-buffered input body chunks.

TYPE: bytes | list[bytes]

expect_100_continue

If the client expects a 100-Continue response.

TYPE: bool DEFAULT: False

on_100_continue

Callback to trigger the 100 status code transmission.

TYPE: Callable | None DEFAULT: None

RETURNS DESCRIPTION
HTTPResponse

The resulting parsed response state ready for wire encoding.

TYPE: HTTPResponse

RAISES DESCRIPTION
RuntimeError

If the ASGI application violates the protocol sequence or crashes.

Source code in palfrey/protocols/http.py
async def run_http_asgi(
    app: ASGIApplication,
    scope: Scope,
    request_body: bytes | list[bytes] | HTTPBodyStream,
    *,
    expect_100_continue: bool = False,
    on_100_continue: Callable[[], Awaitable[None]] | None = None,
    on_response_start: Callable[[HTTPResponse], Awaitable[None]] | None = None,
    on_response_body: Callable[[HTTPResponse, bytes, bool], Awaitable[None]] | None = None,
) -> HTTPResponse:
    """
    Orchestrates the ASGI request/response lifecycle with high-performance formatting.

    This function manages the strict state machine required by the ASGI HTTP specification.
    It incorporates several critical performance optimizations:

    1. **Single-Chunk Optimization:** Defers finalizing headers until the first body chunk arrives.
       If the app yields a single chunk (`more_body=False`) without a `Content-Length`, it calculates
       it instantly to avoid the overhead of HTTP chunked transfer framing.
    2. **In-Flight Chunk Framing:** If `Transfer-Encoding: chunked` is actually required, it formats
       the hex framing continuously during the app's `send()` loop rather than post-processing.



    Args:
        app (ASGIApplication): The user-provided ASGI application callable.
        scope (Scope): The ASGI connection scope dictionary.
        request_body (bytes | list[bytes]): The pre-buffered input body chunks.
        expect_100_continue (bool): If the client expects a 100-Continue response.
        on_100_continue (Callable | None): Callback to trigger the 100 status code transmission.

    Returns:
        HTTPResponse: The resulting parsed response state ready for wire encoding.

    Raises:
        RuntimeError: If the ASGI application violates the protocol sequence or crashes.
    """
    response = HTTPResponse()
    body_stream = request_body if isinstance(request_body, HTTPBodyStream) else None
    body_chunks = (
        []
        if body_stream is not None
        else request_body
        if isinstance(request_body, list)
        else [request_body]
    )
    if body_stream is None and not body_chunks:
        body_chunks = [b""]

    response_started = False
    response_complete = False
    waiting_for_100_continue = expect_100_continue
    body_index = 0
    message_complete: asyncio.Event | None = None

    chunked_encoding: bool | None = None
    expected_content_length = 0

    def mark_message_complete() -> None:
        if message_complete is not None:
            message_complete.set()

    async def wait_for_message_complete() -> None:
        nonlocal message_complete
        if response_complete:
            return
        if message_complete is None:
            message_complete = asyncio.Event()
        await message_complete.wait()

    async def _send_internal_server_error() -> None:
        """Sends a standard 500 Internal Server Error response.

        This is used as a safety mechanism when an ASGI application fails
        to start a response or crashes during its initial execution.
        """
        nonlocal response_started, response_complete, chunked_encoding, expected_content_length
        response_started = True
        response_complete = True
        chunked_encoding = False
        expected_content_length = 0
        response.status = 500
        response.headers = [
            (b"content-type", b"text/plain; charset=utf-8"),
            (b"content-length", b"21"),
            (b"connection", b"close"),
        ]
        response.headers_metadata_trusted = True
        response.has_content_length = True
        response.has_transfer_encoding = False
        response.has_connection_header = True
        response.connection_header = b"close"
        response.body_chunks = [] if scope.get("method") == "HEAD" else [b"Internal Server Error"]
        response.chunked_encoding = False
        response.suppress_body = scope.get("method") == "HEAD"
        mark_message_complete()

    async def receive() -> Message:
        """The ASGI receive channel for the application.

        Returns:
            Message: An 'http.request' or 'http.disconnect' message.
        """
        nonlocal waiting_for_100_continue, body_index
        if response_complete:
            return {"type": "http.disconnect"}

        if waiting_for_100_continue:
            waiting_for_100_continue = False
            if on_100_continue is not None:
                await on_100_continue()

        if body_stream is not None:
            if body_stream.disconnected:
                return {"type": "http.disconnect"}
            if not body_stream.complete:
                body, more_body = await body_stream.receive()
                if body_stream.disconnected and not body:
                    return {"type": "http.disconnect"}
                return {
                    "type": "http.request",
                    "body": body,
                    "more_body": more_body,
                }
            await wait_for_message_complete()
            return {"type": "http.disconnect"}

        if body_index < len(body_chunks):
            body = body_chunks[body_index]
            body_index += 1
            return {
                "type": "http.request",
                "body": body,
                "more_body": body_index < len(body_chunks),
            }

        await wait_for_message_complete()
        return {"type": "http.disconnect"}

    async def send(message: Message) -> None:
        """The ASGI send channel for the application.

        Args:
            message (Message): The message sent by the ASGI application.

        Raises:
            RuntimeError: If the application sends invalid message sequences
                or violates the ASGI HTTP specification.
        """
        nonlocal response_started, response_complete, waiting_for_100_continue
        nonlocal chunked_encoding, expected_content_length

        msg_type = message["type"]

        if msg_type == "http.response.start":
            if response_started:
                raise RuntimeError("ASGI message 'http.response.start' sent more than once.")

            response_started = True
            waiting_for_100_continue = False

            response.status = int(message.get("status", 200))
            _validate_response_status(response.status)
            response.headers = []
            response.headers_metadata_trusted = True
            response.has_content_length = False
            response.has_transfer_encoding = False
            response.has_connection_header = False
            response.connection_header = b""
            for raw_name, raw_value in message.get("headers", []):
                name = raw_name if isinstance(raw_name, bytes) else _coerce_header_bytes(raw_name)
                value = (
                    raw_value if isinstance(raw_value, bytes) else _coerce_header_bytes(raw_value)
                )
                _validate_response_header(name, value)
                response.headers.append((name, value))
                lowered_name = name if name.islower() else name.lower()
                if lowered_name == b"content-length":
                    response.has_content_length = True
                    expected_content_length = int(value.decode("latin-1"))
                    chunked_encoding = False
                elif lowered_name == b"transfer-encoding":
                    response.has_transfer_encoding = True
                    expected_content_length = 0
                    if value.lower() == b"chunked":
                        chunked_encoding = True
                elif lowered_name == b"connection":
                    response.has_connection_header = True
                    response.connection_header = value.lower()
            response.suppress_body = scope.get("method") == "HEAD"

            # Default to chunked if no explicit length/encoding (allows apps to stream without Content-Length)
            if (
                chunked_encoding is None
                and not response.suppress_body
                and response.status not in {204, 304}
            ):
                chunked_encoding = True
                response.headers.append((b"transfer-encoding", b"chunked"))
                response.has_transfer_encoding = True

            response.chunked_encoding = bool(chunked_encoding)

            # HEAD request overrides validation
            if response.suppress_body:
                expected_content_length = 0

            if on_response_start is not None:
                await on_response_start(response)
                response.streamed = True
            elif on_response_body is not None:
                response.streamed = True
            return

        elif msg_type == "http.response.body":
            if not response_started:
                raise RuntimeError(
                    "ASGI message 'http.response.body' sent before 'http.response.start'."
                )
            if response_complete:
                raise RuntimeError(
                    "ASGI message 'http.response.body' sent after response already completed."
                )

            body = message.get("body", b"")
            if not isinstance(body, bytes):
                body = bytes(body)
            more_body = bool(message.get("more_body", False))

            if response.suppress_body:
                pass  # HEAD requests intentionally drop the body
            elif chunked_encoding:
                if not response.streamed:
                    response.body_chunks.append(body)
            else:
                body_size = len(body)
                if body_size > expected_content_length:
                    raise RuntimeError("Response content longer than Content-Length")
                expected_content_length -= body_size
                if not response.streamed:
                    response.body_chunks.append(body)

            if response.streamed and on_response_body is not None:
                await on_response_body(response, body, more_body)

            if not more_body:
                if not chunked_encoding and expected_content_length != 0:
                    raise RuntimeError("Response content shorter than Content-Length")
                response_complete = True
                mark_message_complete()
        else:
            raise RuntimeError(f"Unexpected ASGI message type: '{msg_type}'.")

    try:
        result = await app(scope, receive, send)
    except BaseException as exc:
        if not response_started:
            await _send_internal_server_error()
        elif isinstance(exc, HTTPResponseStartedError):
            raise
        elif isinstance(exc, RuntimeError):
            raise HTTPResponseStartedError(str(exc), response) from exc
        else:
            raise HTTPResponseStartedError("Exception in ASGI application", response) from exc
    else:
        if result is not None:
            message = f"ASGI callable should return None, but returned '{result}'."
            if response_started:
                raise HTTPResponseStartedError(message, response)
            raise RuntimeError(message)
        if not response_started:
            await _send_internal_server_error()
        elif not response_complete:
            raise HTTPResponseStartedError(
                "ASGI callable returned without completing response.",
                response,
            )

    return response

append_default_response_headers(response, config, *, default_headers=None)

Applies configured default headers to the response.

Injects headers like 'Server' and 'Date' if enabled and not already present.

PARAMETER DESCRIPTION
response

The response object to update.

TYPE: HTTPResponse

config

Application configuration.

TYPE: PalfreyConfig

default_headers

Cached list of headers for fast-path insertion.

TYPE: list | None DEFAULT: None

Source code in palfrey/protocols/http.py
def append_default_response_headers(
    response: HTTPResponse,
    config: PalfreyConfig,
    *,
    default_headers: list[tuple[bytes, bytes]] | None = None,
) -> None:
    """Applies configured default headers to the response.

    Injects headers like 'Server' and 'Date' if enabled and not already present.

    Args:
        response (HTTPResponse): The response object to update.
        config (PalfreyConfig): Application configuration.
        default_headers (list | None): Cached list of headers for fast-path insertion.
    """
    if default_headers is not None:
        if not response.headers:
            for name, value in default_headers:
                _append_response_header(response, name, value)
            return

        if len(default_headers) == 2:
            first_name, first_value = default_headers[0]
            second_name, second_value = default_headers[1]
            has_first = False
            has_second = False
            for name, _value in response.headers:
                if name == first_name:
                    has_first = True
                elif name == second_name:
                    has_second = True
                elif not name.islower():
                    lowered_name = name.lower()
                    if lowered_name == first_name:
                        has_first = True
                    elif lowered_name == second_name:
                        has_second = True
                if has_first and has_second:
                    break
            if not has_first:
                _append_response_header(response, first_name, first_value)
            if not has_second:
                _append_response_header(response, second_name, second_value)
            return

        for name, value in default_headers:
            if _has_header(response.headers, name):
                continue
            _append_response_header(response, name, value)
        return

    existing_headers = {name.lower() for name, _ in response.headers}

    configured_headers = config.normalized_headers
    configured_header_names = {name.lower() for name, _ in configured_headers}

    if (
        config.server_header
        and b"server" not in existing_headers
        and "server" not in configured_header_names
    ):
        _append_response_header(response, b"server", _SERVER_HEADER_VALUE)

    if (
        config.date_header
        and b"date" not in existing_headers
        and "date" not in configured_header_names
    ):
        _append_response_header(response, b"date", _http_date_header())

    for name, value in configured_headers:
        _append_response_header(response, name.encode("latin-1"), value.encode("latin-1"))

encode_http_response_head(response, keep_alive)

Serializes the HTTP response status line and headers.

Handles status line and headers while preserving chunked transfer metadata for callers that stream body chunks separately.

PARAMETER DESCRIPTION
response

The response to serialize.

TYPE: HTTPResponse

keep_alive

Whether the connection should be kept alive.

TYPE: bool

YIELDS DESCRIPTION
bytes

Serialized status and header fragments.

TYPE:: Iterable[bytes]

Source code in palfrey/protocols/http.py
def encode_http_response_head(response: HTTPResponse, keep_alive: bool) -> Iterable[bytes]:
    """Serializes the HTTP response status line and headers.

    Handles status line and headers while preserving chunked transfer metadata
    for callers that stream body chunks separately.

    Args:
        response (HTTPResponse): The response to serialize.
        keep_alive (bool): Whether the connection should be kept alive.

    Yields:
        bytes: Serialized status and header fragments.
    """
    if response.status in _STATUS_LINES:
        yield _STATUS_LINES[response.status]
    else:
        try:
            reason = http.HTTPStatus(response.status).phrase
        except ValueError:
            reason = ""
        yield f"HTTP/1.1 {response.status} {reason}\r\n".encode("ascii")

    metadata_trusted = response.headers_metadata_trusted
    has_content_length = response.has_content_length if metadata_trusted else False
    has_transfer_encoding = response.has_transfer_encoding if metadata_trusted else False
    has_connection = response.has_connection_header if metadata_trusted else False

    for name, value in response.headers:
        if not metadata_trusted:
            if name == b"content-length":
                has_content_length = True
            elif name == b"transfer-encoding":
                has_transfer_encoding = True
            elif name == b"connection":
                has_connection = True
            elif not name.islower():
                lowered_name = name.lower()
                if lowered_name == b"content-length":
                    has_content_length = True
                elif lowered_name == b"transfer-encoding":
                    has_transfer_encoding = True
                elif lowered_name == b"connection":
                    has_connection = True

        yield name
        yield _HEADER_SEPARATOR
        yield value
        yield _CRLF

    if not has_content_length and not has_transfer_encoding:
        payload_len = 0 if response.suppress_body else sum(len(c) for c in response.body_chunks)
        yield b"content-length: "
        yield str(payload_len).encode("ascii")
        yield _CRLF

    if not has_connection and not keep_alive:
        yield _CONNECTION_CLOSE

    yield _CRLF

encode_http_response_body_chunk(response, body, *, more_body)

Serializes a single ASGI response body event into wire bytes.

Source code in palfrey/protocols/http.py
def encode_http_response_body_chunk(
    response: HTTPResponse,
    body: bytes,
    *,
    more_body: bool,
) -> Iterable[bytes]:
    """Serializes a single ASGI response body event into wire bytes."""
    if response.suppress_body:
        return
    if response.chunked_encoding:
        if body:
            yield f"{len(body):x}\r\n".encode("ascii")
            yield body
            yield _CRLF
        if not more_body:
            yield b"0\r\n\r\n"
        return
    if body:
        yield body

encode_http_response_head_and_body_chunk(response, body, *, keep_alive, more_body)

Serialize response headers and the first body event in one pass.

Source code in palfrey/protocols/http.py
def encode_http_response_head_and_body_chunk(
    response: HTTPResponse,
    body: bytes,
    *,
    keep_alive: bool,
    more_body: bool,
) -> bytes:
    """Serialize response headers and the first body event in one pass."""
    body_size = len(body)
    if (
        response.status == 200
        and response.headers_metadata_trusted
        and response.chunked_encoding
        and response.has_transfer_encoding
        and not response.has_connection_header
        and not response.suppress_body
        and keep_alive
        and not more_body
        and 0 < body_size < len(_SMALL_CHUNK_PREFIXES)
    ):
        headers = response.headers
        if len(headers) == 4:
            first_name, first_value = headers[0]
            second_name, second_value = headers[1]
            third_name, third_value = headers[2]
            fourth_name, fourth_value = headers[3]
            if (
                first_name == b"content-type"
                and second_name == b"transfer-encoding"
                and second_value == b"chunked"
                and third_name == b"date"
                and fourth_name == b"server"
            ):
                return b"".join(
                    (
                        _STATUS_LINES[200],
                        first_name,
                        _HEADER_SEPARATOR,
                        first_value,
                        _CRLF,
                        second_name,
                        _HEADER_SEPARATOR,
                        second_value,
                        _CRLF,
                        third_name,
                        _HEADER_SEPARATOR,
                        third_value,
                        _CRLF,
                        fourth_name,
                        _HEADER_SEPARATOR,
                        fourth_value,
                        _CRLF,
                        _CRLF,
                        _SMALL_CHUNK_PREFIXES[body_size],
                        body,
                        _CRLF,
                        _CHUNK_END,
                    )
                )

    if response.status in _STATUS_LINES:
        parts = [_STATUS_LINES[response.status]]
    else:
        try:
            reason = http.HTTPStatus(response.status).phrase
        except ValueError:
            reason = ""
        parts = [f"HTTP/1.1 {response.status} {reason}\r\n".encode("ascii")]

    metadata_trusted = response.headers_metadata_trusted
    has_content_length = response.has_content_length if metadata_trusted else False
    has_transfer_encoding = response.has_transfer_encoding if metadata_trusted else False
    has_connection = response.has_connection_header if metadata_trusted else False

    for name, value in response.headers:
        if not metadata_trusted:
            if name == b"content-length":
                has_content_length = True
            elif name == b"transfer-encoding":
                has_transfer_encoding = True
            elif name == b"connection":
                has_connection = True
            elif not name.islower():
                lowered_name = name.lower()
                if lowered_name == b"content-length":
                    has_content_length = True
                elif lowered_name == b"transfer-encoding":
                    has_transfer_encoding = True
                elif lowered_name == b"connection":
                    has_connection = True

        parts.append(name)
        parts.append(_HEADER_SEPARATOR)
        parts.append(value)
        parts.append(_CRLF)

    if not has_content_length and not has_transfer_encoding:
        payload_len = 0 if response.suppress_body else sum(len(c) for c in response.body_chunks)
        parts.append(b"content-length: ")
        parts.append(str(payload_len).encode("ascii"))
        parts.append(_CRLF)

    if not has_connection and not keep_alive:
        parts.append(_CONNECTION_CLOSE)

    parts.append(_CRLF)

    if not response.suppress_body:
        if response.chunked_encoding:
            if body:
                parts.append(f"{len(body):x}\r\n".encode("ascii"))
                parts.append(body)
                parts.append(_CRLF)
            if not more_body:
                parts.append(_CHUNK_END)
        elif body:
            parts.append(body)

    return b"".join(parts)

encode_http_response_chunks(response, keep_alive)

Serializes the HTTPResponse into a sequence of bytes chunks.

Handles status line, headers, and body encoding including chunked transfer.

PARAMETER DESCRIPTION
response

The response to serialize.

TYPE: HTTPResponse

keep_alive

Whether the connection should be kept alive.

TYPE: bool

YIELDS DESCRIPTION
bytes

Serialized fragments of the HTTP response.

TYPE:: Iterable[bytes]

Source code in palfrey/protocols/http.py
def encode_http_response_chunks(response: HTTPResponse, keep_alive: bool) -> Iterable[bytes]:
    """Serializes the HTTPResponse into a sequence of bytes chunks.

    Handles status line, headers, and body encoding including chunked transfer.

    Args:
        response (HTTPResponse): The response to serialize.
        keep_alive (bool): Whether the connection should be kept alive.

    Yields:
        bytes: Serialized fragments of the HTTP response.
    """
    yield from encode_http_response_head(response, keep_alive=keep_alive)
    if response.chunked_encoding:
        chunks = response.body_chunks or [b""]
        for index, chunk in enumerate(chunks):
            yield from encode_http_response_body_chunk(
                response,
                chunk,
                more_body=index < len(chunks) - 1,
            )
        return
    elif not response.suppress_body:
        yield from response.body_chunks

encode_http_response(response, keep_alive)

Zero-copy serialization of the HTTPResponse into raw wire bytes.

This function is aggressively optimized to avoid intermediate string formatting and memory allocations. It appends pre-encoded bytes into a flat list and performs a single C-level b"".join().

PARAMETER DESCRIPTION
response

The finalized response data collected from the app.

TYPE: HTTPResponse

keep_alive

Indicates if the connection will remain open.

TYPE: bool

RETURNS DESCRIPTION
bytes

The fully serialized HTTP response ready for transport.write().

TYPE: bytes

Source code in palfrey/protocols/http.py
def encode_http_response(response: HTTPResponse, keep_alive: bool) -> bytes:
    """
    Zero-copy serialization of the HTTPResponse into raw wire bytes.

    This function is aggressively optimized to avoid intermediate string formatting
    and memory allocations. It appends pre-encoded bytes into a flat list and performs
    a single C-level `b"".join()`.

    Args:
        response (HTTPResponse): The finalized response data collected from the app.
        keep_alive (bool): Indicates if the connection will remain open.

    Returns:
        bytes: The fully serialized HTTP response ready for `transport.write()`.
    """
    return b"".join(encode_http_response_chunks(response, keep_alive))

should_keep_alive(request, response)

Determines if the TCP connection should persist based on headers and protocol version.

Handles the nuances between HTTP/1.0 explicit keep-alive and HTTP/1.1 explicit close.

PARAMETER DESCRIPTION
request

The parsed incoming request.

TYPE: HTTPRequest

response

The finalized outgoing response.

TYPE: HTTPResponse

RETURNS DESCRIPTION
bool

True if the connection should be kept alive, False to close.

TYPE: bool

Source code in palfrey/protocols/http.py
def should_keep_alive(request: HTTPRequest, response: HTTPResponse) -> bool:
    """
    Determines if the TCP connection should persist based on headers and protocol version.

    Handles the nuances between HTTP/1.0 explicit keep-alive and HTTP/1.1 explicit close.

    Args:
        request (HTTPRequest): The parsed incoming request.
        response (HTTPResponse): The finalized outgoing response.

    Returns:
        bool: True if the connection should be kept alive, False to close.
    """
    request_connection = (
        request.connection_header
        if request.headers_normalized
        else request.connection_header or _normalize_connection_value(request.headers)
    )
    if response.headers_metadata_trusted:
        response_connection = response.connection_header
    else:
        response_connection = b""
        for name, value in response.headers:
            if name == b"connection" or (not name.islower() and name.lower() == b"connection"):
                response_connection = value.lower()
                break

    if b"close" in request_connection or b"close" in response_connection:
        return False

    if request.http_version == "HTTP/1.0" and b"keep-alive" not in request_connection:
        return False

    return True

is_websocket_upgrade(request)

Checks if the request is initiating a WebSocket handshake.

Source code in palfrey/protocols/http.py
def is_websocket_upgrade(request: HTTPRequest) -> bool:
    """Checks if the request is initiating a WebSocket handshake."""
    if request.headers_normalized:
        return request.is_websocket_upgrade
    return request.is_websocket_upgrade or _is_websocket_upgrade(request.headers)

requires_100_continue(request)

Verifies if the client is waiting for a '100 Continue' response before sending the body.

Source code in palfrey/protocols/http.py
def requires_100_continue(request: HTTPRequest) -> bool:
    """Verifies if the client is waiting for a '100 Continue' response before sending the body."""
    expect = (
        request.expect_header
        if request.headers_normalized
        else request.expect_header or _header_lookup(request.headers, b"expect")
    )
    if not expect:
        return False
    return expect.lower() == b"100-continue"