# How the RequestServer Process Handles Network Communications in Ladybird

> Explore how Ladybird's RequestServer process manages network communications. Discover its sandboxed service, IPC state machine, and libcurl integration for secure resource fetching.

- Repository: [Ladybird/ladybird](https://github.com/LadybirdBrowser/ladybird)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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`](https://github.com/LadybirdBrowser/ladybird/blob/main/ConnectionFromClient.cpp), where incoming messages instantiate `Request` objects and trigger the state machine.

```cpp
// 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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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.

```cpp
// 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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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`.

```cpp
// 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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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.

```cpp
// 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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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`](https://github.com/LadybirdBrowser/ladybird/blob/main/main.cpp).

```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):**

```cpp
// 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:**

```cpp
// 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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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`](https://github.com/LadybirdBrowser/ladybird/blob/main/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.