# Rust SDK worker refusals
Workers answer a pushed request with `PushClient::reply` or refuse it with
`PushClient::refuse(correlation_id, code, words)`. `PushWriter` offers both
methods on the same connection. A refusal accepts the closed codes `busy`,
`invalid`, `unavailable`, and `internal`.
The wire-configured words limit is 1024 UTF-8 bytes, preserving a useful
explanation while bounding storage and decoding. Both worker APIs validate the
code and byte length before locking or writing the socket. Invalid data returns
`SdkError::Protocol` with the bound or fault name and no application data.
Worker identities must fit in 993 UTF-8 bytes so a malformed refusal can name
the full station within the same 1024-byte explanation bound. Registration
rejects a longer identity with `worker_station_too_long`; names are never
truncated.
```rust
let request = worker.recv()?;
if let liminal_sdk::PushReceiveOutcome::Frame(request) = request {
worker.refuse(request.correlation_id(), "busy", "worker is busy")?;
}
```
A Rust conversation caller receives
`SdkError::WorkerRefused { code, words }`. `code` is the worker code without the
`worker_` prefix, and `words` is exact. Display returns those words; Debug
records only the code and the words' byte count. Route admission errors remain
`SdkError::Conversation`. Classification uses the conversation's numeric reason
code, never a prefix in its message.
The existing host-side server push awaiter returns the stable `worker_<code>`
alone through its legacy `PushReplyRefused` error. Routed calls retain the
validated refusal separately so its words reach only the authorised caller.
The [wire fixture](../../docs/wire/worker-refusal.json) pins every code, a maximal
UTF-8 words case, complete worker and caller frames, and an unchanged opaque
reply. Its `0x0401`–`0x0404` conversation reasons map to `worker_busy`,
`worker_invalid`, `worker_unavailable`, and `worker_internal`. The server logs
malformed or reserved-flag replies by fault name without their bytes. A refusal
settles its slot once; duplicate replies are discarded.
Verify the SDK's live TCP pin on a socket-capable host with
`cargo nextest run -p liminal-sdk --features embedded --lib refusal`.
## What a client links
`std`, the default, gives the native TCP and WebSocket clients, and `wss` adds
wss:// to a TLS-terminating proxy. Neither the default client nor `std` nor
`wss` links a server crate. The wire codec comes from `liminal-bus-protocol`
(AGPL-3.0-only, as `liminal-rs`), which depends only on `serde_json` and
`thiserror`, so a client compiles and links no `liminal-rs`, haematite, beamr
or Cranelift.
`embedded` links `liminal-server`, the whole server, in process, under its own
licence.
A reply names its request as its parent through `DeliveredId::reply_parent`.
Take `DeliveredId` from the SDK, as `liminal_sdk::DeliveredId`. Move
`liminal-sdk` and any `liminal-rs` you depend on together: `liminal-rs` 0.16.24
or older beside this SDK holds a second `DeliveredId` type.