league-link
English | 简体中文
An async Rust client for the League of Legends Client (LCU) API —
the local HTTPS + WebSocket interface exposed by the League Client itself.
Inspired by league-connect
for Node.js.
Contents
- Features
- Installation
- Quick Start
- Usage Guide
- Recipes
- API Reference
- Platform Support
- Design Notes
- Relationship to
league-connect - Roadmap
Features
- Credential discovery — scan the running
LeagueClientUxprocess or parse alockfileto obtain the local port and auth token. - Typed HTTP client — one call, one deserialized response. TLS is pre-configured for the Riot self-signed certificate; a 10-second per-request timeout is applied by default.
- WebSocket event stream — subscribe to every LCU event, or only
specific URIs via server-side WAMP filtering. Events are delivered
through an
EventStreambacked bytokio::sync::mpsc. - Single unified error type —
LcuErrorbuilt onthiserror, with response body preserved on non-2xx responses. - Safe defaults — no panics in library code, no
eprintln!, passwords redacted fromDebugoutput, no global state,#![forbid(unsafe_code)].
Installation
[]
= "0.1"
= { = "1", = ["full"] }
= "1" # optional, if you use `Value` as the response type
MSRV: Rust 1.80 (requires std::sync::LazyLock).
Quick Start
use ;
use Value;
async
Run the bundled examples:
Usage Guide
Credential Discovery
Three strategies are available. Pick the one that matches your situation:
use ;
// (A) Async poll loop — recommended for most apps.
// First arg: poll interval (ms). Second arg: timeout (s).
let creds = authenticate.await?;
// (B) One-shot blocking scan. Returns None if client not found yet.
if let Some = try_find_lcu
// (C) One-shot non-blocking scan (runs on spawn_blocking internally).
if let Some = try_find_lcu_async.await
// (D) Fallback: read the `lockfile` written by the client on disk.
// Useful when process args are unavailable (e.g. protected child).
let creds = try_find_lcu_via_lockfile?;
The Credentials type exposes the low-level building blocks if you
need them for non-standard transports:
creds.port // u16, e.g. 52437
creds.basic_auth // "Basic cmlvdDouLi4="
creds.lcu_base_url // "https://127.0.0.1:52437"
creds.lcu_ws_url // "wss://127.0.0.1:52437"
Debug on Credentials redacts the password as ***, so it is
safe to log.
HTTP Requests
Build the client once and reuse it — it keeps an internal connection pool:
use ;
use ;
use Value;
let client = build_lcu_client?;
// GET — deserialize into any type that implements `serde::Deserialize`.
let me: Summoner = lcu_get.await?;
println!;
// Or accept arbitrary JSON via `Value`.
let me: Value = lcu_get.await?;
// POST — body is any `Serialize` type (no need to pre-convert to `Value`).
let _resp: Value = lcu_post.await?;
// DELETE — leave the current lobby.
let _: Value = lcu_delete.await?;
Need a method that doesn't have a convenience wrapper? Drop down to
lcu_request / lcu_request_with_body:
use ;
use Method;
let _: Value = lcu_request.await?;
let _: Value = lcu_request_with_body.await?;
WebSocket Events
ws_connect subscribes to every LCU JSON API event and hands you
an EventStream — an owning handle that aborts the background task
on drop.
use ;
let mut stream = ws_connect.await?;
while let Some = stream.recv.await
// `stream` dropped here → WebSocket task aborted automatically.
For high-volume paths, subscribe only to the URIs you care about (server-side filter — the LCU never sends the rest to your client):
use ws_connect_filtered;
let mut stream = ws_connect_filtered.await?;
while let Some = stream.recv.await
Error Handling
Every fallible operation returns Result<_, LcuError>. The variants
most callers care about are:
use LcuError;
match .await
See error::LcuError for the full variant list.
Recipes
Auto-reconnect loop
The library gives you the primitives; a reconnect loop is yours to own:
use ;
use Duration;
loop
Track a single gameflow phase
use ;
let mut stream = ws_connect_filtered.await?;
while let Some = stream.recv.await
Use the lockfile when process scanning is unreliable
use ;
let creds = match try_find_lcu_async.await ;
API Reference
Credential discovery (league_link::auth)
| Item | Signature | Notes |
|---|---|---|
authenticate |
async fn(poll_ms: u64, timeout_s: u64) -> Result<Credentials, LcuError> |
Polls until found or timeout. |
try_find_lcu |
fn() -> Option<Credentials> |
Blocking one-shot scan. |
try_find_lcu_async |
async fn() -> Option<Credentials> |
Non-blocking wrapper (uses spawn_blocking). |
try_find_lcu_via_lockfile |
fn(path) -> Result<Credentials, LcuError> |
Parse name:pid:port:pw:proto file. |
Credentials::basic_auth |
fn(&self) -> String |
"Basic <base64>". |
Credentials::lcu_base_url |
fn(&self) -> String |
https://127.0.0.1:<port>. |
Credentials::lcu_ws_url |
fn(&self) -> String |
wss://127.0.0.1:<port>. |
HTTP (league_link::http)
| Item | Signature | Notes |
|---|---|---|
build_lcu_client |
fn() -> Result<reqwest::Client, LcuError> |
TLS + 10 s timeout pre-applied. |
DEFAULT_TIMEOUT |
const Duration |
10 seconds. |
lcu_get<T> |
async fn(client, creds, endpoint) -> Result<T, LcuError> |
GET + JSON decode. |
lcu_post<T, B: Serialize> |
async fn(client, creds, endpoint, body) -> Result<T, LcuError> |
POST with body. |
lcu_delete<T> |
async fn(client, creds, endpoint) -> Result<T, LcuError> |
DELETE. |
lcu_request<T> |
async fn(client, creds, method, endpoint) -> Result<T, LcuError> |
Any method, no body. |
lcu_request_with_body<T, B> |
async fn(client, creds, method, endpoint, body) -> Result<T, LcuError> |
Any method, typed body. |
parse_marketing_version |
fn(raw: &str) -> Option<String> |
"4.21.614.6789" → "14.21". |
WebSocket (league_link::websocket)
| Item | Signature | Notes |
|---|---|---|
ws_connect |
async fn(creds, buffer) -> Result<EventStream, LcuError> |
Subscribe to all events. |
ws_connect_filtered |
async fn(creds, &[&str], buffer) -> Result<EventStream, LcuError> |
Subscribe to specific URIs. |
EventStream::recv |
async fn(&mut self) -> Option<LcuEvent> |
Pull next event. |
EventStream::close |
fn(self) |
Abort the background task explicitly. |
LcuEvent |
{ uri, event_type, data } |
Raw JSON in data. |
EventType |
Create / Update / Delete / Other(String) |
Unknown names preserved. |
Platform Support
| OS | Process scan | Lockfile | HTTP / WS |
|---|---|---|---|
| Windows | ✅ LeagueClientUx |
✅ | ✅ |
MSRV is Rust 1.80, enforced by CI.
Design Notes
Why a channel, not callbacks?
league-connect exposes ws.subscribe(uri, cb). That pattern maps
awkwardly to Rust's ownership model and doesn't compose with
tokio::select!. league-link hands you a receiver wrapped in
EventStream — filtering, backpressure, and cancellation all
fall out for free.
Why skip TLS verification?
The LCU binds to 127.0.0.1 with a Riot-signed certificate whose
CN doesn't match. Every LCU library does this. Since the connection
never leaves localhost, the risk surface is limited to processes
already running as the same user.
Why is Credentials::Debug custom?
Auto-derived Debug would print the password in plain text, which
often ends up in log files or crash dumps. The hand-written impl
shows password: "***" instead — this is a common footgun that
many LCU wrappers miss.
Why EventType::Other(String) instead of Unknown?
Riot adds event types occasionally. A single Unknown variant
swallows the name and leaves you guessing; Other(String) gives
you the raw payload so you can log it or match on it.
Relationship to league-connect
This library is a spiritual port of junlarsen/league-connect for the Rust/Tokio ecosystem. The core concepts are the same (process scan → Basic Auth → WAMP subscribe); the implementation is a from-scratch rewrite, not a translation.
What's shared: API shape (authenticate, discovery via process
args, WAMP opcode 8 dispatch), TLS-skipping defaults, lockfile
format.
What's different: channel-based event delivery instead of
callbacks, typed generic HTTP deserialization, thiserror-based
error type, response body preserved on HTTP errors, secret
redaction in Debug, server-side URI filtering.
Roadmap
- HTTP/2 transport (LCU's preferred path)
- Typed endpoint helpers (
get_current_summoner,get_lobby, …) - Built-in exponential-backoff reconnect helper
-
tracingintegration behind a feature flag - Published typed schemas for common endpoints
Contributing
Issues and PRs welcome. Good first contributions:
- Typed wrappers for common endpoints
- Examples for specific workflows (champ-select overlay, lobby bot, …)
License
MIT © QAQTam