# How Ladybird's Node Client Interacts with the RequestServer: IPC Architecture Explained

> Discover how Ladybird's Node client communicates with RequestServer using IPC. Understand the architecture behind JavaScript fetch calls and Promise resolutions.

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

---

**Ladybird's Node client delegates all network I/O to a dedicated RequestServer process via SerenityOS-style IPC, translating JavaScript `fetch()` calls into `start_request` messages and resolving Promises when `request_complete` callbacks return.**

The Ladybird browser implements a strict separation between its JavaScript runtime and network stack. Instead of performing socket operations directly, the **Node client** acts as a thin wrapper that marshals request data across a Unix socket to the **RequestServer**. This architecture keeps the rendering process lightweight while centralizing TLS, caching, and protocol handling in a privileged service.

## Establishing the IPC Connection

When the browser process launches, it initializes the `RequestServer` service and creates a communication channel for the Node client. On desktop platforms, the server binary starts directly; on Android, the system uses [`UI/Android/src/main/cpp/RequestServerService.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/UI/Android/src/main/cpp/RequestServerService.cpp) to adopt a passed file descriptor and construct the transport layer.

```cpp
auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
auto client = TRY(RequestServer::ConnectionFromClient::try_create(make<IPC::Transport>(move(socket))));

```

The Node side constructs an `IPC::Connection` to the **RequestClientEndpoint**, defined in `Services/RequestServer/RequestClient.ipc`. This endpoint exposes the API that the JavaScript runtime invokes for all network operations, including HTTP requests, WebSocket connections, and DNS resolution.

## Initiating Requests via start_request

When JavaScript code calls `fetch(url, options)`, the binding layer creates a `Request` object and invokes `ConnectionFromClient::start_request`. This method serializes the fetch parameters into an IPC message sent to the RequestServer.

```cpp
virtual void start_request(u64 request_id,
                           ByteString method,
                           URL::URL url,
                           Vector<HTTP::Header> headers,
                           ByteBuffer body,
                           HTTP::CacheMode cache_mode,
                           HTTP::Cookie::IncludeCredentials include_credentials,
                           Core::ProxyData proxy_data) override;

```

The **request ID** parameter enables the client to correlate asynchronous responses with their original requests. The Node client passes `method`, `url`, `headers`, `body`, and cache settings exactly as specified in the JavaScript options object.

## Processing Requests with libcurl

Inside [`Services/RequestServer/ConnectionFromClient.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/RequestServer/ConnectionFromClient.cpp), the server stores the request in `m_active_requests` and instantiates a `Request` object. The Request class, implemented in [`Services/RequestServer/Request.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/Services/RequestServer/Request.cpp), wraps **libcurl** to perform the actual network operation.

The implementation configures TLS using the default certificate path (`RequestServer::g_default_certificate_path`), sets up a timer (`m_timer`) for timeouts, and registers libcurl callbacks (`on_socket_callback`, `on_timeout_callback`) to drive asynchronous I/O without blocking the main thread.

## Receiving Responses Through request_complete

When libcurl finishes the transfer, the `Request` object calls back into `ConnectionFromClient::request_complete`, which sends an IPC message to the Node client:

```cpp
void request_complete(Badge<Request>, Request const&);

```

The client receives a `Messages::RequestServer::RequestComplete` message (generated from `RequestClientEndpoint.idl`) and resolves the JavaScript Promise with a `Response` object containing the status code, headers, and response body. This asynchronous callback mechanism ensures the JavaScript runtime remains responsive during network latency.

## Advanced Network Features

Beyond standard HTTP requests, the Node client interacts with the RequestServer for specialized network operations:

- **WebSocket Support**: The client calls `websocket_connect`, `websocket_send`, and `websocket_close`, implemented in [`WebSocketImplCurl.cpp`](https://github.com/LadybirdBrowser/ladybird/blob/main/WebSocketImplCurl.cpp), to handle bidirectional communication over libcurl.
- **DNS Customization**: Methods like `set_dns_server` and `set_use_system_dns` allow the JavaScript runtime to specify custom resolvers when parsing URLs or making requests.
- **Cache Management**: The server exposes `set_disk_cache_settings` and `remove_cache_entries_accessed_since` to control HTTP caching behavior.
- **Revalidation**: For conditional requests, the server uses `start_revalidation_request` to handle `If-None-Match` headers and returns 304 status codes via the standard `request_complete` path.

## Practical Example: Tracing a Fetch Request

Consider a standard `fetch()` call in a Ladybird-hosted script:

```js
fetch("https://example.com/api", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ hello: "world" })
})
.then(r => r.json())
.then(data => console.log(data))
.catch(err => console.error(err));

```

This JavaScript triggers the following interaction between the Node client and RequestServer:

1. The JS binding invokes `ConnectionFromClient::start_request` with `request_id=1`, `method="POST"`, and the JSON payload.
2. The RequestServer creates a `Request` instance, configures libcurl with TLS settings, and schedules the I/O.
3. Upon completion, the server calls `request_complete` with the HTTP response data.
4. The Node client converts the IPC message into a `Response` object and resolves the original Promise.

## Summary

- Ladybird's **Node client** does not perform socket operations directly; it marshals requests to a separate **RequestServer** process.
- Communication uses **SerenityOS-style IPC** over Unix sockets, with endpoints defined in `RequestClient.ipc`.
- The `start_request` method initiates HTTP requests, while `request_complete` delivers asynchronous responses.
- The RequestServer uses **libcurl** for transport, handling TLS, redirects, cookies, and caching internally.
- This architecture isolates network complexity from the rendering engine, enabling reuse across desktop and Android platforms.

## Frequently Asked Questions

### Why does Ladybird use a separate RequestServer process instead of handling HTTP directly in the Node client?

The separation enhances security and stability by isolating network operations—including TLS certificate validation, DNS resolution, and untrusted server data—from the JavaScript runtime. According to the Ladybird source code, this design prevents libcurl and its dependencies from running inside the rendering process, reducing the attack surface and allowing the network stack to be shared across multiple browser instances.

### How does the Node client handle WebSocket connections?

WebSocket support follows the same IPC pattern as HTTP requests. The JavaScript runtime calls `websocket_connect` on the `ConnectionFromClient`, which the RequestServer handles using `WebSocketImplCurl`. Data flows bidirectionally through IPC messages: the client sends frames via `websocket_send`, and the server pushes received frames back through dedicated IPC callbacks, maintaining the abstraction that the Node client remains a thin wrapper.

### What happens if the RequestServer process crashes during an active request?

Since the Node client maintains the `IPC::Connection`, a server crash would disconnect the socket, causing pending `start_request` calls to fail with connection errors. The JavaScript bindings would reject the associated Promises with network errors. The browser process monitors the RequestServer health and can restart the service, though in-flight requests would need to be retried by the application code.

### How are DNS settings customized in this architecture?

The Node client exposes `set_dns_server` and `set_use_system_dns` methods that map to IPC calls handled by `ConnectionFromClient`. When JavaScript code requires custom name resolution—such as when constructing a `URL` object or initiating a fetch with specific DNS requirements—these methods configure the libcurl resolver inside the RequestServer before the actual HTTP connection begins.