How the RequestServer Process Handles Network Communications in Ladybird

The Ladybird RequestServer process isolates all network I/O into a dedicated sandboxed service that uses an IPC-driven state machine and libcurl to fetch resources on behalf of the browser's UI and WebContent processes.

The Ladybird browser architecture delegates all potentially blocking network operations to a separate RequestServer process. According to the LadybirdBrowser/ladybird source code, this design keeps the UI and rendering processes responsive while centralizing DNS resolution, TLS handshake, and HTTP caching in a single security-boundary service.

Architecture Overview

The RequestServer implements a classic IPC → request-state-machine → network stack pipeline. When the WebContent or UI process needs a resource, it sends a message over a socket to the RequestServer. The server maintains a primary connection concept— the first accepted client becomes the primary connection (ConnectionFromClient::IsPrimaryConnection::Yes), and all subsequent requests are multiplexed over a shared libcurl multi handle. This allows non-blocking network I/O across multiple concurrent requests without threading overhead in the client processes.

Process Startup and IPC Endpoint

The entry point in Services/RequestServer/main.cpp parses command-line options for certificates, DNS settings, disk-cache mode, and resource maps, then registers signal handlers for graceful shutdown. It creates a LibIPC::SingleServer that waits for the first client connection.

The IPC interface is defined in Services/RequestServer/RequestServer.ipc, which declares messages such as start_request, ensure_connection, and stop_request. The server implements these in ConnectionFromClient.cpp, where incoming messages instantiate Request objects and trigger the state machine.

// Simplified IPC message structure from RequestServer.ipc
start_request(
    i32 request_id,
    ByteString method,
    URL::URL url,
    Vector<HTTP::Header> request_headers,
    ByteBuffer request_body,
    HTTP::CacheMode cache_mode,
    HTTP::Cookie::IncludeCredentials include_credentials,
    Optional<HTTP::NetworkProxy> proxy_data
) => (bool success)

Request Lifecycle and State Machine

Each network operation is represented by a Request instance (derived from HTTP::CacheRequest). Static factory methods Request::fetch, Request::connect, and Request::revalidate instantiate the object and immediately invoke process() to begin state transitions.

The state machine in Services/RequestServer/Request.cpp defines eleven distinct phases: Init, ReadCache, ServeSubstitution, DNSLookup, RetrieveCookie, Connect, Fetch, Complete, and Error. Transitions are logged via dbgln_if(REQUESTSERVER_DEBUG, ...), allowing developers to trace request flow.

Initial States and Resource Substitution

Before any socket activity, handle_initial_state checks g_resource_substitution_map. If the requested URL maps to a local file, the request enters ServeSubstitution and returns data from disk without network latency or sandbox exposure.

// From Request.cpp - resource substitution check
if (auto substitution = g_resource_substitution_map.get(url); substitution.has_value()) {
    m_state = State::ServeSubstitution;
    return process();
}

Disk Cache Integration

If a cache is configured and no substitution exists, the request enters ReadCache. When a stale entry is found, the server spawns a revalidation request (via Request::revalidate) while simultaneously streaming the cached body to the client. This minimizes perceived latency during conditional HTTP requests.

DNS Resolution

The handle_dns_lookup_state method delegates to Resolver (Resolver::default_resolver() in Services/RequestServer/Resolver.cpp). The resolver supports plain UDP resolution or DNS-over-TLS when the --use-dns-over-tls flag is provided. In TLS mode, the resolver creates a secure socket via TLS::TLSv12::connect.

// Resolver.cpp - DNS-over-TLS path
if (m_use_dns_over_tls) {
    auto tls_socket = TRY(TLS::TLSv12::connect(nameserver, 853));
    // ... perform TLS handshake and query
}

Connection and Data Fetching

In handle_connect_state, the request builds a curl easy handle and sets the resolved IP address using CURLOPT_RESOLVE via the helper build_curl_resolve_list in Services/RequestServer/CURL.cpp. The handle_fetch_state configures headers, request bodies, and registers callbacks (on_header_received, on_data_received) before adding the handle to the shared multi-handle. The server's event loop drives libcurl until the operation completes.

