# Client and server

A [group of RPC definitions](https://effect.plants.sh/rpc/defining-rpcs/) is interpreted at two ends. On
the **server**, `group.toLayer` asks you to implement one handler per procedure
and produces a `Layer` of those handlers; `RpcServer.layer` attaches a transport
and starts serving. On the **client**, `RpcClient.make` reads the same group and
hands you an object whose methods *are* the procedures — calling
`client.GetUser({ id })` encodes the payload, sends it, and decodes the typed
result or error back into an `Effect`.

Throughout, the group is the single source of truth: handler signatures and
client method signatures are both derived from it, so they cannot disagree.

## Implementing handlers

`group.toLayer` takes an `Effect` that builds an object keyed by RPC tag. Each
handler receives the **decoded payload** as its first argument and returns an
`Effect` (or a [`Stream`](https://effect.plants.sh/streaming/) for streaming RPCs). Building the handlers
inside an `Effect.gen` lets you pull in services first — here a `Database`
service the handlers depend on.

```ts
import { Context, Effect, Layer, Schema } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()(
  "UserNotFound",
  { id: Schema.String }
) {}

const User = Schema.Struct({ id: Schema.String, name: Schema.String })

export class UserRpcs extends RpcGroup.make(
  Rpc.make("GetUser", {
    payload: { id: Schema.String },
    success: User,
    error: UserNotFound
  }),
  Rpc.make("CreateUser", {
    payload: { name: Schema.String },
    success: User
  })
) {}

// A service the handlers depend on. Handlers are wired up like any other Effect
// code, so they can require services from the environment.
class Database extends Context.Service<Database, {
  readonly find: (id: string) => Effect.Effect<{ id: string; name: string }, UserNotFound>
  readonly insert: (name: string) => Effect.Effect<{ id: string; name: string }>
}>()("app/Database") {
  static readonly layer = Layer.succeed(Database)(
    Database.of({
      find: (id) =>
        id === "1"
          ? Effect.succeed({ id, name: "Ada" })
          : Effect.fail(new UserNotFound({ id })),
      insert: (name) => Effect.succeed({ id: "2", name })
    })
  )
}

// `toLayer` returns a Layer that satisfies the handler requirements for every
// procedure in the group. The build effect runs once at construction, so this is
// the place to acquire shared resources.
export const UserRpcsLayer = UserRpcs.toLayer(
  Effect.gen(function*() {
    const db = yield* Database

    return {
      // The first argument is the decoded payload. Return an Effect whose success
      // matches `success` and whose error matches `error`.
      GetUser: ({ id }) => db.find(id),
      CreateUser: ({ name }) => db.insert(name)
    }
  })
).pipe(Layer.provide(Database.layer))
```
**Note:** A handler's required services flow straight into the layer's requirements. The
`Database` dependency above is satisfied with `Layer.provide` so `UserRpcsLayer`
needs nothing further — exactly how you compose any other [Layer](https://effect.plants.sh/services-and-layers/).

The second argument to each handler (omitted above) carries per-request
metadata: the connected `client`, the `requestId`, the request `headers`, and the
`rpc` definition itself — useful for authentication or logging.

```ts
import { Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

UserRpcs.toLayer({
  GetUser: (payload, { client, requestId, headers, rpc }) =>
    Effect.gen(function*() {
      // `client.id` identifies the connection; `headers` are the request headers.
      yield* Effect.log(`req ${requestId} on client ${client.id} for ${rpc._tag}`)
      const auth = headers["authorization"]
      // ... authenticate, then return the success value
      return { id: payload.id, name: "Ada" }
    }),
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})
```

The `client` is an `Rpc.ServerClient` — beyond `client.id` it carries
`client.annotate(tag, value)` for stashing per-request annotations (read by
middleware or later handlers on the same connection).

## Serving over a transport

The handler layer is transport-agnostic. To expose it, combine it with a
transport and a serialization format. `RpcServer.layerHttp` mounts the group on
an [HTTP router](https://effect.plants.sh/http-api/) path, and `RpcSerialization.layerNdjson` (or
`layerJson`) frames the messages.

```ts
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Layer } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { RpcSerialization, RpcServer } from "effect/unstable/rpc"
import { createServer } from "node:http"
import { UserRpcs, UserRpcsLayer } from "./handlers.ts"

// Mount the RPC group at /rpc. The default is websockets; here we use
// request/response HTTP so it pairs with the `layerProtocolHttp` client below.
const RpcRoute = RpcServer.layerHttp({
  group: UserRpcs,
  path: "/rpc",
  protocol: "http"
})

// Assemble the server: the route needs the handler implementations, a
// serialization format, and an HTTP router/server to mount onto.
const ServerLayer = HttpRouter.serve(RpcRoute).pipe(
  Layer.provide(UserRpcsLayer), // the handlers
  Layer.provide(RpcSerialization.layerNdjson), // wire framing
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
```
**Tip:** `layerHttp` is the one-line convenience. The lower-level building blocks —
`RpcServer.make`, `RpcServer.layer`, and the transport `Protocol` constructors —
are listed in the [reference](#rpcserver-entry-points) below and on the
[transports page](https://effect.plants.sh/rpc/transports/).

## Calling from a typed client

`RpcClient.make(group)` returns a client whose methods mirror the group. It needs
a client `Protocol` — `RpcClient.layerProtocolHttp` points it at the server URL —
and the same serialization format the server uses. Wrapping the client in a
`Context.Service` is the idiomatic way to make it injectable.

```ts
import { NodeRuntime } from "@effect/platform-node"
import { Context, Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { RpcClient, RpcClientError, RpcSerialization } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

// Expose the generated client as a service so the rest of the app can depend on
// it without knowing about transports. `RpcClient.make` is a scoped Effect;
// `Layer.effect` runs it in the layer's scope and excludes `Scope` from the
// requirements. Calls can fail with `RpcClientError` (transport-level failures),
// which is part of the derived client type.
export class UserClient extends Context.Service<
  UserClient,
  RpcClient.FromGroup<typeof UserRpcs, RpcClientError.RpcClientError>
>()("app/UserClient") {
  static readonly layer = Layer.effect(UserClient)(RpcClient.make(UserRpcs))
}

// The client Protocol: HTTP transport pointed at the server, NDJSON framing, and
// a concrete HttpClient implementation.
const ClientLayer = UserClient.layer.pipe(
  Layer.provide(RpcClient.layerProtocolHttp({ url: "http://localhost:3000/rpc" })),
  Layer.provide(RpcSerialization.layerNdjson),
  Layer.provide(FetchHttpClient.layer)
)

const program = Effect.gen(function*() {
  const client = yield* UserClient

  // A method call IS the RPC. The payload type, the `User` success type, and the
  // `UserNotFound` error type all come from the group — fully checked here.
  const created = yield* client.CreateUser({ name: "Grace" })
  yield* Effect.log(`created ${created.name}`)

  // Typed errors land in the normal error channel and can be recovered by tag.
  const user = yield* client.GetUser({ id: "404" }).pipe(
    Effect.catchTag("UserNotFound", (e) => Effect.succeed({ id: e.id, name: "unknown" }))
  )
  yield* Effect.log(`resolved ${user.name}`)
})

program.pipe(Effect.provide(ClientLayer), NodeRuntime.runMain)
```

A streaming RPC works the same way, except the method returns a
[`Stream`](https://effect.plants.sh/streaming/) instead of an `Effect` — you consume it with the usual
`Stream` operators.

## Testing handlers in-memory

You usually don't want HTTP in a unit test. `RpcTest.makeClient` wires a client
directly to your handlers through the same routing and middleware machinery, but
skips serialization and the network entirely. Provide the handler layer, acquire
the client in a scoped test, and call it exactly as you would the real one.

```ts
import { Effect } from "effect"
import { RpcTest } from "effect/unstable/rpc"
import { UserRpcs, UserRpcsLayer } from "./handlers.ts"

const test = Effect.gen(function*() {
  // An in-memory client backed by the real handlers from the environment.
  const client = yield* RpcTest.makeClient(UserRpcs)

  const user = yield* client.GetUser({ id: "1" })
  // assert user.name === "Ada"

  // Failures surface as typed errors, just like over a real transport.
  const result = yield* client.GetUser({ id: "404" }).pipe(Effect.flip)
  // assert result._tag === "UserNotFound"
})

// Scope is required because the client is tied to the in-memory connection.
test.pipe(Effect.scoped, Effect.provide(UserRpcsLayer))
```
**Tip:** `RpcTest` exercises routing, handler lookup, typed errors, headers, middleware,
and streaming — everything except byte-level encoding. When you specifically need
to verify wire compatibility, status handling, or schema codecs, test against a
real transport with an actual `RpcSerialization` layer.

## The full loop

1. **Define** the protocol once with `Rpc.make` and `RpcGroup.make`
   ([Defining RPCs](https://effect.plants.sh/rpc/defining-rpcs/)).

2. **Implement** handlers with `group.toLayer`, depending on whatever services
   you need.

3. **Serve** by combining the handler layer with a transport
   (`RpcServer.layerHttp`) and a serialization format.

4. **Consume** with `RpcClient.make` plus a client protocol layer — calling the
   generated methods like local functions.

Because all four steps read the same group, changing a payload or success schema
in step 1 immediately surfaces as a type error in the handler and at every call
site — the guarantee that makes RPC worth reaching for.

---

# Reference

## Implementing handlers (RpcGroup)

The group's `to*` methods all build the same thing — a context entry per RPC tag,
keyed so the server can look up the handler when a request arrives. They differ
only in what they return (a `Layer`, a `Context`, or the identity of a handlers
object) and in granularity (whole group vs. a single tag).

### `group.toLayer`

Implements every handler in the group and returns a `Layer<Rpc.ToHandler<Rpcs>>`.
Pass either a plain handlers object or an `Effect` that builds one (use the
effectful form when handlers need services acquired up front). `Scope` from the
build effect is consumed by the layer, so the layer's requirements never include
`Scope`.

```ts
import { Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

// Plain object form (no setup needed).
const HandlersLayer = UserRpcs.toLayer({
  GetUser: ({ id }) => Effect.succeed({ id, name: "Ada" }),
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})
// => Layer<Rpc.ToHandler<...>, never, ...handler requirements>
```

### `group.toHandlers`

Same as `toLayer` but returns the underlying `Context` inside an `Effect` instead
of a `Layer`. Reach for it when you want to inspect or compose the handler
context directly rather than provide it as a layer.

```ts
import { Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

const handlersEffect = UserRpcs.toHandlers({
  GetUser: ({ id }) => Effect.succeed({ id, name: "Ada" }),
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})
// => Effect<Context<Rpc.ToHandler<...>>, never, ...>
```

### `group.of`

The identity function, typed as the group's handlers object. Use it to author and
type-check a handlers object on its own (e.g. in another module) before passing it
to `toLayer`.

```ts
import { Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

// Fully type-checked against the group, but not yet turned into a layer.
const handlers = UserRpcs.of({
  GetUser: ({ id }) => Effect.succeed({ id, name: "Ada" }),
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})

const HandlersLayer = UserRpcs.toLayer(handlers)
```

### `group.toLayerHandler`

Implements a single handler by tag and returns a `Layer<Rpc.Handler<Tag>>`.
Compose several of these (and merge them) when you want to split a large group's
handlers across files.

```ts
import { Effect, Layer } from "effect"
import { UserRpcs } from "./handlers.ts"

const GetUserLayer = UserRpcs.toLayerHandler(
  "GetUser",
  ({ id }) => Effect.succeed({ id, name: "Ada" })
)
const CreateUserLayer = UserRpcs.toLayerHandler(
  "CreateUser",
  ({ name }) => Effect.succeed({ id: "2", name })
)

// Merge the per-handler layers into the full handler set.
const AllHandlers = Layer.mergeAll(GetUserLayer, CreateUserLayer)
```

### `group.accessHandler`

Retrieves the implemented handler function for a tag from the environment, with
its handler context already provided. Handy for invoking one handler from another,
or for bespoke dispatch outside `RpcServer`.

```ts
import { Effect } from "effect"
import { RequestId } from "effect/unstable/rpc/RpcMessage"
import { Rpc } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const program = Effect.gen(function*() {
  const getUser = yield* UserRpcs.accessHandler("GetUser")
  const result = yield* getUser(
    { id: "1" },
    { client: new Rpc.ServerClient(0), requestId: RequestId(0n), headers: {} }
  )
  // => { id: "1", name: "Ada" }
})
```

### Handler metadata (`Rpc.ServerClient`)

Every handler's second argument is `{ client, requestId, headers, rpc }`. The
`client` is an `Rpc.ServerClient`: `client.id` is the numeric connection id and
`client.annotate(key, value)` records a per-request annotation, returning the same
client for chaining.

```ts
import { Context, Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

const RequestStart = Context.Reference<number>("app/RequestStart", {
  defaultValue: () => 0
})

UserRpcs.toLayer({
  GetUser: ({ id }, { client }) => {
    client.annotate(RequestStart, Date.now()) // => the same ServerClient
    return Effect.succeed({ id, name: "Ada" })
  },
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})
```

## Handler result wrappers (deferred responses, fork, uninterruptible)

A handler's return value is interpreted by the server. Beyond the obvious cases
there are three power features, all in the `Rpc` module.

### Deferred responses

A non-streaming handler may return an `Effect` that succeeds with a
`Deferred<Success, Error>` instead of the value itself. The server keeps the
request open and replies only when the deferred completes — useful for handing the
response off to another fiber (a queue worker, a webhook callback, etc.) without
blocking the handler fiber.

```ts
import { Deferred, Effect } from "effect"
import { UserRpcs } from "./handlers.ts"

UserRpcs.toLayer(
  Effect.gen(function*() {
    return {
      // Return a Deferred: the request stays open until it is completed elsewhere.
      GetUser: ({ id }) =>
        Effect.gen(function*() {
          const deferred = yield* Deferred.make<{ id: string; name: string }>()
          // hand `deferred` to a worker; complete it later
          yield* Effect.forkScoped(
            Deferred.succeed(deferred, { id, name: "Ada" })
          )
          return deferred // => server replies when the deferred resolves
        }),
      CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
    }
  })
)
```

### `Rpc.fork`

Wraps a handler result so the server runs it concurrently, bypassing the server's
`concurrency` limit. Use it for long-lived or independent work that should not
occupy a concurrency permit.

```ts
import { Effect } from "effect"
import { Rpc } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

UserRpcs.toLayer({
  // Runs regardless of the configured server concurrency.
  GetUser: ({ id }) => Rpc.fork(Effect.succeed({ id, name: "Ada" })),
  CreateUser: ({ name }) => Effect.succeed({ id: "2", name })
})
```

### `Rpc.uninterruptible`

Wraps a handler result so it runs in an uninterruptible region — the request will
not be interrupted by a client interrupt or server shutdown mid-flight.

```ts
import { Effect } from "effect"
import { Rpc } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

UserRpcs.toLayer({
  CreateUser: ({ name }) =>
    Rpc.uninterruptible(Effect.succeed({ id: "2", name })),
  GetUser: ({ id }) => Effect.succeed({ id, name: "Ada" })
})
```

### `Rpc.wrap`

The general form behind `fork`/`uninterruptible`: `Rpc.wrap({ fork?, uninterruptible? })`
returns a function that wraps a result with both flags at once. Companion helpers:
`Rpc.unwrap` (strip the wrapper), `Rpc.wrapMap` (map the inner value, preserving
flags), and `Rpc.isWrapper` (guard).

```ts
import { Effect } from "effect"
import { Rpc } from "effect/unstable/rpc"

const wrapBoth = Rpc.wrap({ fork: true, uninterruptible: true })
const result = wrapBoth(Effect.succeed(42))
Rpc.isWrapper(result) // => true
Rpc.unwrap(result) // => Effect.succeed(42)
```

### Streaming handlers

A streaming RPC's handler may return either a [`Stream`](https://effect.plants.sh/streaming/) or an
`Effect` that produces a `Queue.Dequeue` of chunks. Protocols that support
acknowledgements (socket, worker, stdio — but **not** HTTP) wait for client acks
between chunks to provide back pressure.

```ts
import { Effect, Schema, Stream } from "effect"
import { Rpc, RpcGroup } from "effect/unstable/rpc"

class Logs extends RpcGroup.make(
  Rpc.make("Tail", { payload: {}, success: Schema.String, stream: true })
) {}

Logs.toLayer({
  // Return a Stream...
  Tail: () => Stream.make("line 1", "line 2")
})

Logs.toLayer({
  // ...or an Effect producing a Dequeue of chunks (scoped — the queue is
  // torn down when the request completes).
  Tail: () => Stream.toQueue(Stream.make("line 1", "line 2"), { capacity: 16 })
})
```

## RpcServer entry points

These start a server for a group given a server `Protocol` in the environment.
The transport `Protocol` constructors (`makeProtocol*` / `layerProtocol*` for HTTP,
websocket, socket, stdio, and workers) live on the [transports page](https://effect.plants.sh/rpc/transports/).

### `RpcServer.make`

Runs the server loop using the current `Protocol`, returning an
`Effect<never, never, Protocol | Rpc.ToHandler | Rpc.Middleware | Rpc.ServicesServer>`
that never completes. Fork it yourself; `layer` is usually more convenient.

```ts
import { Effect } from "effect"
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const server = RpcServer.make(UserRpcs, { concurrency: 10 })
// => Effect<never, never, Protocol | handlers | middleware | schema services>
```

### `RpcServer.layer`

Wraps `make` in a scoped, forked `Layer<never>` — the standard way to run a
server. Requires a `Protocol` (provided by a transport layer) plus the handlers.

```ts
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const ServerLayer = RpcServer.layer(UserRpcs, { spanPrefix: "UserRpc" })
// => Layer<never, never, Protocol | handlers | ...>
```

### `RpcServer.layerHttp`

The route-mounting convenience used above: takes `{ group, path, protocol? }` and
internally provides an HTTP or websocket `Protocol` (default `"websocket"`,
`protocol: "http"` for request/response). Still needs `RpcSerialization`,
`HttpRouter`, and the handlers in the environment.

```ts
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

RpcServer.layerHttp({ group: UserRpcs, path: "/rpc", protocol: "http" })
// => Layer<never, never, RpcSerialization | HttpRouter | handlers | ...>
```

### `RpcServer.toHttpEffect` / `toHttpEffectWebsocket`

Start a server and hand back a standalone HTTP app `Effect` (an
`Effect<HttpServerResponse, never, Scope | HttpServerRequest>`) instead of
mounting a route — wire it into any HTTP handler yourself. `toHttpEffect` serves
the request/response HTTP protocol; `toHttpEffectWebsocket` upgrades to websockets.

```ts
import { Effect } from "effect"
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const make = Effect.gen(function*() {
  const httpApp = yield* RpcServer.toHttpEffect(UserRpcs)
  // => use `httpApp` as an HttpApp (e.g. with HttpServer.serveEffect)
  return httpApp
})
```

### `RpcServer.makeNoSerialization`

For already-decoded channels: returns an `RpcServer<Rpcs>` with `write(clientId, message)`
and `disconnect(clientId)`, taking an `onFromServer` callback for decoded
responses. This is the seam `RpcTest` builds on; you rarely call it directly.

```ts
import { Effect } from "effect"
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const make = RpcServer.makeNoSerialization(UserRpcs, {
  onFromServer: (response) => Effect.log(response._tag)
})
// => Effect<RpcServer<...>, never, handlers | middleware | Scope>
```

### Server options

`make`, `layer`, and `layerHttp` share these options (the `toHttpEffect*`
functions accept the same set **except** `concurrency`):

- `concurrency` — `number | "unbounded"` (default `"unbounded"`); the max number
  of in-flight requests (forked handlers via `Rpc.fork` bypass it).
- `disableFatalDefects` — when `true`, handler defects stay ordinary request exits
  instead of being sent as protocol-level defects.
- `disableTracing` — turn off per-request spans.
- `spanPrefix` — prefix for request span names (default `"RpcServer"`).
- `spanAttributes` — extra attributes attached to every request span.

```ts
import { RpcServer } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

RpcServer.layer(UserRpcs, {
  concurrency: 25,
  disableFatalDefects: true,
  spanPrefix: "UserRpc",
  spanAttributes: { service: "users" }
})
```

## RpcClient surface

### `RpcClient.make`

Builds a typed client from a group using the current client `Protocol`. Returns a
scoped `Effect` — run it inside a `Layer.effect`/`Effect.scoped`. With
`{ flatten: true }` it returns an `RpcClient.Flat` instead (see below).

```ts
import { Effect } from "effect"
import { RpcClient } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const acquire = RpcClient.make(UserRpcs)
// => Effect<RpcClient<UserRpcs, RpcClientError>, never, Protocol | ... | Scope>
```

### Per-call options

Every generated method accepts an options object as its second argument:

- `headers` — extra headers merged into the request (`Headers.Input`).
- `context` — a `Context` made available to the payload/result codecs.
- `discard` — for non-streaming calls: fire-and-forget. The call resolves to
  `void` and ignores the result/error.
- `asQueue` — for streaming calls: return a scoped `Effect<Queue.Dequeue<...>>`
  instead of a `Stream`.
- `streamBufferSize` — for streaming calls: the queue buffer size (default `16`).

```ts
import { Effect } from "effect"
import { UserRpcs } from "./handlers.ts"
import { RpcClient } from "effect/unstable/rpc"

const program = Effect.gen(function*() {
  const client = yield* RpcClient.make(UserRpcs)

  // Fire-and-forget: resolves to void.
  yield* client.CreateUser({ name: "Grace" }, { discard: true })
  // => void

  // Per-call header.
  yield* client.GetUser({ id: "1" }, { headers: { authorization: "Bearer t" } })
})
```

### Flattened clients (`{ flatten: true }`, `RpcClient.Flat`)

With `{ flatten: true }`, the client is a single function called as
`client(tag, payload, options?)` rather than an object of methods. Useful for
generic dispatch where the tag is dynamic.

```ts
import { Effect } from "effect"
import { RpcClient } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const program = Effect.gen(function*() {
  const client = yield* RpcClient.make(UserRpcs, { flatten: true })
  const user = yield* client("GetUser", { id: "1" })
  // => { id: "1", name: "Ada" }
})
```

### `RpcClient.makeNoSerialization`

Returns `{ client, write }` for an already-decoded channel: `client` is the typed
client and `write(message)` feeds it server messages. This is the in-process seam
that `RpcTest` uses.

```ts
import { Effect } from "effect"
import { RpcClient } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const make = RpcClient.makeNoSerialization(UserRpcs, {
  onFromClient: ({ message }) => Effect.log(message._tag)
})
// => Effect<{ client, write }, never, Scope | client middleware>
```

### Headers: `RpcClient.CurrentHeaders` and `RpcClient.withHeaders`

`CurrentHeaders` is a `Context.Reference<Headers>` merged into every outgoing
request on the current fiber. `withHeaders(effect, headers)` runs `effect` with
those headers merged in — a scoped way to attach auth tokens to a block of calls.

```ts
import { Effect } from "effect"
import { RpcClient } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

const program = Effect.gen(function*() {
  const client = yield* RpcClient.make(UserRpcs)

  // Every call inside this block carries the header.
  yield* RpcClient.withHeaders(
    client.GetUser({ id: "1" }),
    { authorization: "Bearer token" }
  )
})
```

### `RpcClient.Protocol`

The client transport service. Provided by a `layerProtocol*` layer (HTTP, socket,
worker — see [transports](https://effect.plants.sh/rpc/transports/)). It declares `supportsAck` and
`supportsTransferables`; **HTTP supports neither**, so client acknowledgements and
streaming back pressure are only available over socket and worker transports.

### Client types

- `RpcClient.RpcClient<Rpcs, E>` — the object-shaped client (one method per tag).
- `RpcClient.From<Rpcs, E>` — the underlying mapped object type.
- `RpcClient.Flat<Rpcs, E>` — the flattened `(tag, payload, options?)` function type.
- `RpcClient.FromGroup<Group, E>` — the object client derived from an `RpcGroup`
  (the convenient form to use in a `Context.Service` shape).

```ts
import { Context, Layer } from "effect"
import { RpcClient, RpcClientError } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"

class UserClient extends Context.Service<
  UserClient,
  RpcClient.FromGroup<typeof UserRpcs, RpcClientError.RpcClientError>
>()("app/UserClient") {
  static readonly layer = Layer.effect(UserClient)(RpcClient.make(UserRpcs))
}
```

## Client errors (`RpcClientError`)

A call can fail in two distinct ways. **Declared typed errors** (the RPC's `error`
schema, e.g. `UserNotFound`) are decoded from the response exit and land in the
error channel by tag. **Transport-level failures** surface as `RpcClientError`,
which carries a `reason` union: `WorkerErrorReason | SocketErrorReason |
HttpClientErrorSchema | RpcClientDefect`. Server *defects* are sent as protocol
messages and normally fail the call as defects (not as declared errors).

Inspect `error.reason._tag` to decide whether to retry, reconnect, or surface a
protocol/codec mismatch (`RpcClientDefect`).

```ts
import { Effect } from "effect"
import { RpcClientError } from "effect/unstable/rpc"
import { UserRpcs } from "./handlers.ts"
import { RpcClient } from "effect/unstable/rpc"

const program = Effect.gen(function*() {
  const client = yield* RpcClient.make(UserRpcs)

  yield* client.GetUser({ id: "1" }).pipe(
    // Declared domain error — recovered by its own tag.
    Effect.catchTag("UserNotFound", (e) => Effect.succeed({ id: e.id, name: "?" })),
    // Transport failure — a single RpcClientError, inspect its reason.
    Effect.catchTag("RpcClientError", (e: RpcClientError.RpcClientError) => {
      switch (e.reason._tag) {
        case "RpcClientDefect":
          return Effect.die(e) // protocol/codec mismatch
        default:
          return Effect.fail(e) // transport issue: HTTP/socket/worker
      }
    })
  )
})
```

`RpcClientError.RpcClientDefect` (`{ message, cause }`) is the bucket custom
protocols use to put invalid client-side protocol state in the same public error
channel as the built-in transports.

## Testing: `RpcTest.makeClient`

`RpcTest.makeClient(group, options?)` connects a generated client straight to the
server handlers for the same group over the no-serialization path — exercising
routing, handler lookup, middleware, headers, typed errors, interrupts, and
streaming, but skipping bytes entirely. It requires `Scope`, `Rpc.ToHandler`,
`Rpc.Middleware`, and `Rpc.MiddlewareClient` from the environment (i.e. provide the
handler layer and any middleware layers, inside a scoped test). The `flatten`
option matches `RpcClient.makeNoSerialization` for a flattened client.

```ts
import { Effect } from "effect"
import { RpcTest } from "effect/unstable/rpc"
import { UserRpcs, UserRpcsLayer } from "./handlers.ts"

const test = Effect.gen(function*() {
  // Flattened in-memory client.
  const client = yield* RpcTest.makeClient(UserRpcs, { flatten: true })
  const user = yield* client("GetUser", { id: "1" })
  // => { id: "1", name: "Ada" }
})

test.pipe(Effect.scoped, Effect.provide(UserRpcsLayer))
```