// Simplified header callback from Request.cpp
size_t Request::on_header_received(void* buffer, size_t size, size_t nmemb, void* user_data)
{
    auto& self = *static_cast<Request*>(user_data);
    // Append raw header line to self.m_response_headers
    return size * nmemb; // Tell curl we consumed the data
}

Error Handling and Completion

When libcurl finishes, Services/RequestServer/CURL.cpp maps curl result codes to Ladybird's internal error enum via curl_code_to_network_error. For example, CURLE_COULDNT_RESOLVE_HOST becomes Requests::NetworkError::DNSLookupFailed.

On success, handle_complete_state notifies the client via IPC, writes cache entries if applicable, and cleans up the curl handle. On failure, handle_error_state transmits the mapped error code back to the requesting process. The server exits its event loop upon receiving SIGINT/SIGTERM or when the primary client disconnects, ensuring resources are freed in main.cpp.

// CURL.cpp - Error mapping example
Requests::NetworkError err = curl_code_to_network_error(curl_result);
if (err != Requests::NetworkError::Unknown) {
    // Transmit error back to WebContent process
}

Practical Code Examples

The following patterns demonstrate how the RequestServer orchestrates network flow:

Starting a request from WebContent (IPC layer):

// In WebContent process
Messages::RequestServer::StartRequest request{
    .request_id = next_id(),
    .method = "GET",
    .url = URL("https://example.com"),
    .request_headers = header_vec,
    .request_body = {},
    .cache_mode = HTTP::CacheMode::Default,
    .include_credentials = HTTP::Cookie::IncludeCredentials::OnlyIfSameSite
};
request_server_client->post_message(request);

Mapping libcurl errors to Ladybird errors:

// Inside CURL.cpp helper
switch (curl_code) {
    case CURLE_COULDNT_RESOLVE_HOST:
        return Requests::NetworkError::DNSLookupFailed;
    case CURLE_COULDNT_CONNECT:
        return Requests::NetworkError::ConnectionRefused;
    // ... additional mappings
}

Summary

  • RequestServer is a dedicated sandboxed process in Ladybird that isolates all network I/O from the UI and rendering engines.
  • Communication occurs via IPC defined in RequestServer.ipc and handled by ConnectionFromClient.
  • Each request traverses an eleven-state machine (Request.cpp) managing substitution, caching, DNS, and HTTP fetching.
  • DNS resolution supports both UDP and DNS-over-TLS via the Resolver class.
  • libcurl performs actual socket operations using a shared multi-handle for efficient concurrency.
  • Errors are translated from curl codes to Requests::NetworkError values for consistent client handling.

Frequently Asked Questions

Why does Ladybird isolate network operations in a separate process?

Isolating network I/O into the RequestServer process prevents blocking the UI thread during slow DNS lookups or TLS handshakes. It also creates a security boundary where the sandboxed WebContent process cannot access raw sockets directly, reducing the attack surface for network-based exploits.

How does RequestServer handle HTTPS connections?

The server uses libcurl for the TLS handshake and HTTP exchange. During the handle_connect_state, it configures the curl easy handle with CURLOPT_RESOLVE to inject pre-resolved IPs from the Resolver. For DNS-over-TLS, the Resolver class establishes a secure TLSv12 connection to the nameserver before resolving hostnames.

What happens when a cached resource becomes stale?

When Request::process() enters ReadCache and detects a stale entry, it spawns a revalidation request using Request::revalidate. This performs a conditional HTTP request (If-None-Match/If-Modified-Since) while the cached body continues streaming to the client, ensuring minimal perceived latency.

How are network errors reported back to the browser UI?

After a curl operation completes, curl_code_to_network_error in Services/RequestServer/CURL.cpp translates libcurl result codes (e.g., CURLE_COULDNT_RESOLVE_HOST) into Requests::NetworkError enums. The handle_error_state method then transmits this error code through the IPC endpoint to the WebContent process, which propagates it to the JavaScript environment or UI layer.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →