Leyline user guide
Leyline is an HTTP client that mimics browsers on the wire. This guide shows you how to use it, in reading order.
Leyline has two kinds of session:
| You want | Call | What the server sees |
|---|---|---|
| Plain HTTP | leyline::get(url) or Session::new() | A plain client with a leyline/<version> user agent. |
| Look like a browser | Session::browser(Browser::default()) or Session::builder().browser(Browser::Chrome154) | The TLS, HTTP/2, and headers of a captured browser. |
Session::browser(Browser::default()) impersonates the newest bundled Chrome
on Windows. Session::new() and a builder with no browser build a plain client.
- Quick start: add the dependency, send your first request.
- Sessions: pick a browser, a platform, and a brand.
- Choosing a profile: which browser and platform to claim.
- Requests: methods, headers, bodies, presets, tabs, and downloads.
- Responses: status, headers,
Linkpagination, block detection, bodies, timing, audit. - Streaming: stream a body in either direction, download a file, or stop a read early.
- Redirects: the redirect limit and custom redirect policies.
- Retries and timeouts: retry policies and the four timeouts.
- Errors: group errors by category, map them to statuses, and find out which errors a retry covers.
- Proxies: proxy rules, bypass lists, environment discovery.
- Cookies: the jar, saving it, devices, and connection state.
- WebSocket: connect over HTTP/2 or HTTP/1.1.
- HTTP/3: the
http3feature and the protocol policy. - TLS trust: roots, certificate pins, client certificates, the TLS version floor.
- Network: DNS overrides, Happy Eyeballs, socket options.
- Logging and tracing:
tracingtargets and request phases. - Fingerprints: what a profile pins and how to audit it.
- Features and targets: cargo features, targets, MSRV.
- Supported platforms: the six build targets and the tools each one needs.
- Cancellation: what happens when you drop a request or a body.
- Testing: local plain and TLS servers, and asserting what was sent.
- Service integration: Tower, shared sessions, and error mapping in a server.
- Crawling: host limits, proxy pools, block detection, and tags.
- Accounts: tabs, saved devices, and the state of a returning browser.
Reference pages beside this guide: profiles and MSRV.
The Rust blocks in these pages are doctests of the leyline-http crate. The
Tower block in Requests compiles only with the tower
feature. A plain cargo test does not enable it. The pull-request check in CI
runs cargo test with
--features leyline-http/full,leyline-http/bench-internals, which does. The
toml, sh, and js blocks are not compiled. Blocks that open a socket are
marked no_run, so they compile but do not send traffic.
Quick start
This chapter takes you from an empty project to a request and a parsed response. Each section links to the chapter that owns the topic.
Add the dependency
The package is leyline-http. The library it builds is leyline, so that is
the name you import. Leyline is async and runs on Tokio.
[dependencies]
leyline-http = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde and serde_json are only for the JSON examples. The first build
compiles BoringSSL, so it needs a C and C++ compiler and Git. Supported platforms lists the targets and the tools.
Send a GET request
leyline::get sends one GET from a plain session. For more than one request,
build a Session and reuse it: it holds the connection pool and the cookie
jar.
#[tokio::main]
async fn main() -> leyline::Result<()> {
let resp = leyline::get("https://example.com/").await?;
println!("{} {}", resp.status(), resp.url());
Ok(())
}
Session::get and its siblings return a RequestBuilder. Awaiting the
builder sends the request. .send() does the same and returns a future you
can hold in a variable.
Choose a plain or a browser session
| Session | Build it with | Sends |
|---|---|---|
| Plain | Session::new() or Session::builder().build() | user-agent: leyline/<version>, accept: */*, and no sec-* headers |
| Browser | Session::browser(Browser::default()) | The TLS, HTTP/2, and headers of the newest bundled Chrome on Windows |
| Pinned browser | Session::builder().browser(Browser::Chrome154).build() | One fixed browser profile |
Use a plain session for APIs. Use a browser session when the server checks
that the client is a browser. SessionBuilder::user_agent replaces the
default user-agent on either kind.
#![allow(unused)]
fn main() {
use leyline::{Browser, Session};
fn run() -> leyline::Result<()> {
let plain = Session::builder().user_agent("my-app/1.0").build()?;
let chrome = Session::browser(Browser::default());
let pinned = Session::builder().browser(Browser::Chrome154).build()?;
drop((plain, chrome, pinned));
Ok(())
}
}
Sessions lists the headers each kind sends.
Read the body
text() decodes the body with the charset from Content-Type, and falls
back to UTF-8. json::<T>() deserializes into any DeserializeOwned type. A
body that does not parse is an ErrorCategory::Decode error. Each body call
consumes the response, so read status() and headers() first.
#![allow(unused)]
fn main() {
use serde::Deserialize;
#[derive(Deserialize)]
struct State {
name: String,
ready: bool,
}
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let page = session.get("https://example.com/").await?.text().await?;
let state = session
.get("https://api.example/state")
.await?
.json::<State>()
.await?;
println!("{} {} {}", page.len(), state.name, state.ready);
Ok(())
}
}
Send JSON, a form, and query parameters
json serializes the value and sets content-type: application/json. form
sends application/x-www-form-urlencoded. query appends pairs to the URL.
query and form take any iterator of (key, value) pairs where both sides
are AsRef<str>. timeout with a Duration bounds the whole request,
retries included; for a streamed body or a download, add
TimeoutConfig::body.
#![allow(unused)]
fn main() {
use std::time::Duration;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let created = session
.post("https://api.example/items")
.json(&serde_json::json!({ "name": "leyline" }))
.timeout(Duration::from_secs(10))
.await?;
let found = session
.get("https://api.example/search")
.query([("q", "leyline"), ("page", "2")])
.await?;
let login = session
.post("https://api.example/login")
.form([("user", "ada"), ("password", "secret")])
.await?;
println!("{} {} {}", created.status(), found.status(), login.status());
Ok(())
}
}
See Requests and Retries and timeouts.
Handle a 404
A 4xx or 5xx response is Ok by default. Call .error_for_status() on the
request builder to get a status of 400 or more as a Kind::Status error. The
error keeps the status, the headers, and up to 64 KiB of the body.
leyline::http re-exports the http crate, so StatusCode needs no extra
dependency.
#![allow(unused)]
fn main() {
use leyline::http::StatusCode;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
match session.get("https://api.example/users/7").error_for_status().await {
Ok(resp) => println!("{}", resp.text().await?),
Err(err) if err.status() == Some(StatusCode::NOT_FOUND) => {
println!("no such user: {}", err.body_text().unwrap_or_default());
}
Err(err) => return Err(err),
}
Ok(())
}
}
Error::category() sorts every failure into one ErrorCategory, such as
Timeout, Dns, Tls, or Status, for a short message. See
Errors.
Use ? with other error types
leyline::Error converts from std::io::Error and url::ParseError, so ?
works on file calls in a function that returns leyline::Result. It
implements std::error::Error + Send + Sync + 'static, so ? also converts it
into Box<dyn std::error::Error + Send + Sync>, anyhow::Error, or your own
error type.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let token = std::fs::read_to_string("token.txt")?;
let session = leyline::Session::builder()
.bearer_auth(token.trim())
.build()?;
let body = session.get("https://api.example/me").await?.text().await?;
std::fs::write("me.json", body)?;
Ok(())
}
}
Download a file
RequestBuilder::download streams the decoded body to a file in one atomic
step and returns the number of bytes written. A status of 400 or more is an
error. The limit caps the decoded size; None leaves only the session’s
max_body_size (100 MiB by default). See
Streaming.
#![allow(unused)]
fn main() {
use std::time::Duration;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let bytes = session
.get("https://example.com/archive.tar.gz")
.timeout(Duration::from_secs(600))
.download("archive.tar.gz", Some(512 * 1024 * 1024))
.await?;
println!("{bytes} bytes");
Ok(())
}
}
Find the page for your task
| I want to | Read |
|---|---|
| Build a client for a JSON API, end to end | API clients |
| Set a token, a base URL, or share a session across tasks | Sessions |
| Send headers, forms, or the requests of a browser page | Requests |
| Read headers, the final URL, the redirect chain, or timing | Responses |
| Download a large file with a size limit | Streaming |
| Read or stop redirects | Redirects |
| Retry failed requests | Retries and timeouts |
| Map failures to my own errors or HTTP statuses | Errors |
| Keep a login between runs | Cookies |
| Keep an account’s device, tab, and cookies between runs | Accounts |
| Crawl many hosts with rate limits and proxy pools | Crawling |
| Send traffic through a proxy | Proxies |
| Stop a request early | Cancellation |
| Use Leyline inside a web service | Service integration |
| Test against a local server | Testing |
API clients
This chapter builds one plain client for a JSON API: a base URL, a token,
timeouts, retries with a rate limit, status errors, pagination, and uploads.
Each section links to the chapter with the full detail.
examples/api_client.rs puts the steps together.
Build the client once
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::http::StatusCode;
use leyline::{RetryPolicy, Session, TimeoutConfig, WaitFormat};
const BASE_URL: &str = "https://api.example/v1/";
const USER_AGENT: &str = "my-tool/1.0";
const RATE_REMAINING: &str = "x-ratelimit-remaining";
const RATE_RESET: &str = "x-ratelimit-reset";
const MAX_RATE_WAIT: Duration = Duration::from_secs(120);
fn run() -> leyline::Result<()> {
let token = std::env::var("API_TOKEN").unwrap_or_default();
let policy = RetryPolicy::transient()
.retry_if(|resp| {
resp.status() == StatusCode::FORBIDDEN && resp.header(RATE_REMAINING) == Some("0")
})
.wait_header(RATE_RESET, WaitFormat::UnixSeconds)
.max_retry_after(MAX_RATE_WAIT)
.retry_unsent(true);
let api = Session::builder()
.base_url(BASE_URL)
.bearer_auth(&token)
.user_agent(USER_AGENT)
.headers([("accept", "application/json")])
.timeout(
TimeoutConfig::new()
.connect(Duration::from_secs(5))
.total(Duration::from_secs(180)),
)
.retry(policy)
.build()?;
drop(api);
Ok(())
}
}
| Call | Effect |
|---|---|
base_url | Resolves a relative request URL. End it with / to keep the last path segment |
bearer_auth | Sends authorization: Bearer <token> to the base_url origin when a base_url is set, and to every host otherwise |
user_agent | Replaces leyline/<version> |
headers | Default headers for every request |
timeout(TimeoutConfig) | connect bounds each connection, total the whole send |
retry | The default RetryPolicy of every request |
Sessions has the URL resolution rules and the token scope.
Session is Clone, Send, and Sync. A clone shares the pool, the cookie
jar, and every setting, so build the client once and clone it into each task.
Retry transient failures and rate limits
RetryPolicy::transient() retries idempotent methods up to 3 times on a
connection error, a timeout, 429, 502, 503, and 504, with jittered
exponential backoff. The setters in the example extend it:
| Setter | Effect |
|---|---|
retry_if(predicate) | Also retries a response for which the predicate returns true |
wait_header(name, format) | Reads the wait from a header. It wins over Retry-After |
max_retry_after(d) | A longer requested wait ends the retries. The default is 60 s |
retry_unsent(true) | Retries any method when the server did not process the request: a DNS, connect, TLS, or proxy error, a connect timeout, an HTTP/2 REFUSED_STREAM reset, or an HTTP/3 request the server reports it did not process. Not after a redirect leg got a response |
total spans every attempt of one send, waits included. A wait that does not
fit in the time left ends the retries, so set total above max_retry_after
plus the time of the requests. See
Retries and timeouts.
Turn statuses into errors
error_for_status() on the request turns a status of 400 or more into a
Kind::Status error after the retries. The error keeps the status, the
headers, and up to 64 KiB of the body; see
Turn a status into an error.
err.retry_after() returns the wait the policy read from the last response.
err.retries_exhausted() is true when the policy wanted another try and
did not make it; see
Status codes are not errors.
#![allow(unused)]
fn main() {
use leyline::{Error, ErrorCategory};
fn explain(err: &Error) -> String {
match err.category() {
ErrorCategory::Status => match err.retry_after() {
Some(wait) => format!("rate limited, retry in {wait:?}"),
None => format!(
"api error {:?} request {:?}: {}",
err.status(),
err.header("x-request-id"),
err.body_text().unwrap_or_default()
),
},
ErrorCategory::Decode => format!("unexpected body: {err}"),
ErrorCategory::Timeout => "the api timed out".to_owned(),
other => format!("{other:?}: {err}"),
}
}
let _ = explain;
}
See Errors.
Read JSON pages
pages() sends the request, then follows the Link: rel="next" header of
each response with the same settings. See
Responses.
There is no default page limit. Call limit(max) to stop after max pages.
When a next link goes to another origin, the request drops the session
credentials: the Authorization, Cookie, and Proxy-Authorization
defaults and the session bearer token, as a redirect does.
#![allow(unused)]
fn main() {
use serde::Deserialize;
#[derive(Deserialize)]
struct Repo {
name: String,
}
async fn run(api: leyline::Session) -> leyline::Result<()> {
let mut repos = Vec::new();
let mut pages = api.get("repos").error_for_status().pages().limit(100);
while let Some(page) = pages.next().await {
repos.extend(page?.json::<Vec<Repo>>().await?);
}
for repo in &repos {
println!("{}", repo.name);
}
Ok(())
}
}
Send JSON and upload files
json(&value) sends a JSON body. multipart(form) sends
multipart/form-data; Part::file(path)? streams a file from disk, and
mime sets the part’s content-type. See
Requests.
#![allow(unused)]
fn main() {
use leyline::multipart::{Form, Part};
async fn run(api: leyline::Session) -> leyline::Result<()> {
api.post("issues")
.json(&serde_json::json!({ "title": "hello" }))
.error_for_status()
.await?;
let form = Form::new()
.text("title", "report")
.part("file", Part::file("report.pdf")?.mime("application/pdf"));
api.post("uploads").multipart(form).error_for_status().await?;
Ok(())
}
}
Response bodies are decoded for you: the default features cover gzip, deflate, Brotli, and zstd. See Features and targets.
Sessions
A Session owns everything a request needs: the browser profile, the TLS
context, the connection pool, and the cookie jar. Build one, reuse it, and
clone it into tasks. This chapter covers building, deriving, stopping, and
saving a session.
Build a session
Session::builder() returns a SessionBuilder. build() returns
Result<Session>, so a bad proxy URL or an unknown profile fails there
rather than at the first request.
#![allow(unused)]
fn main() {
use leyline::{Browser, Platform, Session};
fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::Firefox154)
.platform(Platform::MacOS)
.user_agent("my-tool/1.0 (+https://my-tool.example)")
.headers([("accept", "application/json")])
.build()?;
let _ = session;
Ok(())
}
}
Session::new() and Session::default() build a plain session and do not
fail. Session::browser(b) builds a session for browser b and does not fail
either. A trust-store problem then fails each request that needs it with
Kind::Tls, and an invalid proxy in the environment fails each request with
Kind::Proxy. To add settings to a browser session, start from
Session::builder().browser(b), whose build() reports those problems.
SessionBuilder::headers takes any iterator of (name, value) pairs, or
references to pairs, where both sides are AsRef<str>. An entry replaces a
default or profile header of the same name, compared without case.
user_agent(value) replaces the default user-agent; it and a user-agent
pair in headers set the same header, and the later call wins. On a browser
session, a session user-agent also removes the profile’s sec-ch-ua client
hint, so the user agent and the hint never name different browsers.
When a session has a browser and you do not call protocol, it uses
ProtocolPolicy::Race if the http3 feature is on and the profile’s [h3]
table sets race = true, as the bundled Chrome profiles do, and
ProtocolPolicy::Auto otherwise. See HTTP/3.
Plain and browser sessions
| Session | Build it with | Imitates |
|---|---|---|
| Plain | Session::new(), Session::builder().build(), or leyline::get(url) for one request | Nothing |
| Browser | Session::browser(b), or a builder with .browser(), .profile(), or .identity() | A captured browser |
A plain HTTP/1.1 GET sends these headers, in this order:
| Header | Value |
|---|---|
Host | From the URL |
user-agent | leyline/<crate version> |
accept | */* |
accept-encoding | The codings compiled in and on in CompressionConfig, in the order gzip, deflate, br, zstd. Left out when none is on |
Connection | keep-alive. Left out on HTTP/2 and HTTP/3 |
A plain session sends no accept-language and no browser-only headers:
sec-* headers, client hints, upgrade-insecure-requests, and priority.
This is true for every preset. languages turns accept-language on. A
request adds the headers it needs, such as content-type, origin,
referer, and cookie.
Set a token and a base URL
bearer_auth(token) sends authorization: Bearer <token> and replaces an
authorization header set earlier on the builder. With a base_url, the
token goes only to requests whose origin (scheme, host, and port) matches the
base URL. Without one, it goes to every host. A redirect to another origin
drops it. RequestBuilder::bearer_auth overrides it for one request, which
is how to reach a second host. An authorization header set with headers
is not scoped. Debug output masks the token.
base_url(url) resolves a relative &str or String request URL by the
rules of Url::join (RFC 3986). A Url value goes out unchanged.
| Base URL | Request URL | Result |
|---|---|---|
https://api.example/v1/ | repos/x | https://api.example/v1/repos/x |
https://api.example/v1/ | /health | https://api.example/health |
https://api.example/v1 | repos/x | https://api.example/repos/x |
| any | https://example.com/a | https://example.com/a |
End the base URL with / to keep its last path segment. An invalid base URL
fails build() with Kind::Config. A relative URL in a session with no base
URL fails at send() with Kind::Url.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let api = leyline::Session::builder()
.base_url("https://api.example/v1/")
.bearer_auth("my-token")
.build()?;
let repo = api.get("repos/leyline").await?;
let staging = api.with_base_url("https://staging.api.example/v1/")?;
let health = staging.get("/health").await?;
drop((repo, health));
Ok(())
}
}
with_base_url(url) derives a session with another base URL; the session
token moves with it and goes to the new base URL’s origin. It returns
Kind::Url for a URL that does not parse, and Kind::Config for a URL that
cannot be a base.
Set the languages
languages(tags) sets accept-language in the format of the session’s
browser family. The first tag has the highest weight. An invalid tag or an
empty list fails build() with Kind::Config. A plain session uses the
Chromium format.
| Family | Rule | Tags | accept-language |
|---|---|---|---|
| Chromium | Adds the base language after each region tag, unless the next tag has the same base. Removes duplicates. Weights go down by 0.1 from 1, to a minimum of 0.1 | ["de-DE", "de", "en-US"] | de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7 |
| Firefox | No base language added. Canonical case. Weights go down in equal steps from 1, as Firefox computes them | ["fr", "en"], ["de-DE", "de", "en"] | fr,en;q=0.5, de-DE,de;q=0.7,en;q=0.3 |
| Safari | The first tag only | ["de-DE", "de"] | de-DE |
Without languages, a browser session sends the value captured with its
profile. SessionIdentity::languages() returns the tags the session was built
with.
#![allow(unused)]
fn main() {
use leyline::{Browser, Session};
fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.languages(["de-DE", "de", "en-US"])
.build()?;
let langs = session.identity().languages().map(<[String]>::to_vec);
let _ = langs;
Ok(())
}
}
Choose a browser
The Browser enum has a variant for each bundled profile. The
API reference lists them, and
Browser profiles gives the capture kind of each.
Browser::latest(Family)is the newest profile of a product line that is not deprecated and whose capture kind the line accepts. Pin the line and take what the crate release carries.Browser::default()isBrowser::latest(Family::Chrome).Browser::get(Family, version)returns the profile for one major version, orNone.Browser::version()returns the major version.Browser::family()returns the product line. The engine family is themeta.familyfield ofBrowser::profile().
#![allow(unused)]
fn main() {
use leyline::{Browser, Family};
let latest = Browser::latest(Family::Firefox);
assert_eq!(latest.family(), Family::Firefox);
assert_eq!(Browser::default().family(), Family::Chrome);
}
Choose a platform
Platform sets the operating system the session claims: Windows, MacOS,
Linux, Android, IOS, or Host. Platform::Host resolves to the OS you
compiled for, or Windows for an unrecognized target. The platform drives
Sec-CH-UA-Platform, Sec-CH-UA-Mobile, and the TCP fingerprint.
Platform defaults and call order
- A browser with no platform claims the first platform, in the order Windows,
macOS, Linux, Android, iOS, that its profile covers. Chrome claims Windows,
Browser::Safari26macOS, andBrowser::SafariIOS27iOS.Session::browser(b)uses the same rule. - A
.profile()with no.platform()claims Windows and logs oneinfoevent under theleyline::sessiontarget. .browser()and.platform()map the browser to its sibling for the platform, in either call order.Browser::for_platformdoes the same.- A session with no browser claims the host platform.
Apply a brand overlay
Edge and Opera are overlays on a Chrome profile: the HTTP/2 settings stay
Chrome’s, and the User-Agent and sec-ch-ua brand list change. Edge also
leaves out the trust anchors extension of the ClientHello.
#![allow(unused)]
fn main() {
use leyline::{Browser, ChromiumBrand, Session};
fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.brand(ChromiumBrand::Edge)
.build()?;
let _ = session;
Ok(())
}
}
Both brands exist on desktop platforms only; a mobile platform fails
build(). Edge takes its version from Chromium and fits every desktop Chrome
profile. Opera supports Chromium 145 to 153, so Browser::default() with
ChromiumBrand::Opera can fail build(); pin an older Chrome such as
Browser::Chrome153. Brave has its own profiles, such as
Browser::Brave146.
ChromiumBrand::all() lists the brands. A brand prints as its lowercase name
(chrome, edge, opera), and str::parse reads it back without case. An
unknown name is a Kind::Config error.
Browser::identity(platform, brand) returns the PlatformIdentity a session
sends: user_agent, sec_ch_ua, and accept_language. Use it when a payload
that is not a header must carry the same values. It returns None when the
browser has no identity for the platform or the brand does not apply.
Session::identity() returns a SessionIdentity with the values build()
resolved: identity() (None for a plain or loaded-profile session),
browser(), platform(), brand(), and user_agent(). A user-agent from
SessionBuilder::headers is the value user_agent() returns.
#![allow(unused)]
fn main() {
use leyline::{Browser, ChromiumBrand, Platform};
let edge = Browser::default().identity(Platform::Windows, Some(ChromiumBrand::Edge));
assert!(edge.is_some_and(|id| id.user_agent.contains("Edg/")));
let session = leyline::Session::browser(Browser::default());
let sent = session.identity();
println!("{:?} on {} sends {}", sent.browser(), sent.platform(), sent.user_agent());
}
Share a session
A clone is an Arc clone: it shares the cookie jar, the connection pool, the
TLS context, and every setting. Session is Send + Sync. Pass clones into
tasks instead of building a second session.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let worker = session.clone();
tokio::spawn(async move { worker.get("https://example.com/").await })
.await
.expect("task panicked")?;
Ok(())
}
}
host_limits(HostLimits) and proxy_pool(ProxyPool) are shared by clones
and derived sessions too. See Crawling and
Proxies.
Derive a session
A derived session changes one setting and shares the rest with its parent.
None of the calls below fail except with_base_url and with_identity.
| Derive | Changes | Cookie jar | Pool | TLS session cache |
|---|---|---|---|---|
clone() | Nothing | Shared | Shared | Shared |
with_proxy(config) | The proxy | Shared | Shared | Shared |
with_cookie_jar(jar) | The jar | The new jar | Shared | Shared |
with_redirect(policy) | The redirect policy | Shared | Shared | Shared |
with_base_url(url) | The base URL | Shared | Shared | Shared |
with_identity(identity) | The browser identity | Shared | Shared, own partition | New |
fresh_pool() | Nothing | Shared | New | New |
Every derived session keeps the host limits, the proxy pool, the audit setting, and the shutdown state.
with_proxytakesimpl Into<ProxyConfig>. Pool entries are keyed by proxy, so connections never cross proxies. An invalid URL or an unsupported scheme fails the firstsend()that uses it, withKind::Config.with_identitysends anotherIdentity. Its connections sit in their own partition of the parent’s pool, so they never cross identities, and they share the parent’s pool limits andpool_stats(). It also shares the HSTS store and theAlt-Svcknowledge. It returnsKind::Configfor a plain session. See Mix and rotate identities.fresh_poolstarts with an empty pool, TLS session cache, and HSTS store, and has its own pool limits. Call it when the next request must open new connections.tab()returns aTabthat keeps the current page. See Requests.
#![allow(unused)]
fn main() {
use leyline::cookie::Jar;
fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let other_user = session.with_cookie_jar(Jar::new());
let via_proxy = session.with_proxy("http://user:pass@proxy.example:8080");
let new_exit = via_proxy.fresh_pool();
let no_follow = session.with_redirect(leyline::RedirectPolicy::none());
let _ = (other_user, new_exit, no_follow);
Ok(())
}
}
Stop a session
Session::shutdown() stops a session, its clones, and every session derived
from it. Requests in flight and new requests fail with Kind::Request and the
message “session shut down”, and a streamed body fails on its next read.
Error::is_shut_down() is true for this error, and Session::is_shut_down()
is true after the call.
WebSocket handshakes and Session::preconnect follow the same rule: a call
after shutdown fails with the shut-down error before it sends any bytes, and a
call in flight ends with that error. An established WsConnection stays open
until the caller closes it. Shutdown does not close idle pooled connections.
#![allow(unused)]
fn main() {
use leyline::Kind;
async fn run() {
let session = leyline::Session::new();
let worker = session.clone();
session.shutdown();
assert!(worker.is_shut_down());
let err = worker.get("https://example.com/").await.unwrap_err();
assert_eq!(err.kind(), Kind::Request);
assert!(err.is_shut_down());
}
}
Save and restore an identity
Identity implements Serialize and Deserialize. Browser, Platform,
Family, and ChromiumBrand serialize as stable lowercase ids, such as
chrome-154, windows, and edge; id() returns the id and str::parse
reads it back. Display prints a label for people, not the id.
session.identity().to_identity() returns the Identity that rebuilds the
session, and SessionBuilder::identity(id) builds from it.
profile_id() returns 16 hex characters: a hash of the profile data the
session sends, with the platform and the brand. The same data gives the same
id in every release. It is None for a plain session. A profile loaded from
TOML gets an id from its TOML text. SessionBuilder::expect_profile_id(id)
makes build() fail with Kind::Config when the id differs or is None;
Error::is_profile_changed() is true for that error.
#![allow(unused)]
fn main() {
use leyline::{Browser, Identity, Platform, Session};
fn run() -> Result<(), Box<dyn std::error::Error>> {
let session = Session::builder()
.browser(Browser::Chrome154)
.platform(Platform::MacOS)
.build()?;
let sent = session.identity();
let saved = serde_json::to_string(&sent.to_identity())?;
let profile_id = sent.profile_id().map(str::to_owned).ok_or("plain session")?;
let identity: Option<Identity> = serde_json::from_str(&saved)?;
let restored = Session::builder()
.identity(identity.ok_or("plain session")?)
.expect_profile_id(&profile_id)
.build()?;
drop(restored);
Ok(())
}
}
To save the identity with the cookie jar and the proxy, use a Device; see
Accounts. Session::state() returns the TLS session tickets,
the Alt-Svc entries, and the HSTS store as a SessionState; see
Accounts.
Keep connections warm
preconnect(url) opens the TCP, proxy, TLS, and HTTP/2 connection for an
https origin and stores it in the pool, so the first request skips the
handshake. It does nothing for an http URL or under
ProtocolPolicy::Http1. If the origin speaks only HTTP/1.1, the session
records that and returns Ok. To warm a connection through another proxy,
call session.with_proxy(proxy).preconnect(url).
Before the pool reuses an HTTP/2 connection idle for 10 seconds, it sends a
PING, as Chrome does. Without an answer in 2 seconds it drops the connection
and opens a new one, and pool_stats().h2_ping_failures counts it.
Network lists the PoolConfig settings.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::browser(leyline::Browser::default());
session.preconnect("https://example.com/").await?;
println!("{:?}", session.pool_stats());
Ok(())
}
}
Trace the request lifecycle
trace() installs a listener for each phase of every request: name
resolution, connect, TLS handshake, request sent, response head, and
completion. Implement leyline::trace::Trace and override only the events you
want; every method has a no-op default. TracingTrace is the bundled
listener; see Logging and tracing.
#![allow(unused)]
fn main() {
use leyline::Session;
use leyline::trace::{Head, Trace};
struct Slow;
impl Trace for Slow {
fn head(&self, ev: &Head<'_>) {
if ev.elapsed.as_millis() > 500 {
eprintln!("{} took {:?} to answer", ev.host, ev.elapsed);
}
}
}
fn run() -> leyline::Result<()> {
let session = Session::builder().trace(Slow).build()?;
let _ = session;
Ok(())
}
}
- Every event carries an
idunique per attempt. A retry is a new attempt with a newid. - An event fires on the task that produced it. Leyline opens an HTTP/2 or
HTTP/3 connection on a task it spawns, so those events fire there, with the
idof the request that started the open. A listener that blocks slows every request waiting for that connection. dnsfires each time Leyline resolves a name for a TCP connection. Through a proxy it reports the proxy host. A direct HTTP/3 connection fires none.sentcarries the method and the path with the query. On HTTP/2 and HTTP/3 it fires before the connection driver takes the request, and itselapsedis zero.headcarries the response headers as received, before decompression and redirect handling.
Response::timing() needs no listener. See Responses.
Choosing a profile
A browser session has a profile and a platform. The profile sets the TLS ClientHello, the HTTP/2 settings, and the request headers. The platform sets the operating system that the session claims. This page picks both.
Start with the default
Browser::default() is Browser::latest(Family::Chrome), the newest bundled
Chrome. Without .platform(), a Chrome session claims Windows.
#![allow(unused)]
fn main() {
use leyline::{Browser, Platform, Session};
fn run() -> leyline::Result<()> {
let windows = Session::browser(Browser::default());
let mac = Session::builder()
.browser(Browser::default())
.platform(Platform::MacOS)
.build()?;
let _ = (windows, mac);
Ok(())
}
}
With the http3 feature, a Chrome session races HTTP/3 against HTTP/2 for an
origin that advertised h3. See HTTP/3.
Pick another browser
| You need | Use |
|---|---|
| Firefox | .browser(Browser::latest(Family::Firefox)) |
| Safari on macOS | .browser(Browser::Safari26).platform(Platform::MacOS) |
| Safari on iPhone | .browser(Browser::latest(Family::SafariIos)). It needs no .platform(): the session claims iOS |
| Edge or Opera | .brand(ChromiumBrand::Edge) or .brand(ChromiumBrand::Opera) on a Chrome profile. See Sessions |
| Brave | .browser(Browser::Brave146).platform(Platform::MacOS) |
| A phone app | Browser::OkHttpAndroid10 or Browser::CfnetworkIOS18 |
| A phone browser | .platform(Platform::Android) or .platform(Platform::IOS) with a browser that has that platform |
| One fixed version | .browser(Browser::Chrome150), then .platform(...) |
.browser(Browser::Safari26).platform(Platform::IOS) maps to the Safari on
iPhone sibling. Sessions
gives the platform defaults and the call order, and
Set the languages sets accept-language.
Match the platform
The platform sets Sec-CH-UA-Platform, Sec-CH-UA-Mobile, the User-Agent,
and the TCP fingerprint. A site can compare the claimed OS with the TCP
fingerprint, so a claim that differs from the real host is visible. Use
Platform::Host to claim the OS you build for.
Keep a device stable
Browser::latest moves to newer captures in patch releases, and a release can
correct the data behind a pinned variant. For an account that must keep one
device, pin the variant and store session.identity().profile_id(). Pass it
to SessionBuilder::expect_profile_id on start: build() then fails with
Kind::Config when the profile changed, and err.is_profile_changed() is
true. To keep the data itself, freeze it with Device::pin_profile. See
Profile data stability and
Accounts.
Read the JA4 labels
The profile reference labels the JA4 of each profile gated
or estimated. A Chrome or Brave profile is estimated because Chrome
shuffles the extension order on every connection. JA4 sorts the extensions
before it hashes them, so the JA4 stays the same; JA3 does not. To check a
live session, see
Fingerprints.
Next
Read Requests to build and send a request.
Requests
Session::get and its siblings return a RequestBuilder. Chain settings on
the builder, then await it. This chapter covers methods, headers, bodies, the
fetch context of a browser request, and per-request overrides. The API uses
the http crate’s types, re-exported as leyline::http.
Methods and URLs
The session has get, post, put, patch, delete, and head. For any
other method, call request with an http::Method. Each takes a &str, a
String, a &String, a url::Url, or a &url::Url.
#![allow(unused)]
fn main() {
use leyline::http::Method;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.request(Method::OPTIONS, "https://example.com/api").await?;
println!("{}", resp.status());
Ok(())
}
}
A relative &str or String URL resolves against the session base URL; see
Set a token and a base URL. A URL
that does not parse does not panic: send returns Kind::Url with the
url::ParseError as its source.
Headers
Header setters take anything that converts to an http::HeaderName and an
http::HeaderValue, such as &str, String, or a HeaderName constant. An
invalid name or value is an error from send. header appends: a second
call with the same name adds a second value. headers takes an iterator of
pairs. bearer_auth and basic_auth build the Authorization header.
On HTTP/2 and HTTP/3 the session does not send a Host header you set,
because the request authority carries the host.
#![allow(unused)]
fn main() {
use leyline::http::header::ACCEPT_LANGUAGE;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session
.get("https://example.com/")
.header("x-request-id", "1")
.header(ACCEPT_LANGUAGE, "en-GB,en;q=0.9")
.header("x-tag", "a")
.header("x-tag", "b")
.await?;
let _ = resp;
Ok(())
}
}
On a browser session, see Override headers safely for the headers you can change without breaking the fingerprint.
Order
Order matters to a fingerprint, so the profile’s order applies on the wire. A
request header replaces every session or profile header of the same name, in
that header’s position; repeated header calls for the name send every value
there. A name the session does not send goes where a browser places it, such
as authorization after user-agent, or else at the end.
Two methods override the order:
header_order(&["a", "b"])pins the wire order of the regular headers for this request, on every protocol.anchored(anchor, name, value)inserts one header at aHeaderAnchorslot:BeforeCchUa,AfterCchUa,AfterCchUaMobile,AfterCchUaPlatform,AfterUserAgent,AfterAccept,AfterContentType,AfterFetchDest, orBeforeAcceptEncoding.
#![allow(unused)]
fn main() {
use leyline::HeaderAnchor;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session
.get("https://example.com/")
.anchored(HeaderAnchor::AfterUserAgent, "x-client", "leyline")
.header_order(&["x-client", "accept"])
.await?;
let _ = resp;
Ok(())
}
}
To see the headers a request was prepared with, build the session with
.audit(true) and read Response::request_headers.
Query parameters and bodies
query appends pairs to the URL after any query it already has. form sends
pairs as application/x-www-form-urlencoded. Both take any iterator of
(K, V) tuples, or references to them, where K and V are AsRef<str>.
One request can carry both.
body takes a Body, or anything that converts to one: String,
&'static str, Vec<u8>, &'static [u8], Bytes, or (). json sets
content-type: application/json. compress(encoding) compresses a buffered
body and sets Content-Encoding.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let search = session
.post("https://example.com/search")
.query([("page", "2")])
.form([("q", "leyline"), ("sort", "new")])
.await?;
let item = session
.post("https://example.com/items")
.json(&serde_json::json!({ "name": "leyline" }))
.await?;
let raw = session
.post("https://example.com/raw")
.header("content-type", "application/octet-stream")
.body(vec![0u8, 1, 2])
.await?;
let _ = (search, item, raw);
Ok(())
}
}
To stream a request body, pass Body::stream; see
Streaming.
Upload multipart forms
The multipart feature, on by default, adds leyline::multipart::{Form, Part}
and RequestBuilder::multipart, which sets content-type: multipart/form-data with the form’s boundary. A file part streams from disk
chunk by chunk.
#![allow(unused)]
fn main() {
use leyline::multipart::{Form, Part};
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let form = Form::new()
.text("name", "ada")
.file("log", "server.log")?
.part("logo", Part::file("logo.png")?.mime("image/png"))
.part(
"photo",
Part::bytes(std::fs::read("photo.jpg")?)
.filename("photo.jpg")
.mime("image/jpeg"),
);
let resp = session.post("https://api.example/upload").multipart(form).await?;
let _ = resp;
Ok(())
}
}
Form::file(name, path) and Part::file(path) use the last component of
path as the filename, or file when it has none or is not UTF-8. A
Form::file part has no Content-Type, so the server reads it as
text/plain (RFC 7578); use Part::file(path)?.mime(..) to send a type.
Both return std::io::Result, which ? converts into leyline::Error.
examples/multipart.rs is a
complete program.
Presets
A Preset sets the sec-fetch-* headers and the header order for a fetch
context: Native, Navigate, FrameNavigate, Reload, Script, Image,
Xhr, Form, CrossOrigin, SameSite, and FormNavigate.
Preset::Navigate is the default for a GET from a browser session.
On a browser session with no preset, a POST, PUT, or PATCH infers one from
content-type: application/json gives Xhr, and
application/x-www-form-urlencoded gives Form. Other types leave the
preset unset. Set .preset(..) when the guess is wrong.
Form is a script that posts form data with fetch(). FormNavigate is a
person who clicks the submit button of an HTML form, as on a login page;
form() never infers it.
| Preset | sec-fetch-mode | sec-fetch-dest | sec-fetch-user | Other headers |
|---|---|---|---|---|
Form | cors | empty | none | A script accept, priority: u=1, i |
FormNavigate | navigate | document | ?1 | An HTML accept, upgrade-insecure-requests: 1, cache-control: max-age=0 |
Send the requests of a page
Pass the page that sends a request to initiator. Leyline then sets
Referer, Origin, and sec-fetch-site the way the browser does for the
request’s preset. A navigation with an initiator is a link click. A
navigation without one is a typed URL: sec-fetch-site: none and no
Referer.
#![allow(unused)]
fn main() {
use leyline::Preset;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::browser(leyline::Browser::default());
let page = session.get("https://shop.example/cart").await?;
let page_url = page.url().clone();
let cart = session
.get("https://shop.example/api/cart")
.preset(Preset::Xhr)
.initiator(page_url.clone())
.await?;
let logo = session
.get("https://cdn.example/logo.png")
.preset(Preset::Image)
.initiator(page_url)
.await?;
drop((cart, logo));
Ok(())
}
}
Referer follows strict-origin-when-cross-origin. P is the page and T the
target:
| P and T | Referer |
|---|---|
| Same origin | The full page URL, without fragment and user info |
| Same site or cross site | The page origin and / |
P is https, T is http | None |
| Preset | No initiator | Same origin | Same site or cross site | P is https, T is http |
|---|---|---|---|---|
Xhr GET | None | None | P origin | P origin |
Xhr POST, Form | T origin | T origin | P origin | P origin |
FormNavigate | T origin | T origin | P origin | null |
Image, Script | None | None | None | None |
The second table gives the Origin header. sec-fetch-site is computed from
the page and every redirect, by the same rules for Chromium and Gecko
profiles. sec-fetch-user: ?1 goes only with Navigate and FormNavigate.
A referer header you set wins over the computed value.
Keep the page with a tab
A Tab keeps the current page, as a browser tab does: each request sends
that page as its initiator, and each navigation sets the next page.
Session::tab() makes one, and clones of a tab share the page.
| Call | Sends | Changes the page |
|---|---|---|
open(url) | A typed URL: no initiator | Yes |
follow(url) | A link click from the current page | Yes |
submit(url, fields) | A clicked form, Preset::FormNavigate | Yes |
submit_form(&form) | A parsed html::Form, with its own method and action | Yes |
xhr(url), fetch(method, url), post_json(url, &body) | A script request, Preset::Xhr | No |
subresource(url, preset) | An image or script, for example Preset::Image | No |
The page changes after each navigation response, 4xx and 5xx included, to
the final URL after redirects. A transport error leaves it unchanged.
Relative URLs resolve against the current page, or else the base URL. A
.stream() navigation changes the page when the response head arrives.
set_current(page) sets the page by hand. A script request or subresource on
a tab with no page fails with Kind::Request and sends nothing: open a page
first.
#![allow(unused)]
fn main() {
use leyline::Preset;
async fn run() -> leyline::Result<()> {
let tab = leyline::Session::browser(leyline::Browser::default()).tab();
tab.open("https://shop.example/").await?;
tab.follow("/cart").await?;
let cart = tab.xhr("/api/cart").await?;
let logo = tab.subresource("https://cdn.example/logo.png", Preset::Image).await?;
tab.submit("/checkout", [("step", "address")]).await?;
println!("{:?}", tab.current());
drop((cart, logo));
Ok(())
}
}
Accounts uses a tab with a saved device and submits the forms of a page.
Override the session for one request
| Call | Effect for this request |
|---|---|
proxy(url) | Sends through this proxy |
timeout(..) | Overrides the session timeouts |
redirect(policy) | Overrides the redirect policy; see Redirects |
retry(policy) | Overrides the retry policy |
cookie_jar(jar) | Reads and writes jar for the request and its redirects; the session jar does not change |
tag(text) | Names the request in the trace summary and the TracingTrace line; see Logging and tracing |
timeout takes a Duration or a TimeoutConfig. A Duration sets total,
which bounds the request up to the head of a streamed response; set
TimeoutConfig::body to bound a streamed body or a download. A
TimeoutConfig sets total, read, body, and response_header; an unset
field keeps the session value, and None turns that timeout off. connect
is session-wide, because connections are pooled. See
Retries and timeouts.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::TimeoutConfig;
use leyline::cookie::Jar;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let guest = Jar::new();
let resp = session
.get("https://example.com/slow")
.proxy("http://user:pass@proxy.example:8080")
.timeout(
TimeoutConfig::new()
.total(Duration::from_secs(30))
.read(Duration::from_secs(5)),
)
.cookie_jar(guest.clone())
.tag("account-7")
.await?;
println!("{} cookies", guest.all_cookies().len());
drop(resp);
Ok(())
}
}
Send an http::Request
Session::execute sends an http::Request<Body> with its method, URI,
headers, and body. It reads an optional Preset, TimeoutConfig,
RetryPolicy, RedirectPolicy, and ProxyConfig from the request
extensions, infers the preset from content-type as the builder does, and
reads the whole response before it returns. Digest authentication and
response streaming need the RequestBuilder.
#![allow(unused)]
fn main() {
use leyline::Body;
use leyline::http::{Method, Request};
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let req = Request::builder()
.method(Method::POST)
.uri("https://example.com/items")
.header("content-type", "application/json")
.body(Body::from(r#"{"ok":true}"#))
.expect("valid request");
let resp = session.execute(req).await?;
let _ = resp;
Ok(())
}
}
The tower feature adds LeylineService, a tower_service::Service over the
same send path. See
Service integration.
Responses
A Response carries the status, the headers, the body, and the metadata
Leyline collected while sending: the final URL, the redirect chain, the proxy,
the timing, and the TLS facts. Read the metadata first, because every body
call consumes the response.
Status and headers
status() returns an http::StatusCode, with is_success,
is_redirection, is_client_error, and is_server_error. version()
returns HttpVersion::Http1_1, Http2, or Http3, and as_str() gives
"HTTP/1.1", "HTTP/2", or "HTTP/3".
headers() returns the http::HeaderMap, duplicates included. Iteration
does not follow wire order; get_all yields the values of one name in the
order they arrived. header(name) returns the first value as &str, or
None when it is missing or holds a byte outside visible ASCII. Read
headers() for the raw bytes.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.get("https://example.com/").await?;
if resp.status().is_success() {
println!("{} {}", resp.status().as_u16(), resp.version().as_str());
}
let ctype = resp.header("content-type");
let cookies: Vec<&[u8]> = resp
.headers()
.get_all("set-cookie")
.iter()
.map(|v| v.as_bytes())
.collect();
let _ = (ctype, cookies);
Ok(())
}
}
content_length() parses Content-Length. When Leyline decodes a buffered
body, it removes Content-Encoding and Content-Length from the headers. A
.stream() response keeps the wire values.
request_headers() lists the headers the session prepared to send, in send
order, after the preset, the cookie jar, and your headers were merged. It
needs .audit(true) on the session and is empty otherwise. The values are
prepared before dispatch, not captured from the transport.
trailers() iterates trailing headers in wire order. Buffered HTTP/2 and
HTTP/3 responses carry them; a streamed response and HTTP/1.1 yield none.
Relay headers
relay_headers(body) returns the headers without the hop-by-hop ones:
connection, each header that connection names, keep-alive,
proxy-connection, proxy-authenticate, proxy-authorization, te, trailer, transfer-encoding, and upgrade (RFC
9110, section 7.6.1). RelayBody::AsReceived matches a body from
into_stream(). RelayBody::Decoded matches a body from bytes(), text(),
or into_decoded_stream(), and also drops content-encoding and
content-length when Leyline decodes the body. When the session does not
decode the coding, for example because CompressionConfig turns it off, the
body arrives as sent and content-encoding stays. proxy-authenticate and
proxy-authorization are dropped as hop-by-hop headers. See
Service integration.
Bodies
Without .stream(), Leyline reads, decodes, and caps the whole body before
send() returns. With .stream(), send() returns at the headers. The
reading calls work in both modes: a buffered body returns at once, and a
streamed one is drained, with the read timeout on each chunk, then decoded.
| Call | Returns | Notes |
|---|---|---|
text().await | Result<String> | Decodes with the Content-Type charset, default UTF-8 |
text_with_charset(label).await | Result<String> | Uses label when the response declares no charset |
bytes().await | Result<bytes::Bytes> | The decoded body |
json::<T>().await | Result<T> | Deserializes with serde. A failure is Kind::Json, in ErrorCategory::Decode |
into_decoded_stream(limit) | Result<BodyStream> | The decoded body as a stream, capped at limit |
copy_decoded_to(writer, limit).await | Result<u64> | Writes the decoded body, capped at limit |
download_to(path, limit).await | Result<u64> | Writes the decoded body to a file in one atomic step |
into_stream(), copy_to(writer).await | Result<BodyStream>, Result<u64> | The bytes as sent on a .stream() response; the decoded body on a buffered one |
read_until(limit, done).await | Result<Vec<u8>> | Stops early on a predicate or a byte limit. RequestBuilder::read_until streams for you and also says why it stopped; see Streaming |
Streaming covers the streaming calls and downloads.
text() replaces invalid sequences with U+FFFD, and a leading byte order mark
overrides the declared charset. Without the charset feature, on by default,
text() uses lossy UTF-8.
Size cap and content coding
CompressionConfig::max_body_size (100 MiB by default) caps a buffered body
and the decoded body, on every protocol. A body over the cap fails with
Kind::Body, and the message names max_body_size. Set the cap on the
session with SessionBuilder::compression:
#![allow(unused)]
fn main() {
use leyline::{CompressionConfig, Session};
fn run() -> leyline::Result<()> {
let session = Session::builder()
.compression(CompressionConfig::new().max_body_size(8 * 1024 * 1024))
.build()?;
let _ = session;
Ok(())
}
}
CompressionConfig::new() turns on gzip, deflate, br, and zstd, and a
session decodes and advertises those that are also compiled in.
CompressionConfig::none() turns them all off. Repeated Content-Encoding
fields combine into one list. More than four codings fail with
Kind::Decode. When a listed coding is turned off, the body comes back as
received.
Final URL and redirects
url() returns the final URL after redirects. redirect_chain() returns
each URL that answered with a redirect Leyline followed, in order, without
the final URL and without user name or password. It is empty when nothing
redirected. attempts() returns the number of transport attempts, 1 when no
retry ran.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.get("https://example.com/old").await?;
for url in resp.redirect_chain() {
println!("via {url}");
}
println!("landed on {} after {} attempts", resp.url(), resp.attempts());
Ok(())
}
}
See Redirects.
Follow Link pagination
link(rel) returns the URL of the first Link entry with that relation.
links() returns every entry as a Link with url, rel, and params.
Parsing follows RFC 8288: several headers, comma-separated entries, quoted
parameters, and several relations in one rel. Relative URLs resolve
against url(), and the rel match ignores case.
RequestBuilder::pages() turns a request into a Pages sequence that
follows Link: rel="next". Await pages.next(), or use it as a
futures_util::Stream. Each follow-up is a GET with the first request’s
headers, timeouts, retry policy, and error_for_status() setting. The
sequence ends after a page with no next link, after an error, or at a
next URL it already fetched. The first request URL and the final URL of
each page, after redirects, count as fetched.
#![allow(unused)]
fn main() {
use serde::Deserialize;
#[derive(Deserialize)]
struct Item {
id: u64,
name: String,
}
async fn run() -> leyline::Result<()> {
let api = leyline::Session::builder().bearer_auth("my-token").build()?;
let mut pages = api
.get("https://api.example/items")
.error_for_status()
.pages();
while let Some(page) = pages.next().await {
for item in page?.json::<Vec<Item>>().await? {
println!("{} {}", item.id, item.name);
}
}
Ok(())
}
}
Cookies
The session jar stores Set-Cookie automatically. cookies() parses the
Set-Cookie headers of the final response only. It lists every header that
parses, deletions included, and does not ask the jar, so it can list a cookie
the jar refused. Read the jar with session.cookies().get_cookie(&url, name).
See Cookies.
Turn a status into an error
A 4xx or 5xx response is Ok. error_for_status() consumes the response and
returns a Kind::Status error for a status of 400 or more, with the status,
the headers, and the final URL. error_for_status_ref() makes the same check
on a borrowed response, so you can still read the body of an error.
RequestBuilder::error_for_status() checks the final response after the
retry policy is done, and keeps the start of the body in the error:
| Limit | Default | Set with |
|---|---|---|
| Body bytes kept, after decoding | 64 KiB, and never more than max_body_size | CompressionConfig::max_error_body |
| Time to read that body | 10 s, and never past the total timeout | TimeoutConfig::error_body |
Leyline decides after the status line. A response below 400 is read in full
as usual. An error response is read only up to these limits, so a large or
slow error body cannot hold the request to the body timeout or exceed
max_body_size. Decompression stops at the byte limit, so a small
compressed body cannot expand past it.
The error-body timeout starts when the response head arrives, and each error
response gets one window, however its body is read. If the read fails or the
window ends first, the error keeps the bytes decoded so far, or no body if
none arrived, with the status and headers. The error’s headers drop
content-encoding when Leyline decoded the body, and content-length when
the kept body is not the full declared length.
max_error_body counts decoded bytes. Over HTTP/1.1, an error body that
declares a length of max_error_body or less is read with the response. If
it arrives within the window, the connection goes back to the pool;
otherwise Leyline closes it. A larger or unframed error body is read only
until max_error_body decoded bytes. If the rest of a fixed-length error
body is no larger than max_error_body, Leyline reads and discards it in the
background, within the pool’s idle timeout, so the connection can be reused;
this never extends the error-body window. Otherwise it closes the connection.
Over HTTP/2 and HTTP/3, the stream is reset and the connection stays open.
download uses the same limits for a
status error.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.get("https://example.com/api").await?;
if let Err(e) = resp.error_for_status_ref() {
let ray = resp.header("cf-ray").map(str::to_owned);
let body = resp.text().await?;
println!("{e}: {body} {ray:?}");
}
Ok(())
}
}
Errors covers the status error and its accessors.
Proxy, timing, and TLS
proxy() returns the proxy that carried the response, with the password, or
a user name without a password, replaced by ***. It is None for a direct
connection. Under a ProxyPool it names the proxy of the attempt that
produced the response.
Timing
timing() returns a ResponseTiming summed across redirect legs, on every
protocol. It does not include the body read.
| Field | Meaning |
|---|---|
reused | Every leg reused a pooled connection |
connect_ms | DNS, TCP, TLS, and HTTP/2 preface over the legs that opened a connection; None when every leg was warm |
send_ms | Request sent to response head |
total_ms | Connect plus send |
tls() returns a TlsInfo with the negotiated version, the cipher, and
the peer certificate as DER (peer_cert_der). audit() returns the
fingerprints the session presents when it was built with .audit(true), and
None otherwise. See Fingerprints.
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new().with_proxy("http://user:pass@proxy.example:8080");
let resp = session.get("https://example.com/").await?;
let t = resp.timing();
println!(
"{} reused={} total={}ms tls={:?}",
resp.proxy().unwrap_or("direct"),
t.reused,
t.total_ms,
resp.tls().and_then(|tls| tls.version.as_deref()),
);
Ok(())
}
}
Detect a block page
block() returns a BlockSignal when the response matches a built-in rule
for a bot challenge, a captcha, or a block page. BlockRules adds your own
rules. See Crawling.
Print a response
{:?} prints the status, version, URL, redirect chain, headers, trailers,
request headers, and timing, but not the body. URLs print with the password
and the query hidden. Authorization, Proxy-Authorization, Cookie, and
Set-Cookie values print as ***.
Streaming
Stream when a body is larger than you want in memory, or when you want to act
on the first bytes before the last ones arrive. This chapter covers request
bodies, response bodies, downloads, and flow control. Streaming needs no
feature. The examples also need bytes, futures-util, and tokio-util with
the io feature.
Stream a request body
Body::stream(s, len) takes any Stream of io::Result<Bytes> and an
Option<u64> length. Give the length whenever you know it: Leyline then
sends Content-Length on every protocol. Without it, HTTP/1.1 sends
Transfer-Encoding: chunked and HTTP/2 and HTTP/3 declare no length, which
some origins reject. Body::len_hint() reports the declared length.
#![allow(unused)]
fn main() {
use leyline::Body;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let file = tokio::fs::File::open("upload.bin").await?;
let len = tokio::fs::metadata("upload.bin").await?.len();
let body = Body::stream(tokio_util::io::ReaderStream::new(file), Some(len));
let resp = session
.post("https://example.com/upload")
.header("content-type", "application/octet-stream")
.body(body)
.await?;
println!("{}", resp.status());
Ok(())
}
}
When your stream returns an error, the request fails with Kind::Body, and
Error::io() returns your error. Leyline does not resend, because a stream
cannot be replayed. An HTTP/2 or HTTP/3 connection resets only that stream
and stays open. An HTTP/1.1 connection closes, because it holds part of the
request.
When a body cannot be replayed
A streamed body is consumed by the first attempt, so:
- Redirects. A redirect that turns the request into a GET follows
normally. A redirect that keeps the method and body, such as a 307 or 308,
fails with
Kind::Redirect. See Redirects. - Retries. The retry policy is skipped, and the first outcome is returned.
For retries or replayable redirects, buffer the body and send it as Bytes.
Stream a response body
Call .stream() on the request builder. The response arrives with the
headers, and into_stream() returns the body as a BodyStream, a
futures_util::Stream<Item = io::Result<Bytes>>.
#![allow(unused)]
fn main() {
use futures_util::StreamExt;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.get("https://example.com/big").stream().await?;
let mut body = resp.into_decoded_stream(Some(64 * 1024 * 1024))?;
let mut total = 0u64;
while let Some(chunk) = body.next().await {
total += chunk?.len() as u64;
}
println!("{total} bytes");
Ok(())
}
}
text(), bytes(), and json() also work on a .stream() response: they
drain and decode it. A streamed response carries no trailers.
Decoded or raw
| Calls | Bytes | Cap | Use it to |
|---|---|---|---|
into_decoded_stream(limit), copy_decoded_to(writer, limit) | Content coding removed | The smaller of limit and max_body_size | Save a file, parse the body |
into_stream(), copy_to(writer) | As sent, still encoded | None | Pass the body on with its content-encoding, as a proxy does |
The decoded calls decode chunk by chunk and work on a buffered response too.
limit: None means max_body_size. A body of exactly limit bytes passes. A
longer one fails with Error::is_body_limit() true, and the chunk that
crosses the limit is not written. A coding that CompressionConfig turns off
passes through still encoded. See Responses.
Download a file
RequestBuilder::download(path, limit) sends the request with .stream()
and saves the decoded body to path. It returns the number of bytes written.
Response::download_to(path, limit) does the same for a response you hold.
| Step | What happens |
|---|---|
| Status | With download, a status of 400 or more is a Kind::Status error with the status, the URL, the headers, and the start of the body, within the status-error limits. Nothing is written. download_to does not check the status; call error_for_status() first |
| Write | The decoded body goes to a temporary file .<name>.<16 hex>.part in the same directory |
| Limit | The smaller of limit and max_body_size, in decoded bytes. content_length() is the encoded size |
| Commit | Leyline flushes and syncs the file, renames it to path, and on Unix syncs the directory. When path already exists, the new file keeps its permission bits, without setuid, setgid, or sticky bits. The commit starts on a blocking thread after the file is synced. Once it has started, it finishes even if the future is dropped |
| Error or drop | Before the commit starts, Leyline removes the temporary file, and path does not exist or keeps its old content |
| Directory sync failure | path already holds the new file. The error is Kind::Io, and its message says that the target was replaced but the directory sync failed |
A path with no file name fails with Kind::Request.
Bound the whole download
For a .stream() request, total covers the request up to the response
head. Two timeouts bound the body:
readlimits the gap between two chunks. It stops a server that goes quiet. A gap past it is anio::ErrorKind::TimedOuterror from the stream.bodylimits the whole body, from its first read. It stops a server that sends slowly but never stops. A plainDurationpassed to.timeoutsets onlytotal, which ends at the head of a streamed response.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::TimeoutConfig;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
match session
.get("https://example.com/image.iso")
.timeout(
TimeoutConfig::new()
.read(Duration::from_secs(30))
.body(Duration::from_secs(600)),
)
.download("image.iso", Some(8 * 1024 * 1024 * 1024))
.await
{
Ok(n) => println!("{n} bytes"),
Err(e) if e.is_body_limit() => eprintln!("too large: {e}"),
Err(e) => return Err(e),
}
Ok(())
}
}
See Retries and timeouts. To stop a download from your code, drop the future; see Cancellation.
Stop at a marker
RequestBuilder::read_until(limit, done) sends the request, reads and decodes
the body as the chunks arrive, and stops when you tell it to. Use it when the
value you need sits near the top of a large page. It streams the response
itself, so the body is not buffered first, and it keeps the request’s
redirects, retries, authentication, host limits, and error_for_status().
After each chunk that adds decoded bytes, Leyline calls done(body, from):
body is every decoded byte so far, and from is where the new bytes start.
Return true to stop. The call returns a PrefixRead: the decoded prefix in
bytes, and why the read stopped in stopped_by.
StopReason | Meaning |
|---|---|
PredicateMatched | done returned true. A match inside the first limit bytes wins over the limit |
LimitReached | The prefix reached limit decoded bytes. The body may or may not continue; Leyline reads no further to find out |
EndOfBody | The body ended first |
LimitReached and EndOfBody mean that done found no answer, not that the
answer is “no”. A zero limit reads nothing and returns LimitReached, and
done is never called for an empty body.
#![allow(unused)]
fn main() {
use leyline::StopReason;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let marker = b"</head>";
let read = session
.get("https://example.com/big")
.read_until(256 * 1024, |body, from| {
let start = from.saturating_sub(marker.len() - 1);
body[start..].windows(marker.len()).any(|w| w == marker)
})
.await?;
match read.stopped_by {
StopReason::PredicateMatched => println!("head is {} bytes", read.bytes.len()),
other => println!("no </head> found: {other:?}"),
}
Ok(())
}
}
Scan from from minus the marker length plus one, as the example does: a
marker can straddle two chunks, and a scan of the whole body on every chunk
costs quadratic time. examples/stock_monitor.rs reports a product as in
stock, out of stock, or unknown with the same pattern.
The limit counts decoded bytes. The bytes received from the network differ: fewer for a compressed body, and more by whatever the socket had already received when the read stopped. Leyline does not stop at the exact network byte where the marker ends. When it stops early, it drops the rest of the body: over HTTP/1.1 it closes the connection, and over HTTP/2 and HTTP/3 it resets the stream and keeps the connection.
total and response_header bound the request up to the response head.
read bounds each wait for a chunk, and body the whole read. See
Retries and timeouts.
Response::read_until does the same on a response you already have and
returns only the bytes. Send that request with .stream() first; without
it, the body is already buffered, and stopping early saves nothing.
Corrupt compressed data fails with Kind::Decode, a body cut short with
Kind::Io, and an unknown or turned-off Content-Encoding is not decoded.
Back-pressure
A streamed response body reaches you through a bounded channel, so a slow reader slows the sender instead of growing a buffer.
| Protocol | Channel | When you stop reading |
|---|---|---|
| HTTP/1.1 | 16 chunks | The socket reader waits, the socket buffer fills, and TCP flow control slows the peer |
| HTTP/2 | 32 chunks | The driver keeps serving other streams. It queues the stalled stream’s data and sends no window update for it, so the queue stays within the stream’s receive window |
| HTTP/3 | 32 chunks | Leyline stops reading the QUIC stream, and QUIC flow control slows the peer |
Dropping a BodyStream before its end stops the transfer. See
Cancellation for what happens to the connection.
Cancellation
To cancel a request, drop it: the request future, the Response of a
.stream() request, or its BodyStream. Leyline then stops the request and
frees what it holds. A timeout, tokio::select!, and JoinHandle::abort all
cancel by dropping. What happens on the wire depends on the protocol and on
how far the request got.
Drop a request before the response head
A request without .stream() reads the whole body before send() returns, so
dropping its future also stops the body read.
| Protocol | Effect |
|---|---|
| HTTP/1.1 | Leyline closes the connection and does not return it to the pool. The server can already have part or all of the request. The per-host connection slot is released |
| HTTP/2 | A request that waits for a stream slot is never sent. A sent request gets RST_STREAM with CANCEL within about 100 ms. The connection stays in the pool |
| HTTP/3 | A queued request is never sent, and an open stream is cancelled within about 100 ms. The connection stays in the pool |
- A connection that Leyline is dialing for the request keeps dialing. When it is ready, the pool keeps it for the next request.
- A request that waits for a
HostLimitsslot leaves the wait, and one that holds a slot gives it back. See Crawling. - A
downloaddropped before its commit starts removes its temporary.partfile and leaves the target path as it was. The commit starts on a blocking thread after the body is written and synced. Once it has started, it renames the file over the target even if the future is dropped. - A request with a streaming body stops polling the body. On HTTP/2 and
HTTP/3, Leyline resets the stream with
CANCEL. It polls the body only after the request starts and buffers at most 256 KiB ahead of the peer’s flow-control window.
Drop a streamed response
With .stream(), send() returns at the response head. Drop the Response
or its BodyStream to stop the body. A drop is a cheap way to skip a body
you do not want.
| Protocol | Effect |
|---|---|
| HTTP/1.1 | Leyline closes the connection and does not return it to the pool, so the next request to the host opens a new one. A body read to the end returns the connection to the pool |
| HTTP/2 | Leyline resets the stream. The connection stays in the pool |
| HTTP/3 | Leyline resets the stream. The connection stays in the pool. HTTP/3 lists the reset codes |
#![allow(unused)]
fn main() {
use futures_util::StreamExt;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let resp = session.get("https://example.com/file").stream().await?;
if !resp
.header("content-type")
.is_some_and(|value| value.starts_with("text/html"))
{
return Ok(());
}
let mut body = resp.into_stream()?;
let mut seen = 0usize;
while let Some(chunk) = body.next().await {
seen += chunk?.len();
if seen > 1024 * 1024 {
break;
}
}
Ok(())
}
}
Cancel from your code
Prefer a timeout: .timeout(duration) bounds the request, and
TimeoutConfig::body bounds a streamed body. See
Retries and timeouts. To stop a
request on another event, such as a shutdown signal, use tokio::select!; the
branch that loses is dropped.
#![allow(unused)]
fn main() {
use tokio::sync::oneshot;
async fn run(shutdown: oneshot::Receiver<()>) -> leyline::Result<()> {
let session = leyline::Session::new();
tokio::select! {
result = session.get("https://example.com/feed").send() => {
println!("{}", result?.status());
}
_ = shutdown => println!("shutting down"),
}
Ok(())
}
}
To stop every request of a session and its clones at once, call
Session::shutdown(); see Sessions.
A dropped request future returns no error, because nothing polls it, and the
Trace::summary hook does not fire for it. See
Logging and tracing.
Redirects
A session follows a 301, 302, 303, 307, or 308 response that has a
Location header, up to 10 per request. This chapter covers what a redirect
changes, how to limit or stop redirects, and how to decide each one.
What a redirect changes
| Status | Next request |
|---|---|
301, 302 | A POST becomes a GET with no body. Other methods keep the method and the body |
303 | Every method except HEAD becomes a GET with no body |
307, 308 | Keeps the method and the body |
- A redirect that keeps a streamed request body fails with
Kind::Redirect, because the stream cannot be sent twice. - A
Locationwith a scheme other thanhttporhttpsfails withKind::Redirect. - When the redirect leaves the origin of the first request, the session
removes the
Authorization,Proxy-Authorization, andCookieheaders you set. The cookie jar adds the cookies that match the new URL.
A request that follows a URL the server chose, such as a pages() next link,
Tab::follow, or Tab::submit_form, drops the same credentials when its
origin differs from the page it came from: the Authorization, Cookie, and
Proxy-Authorization session defaults and the session bearer token.
Response::url is the final URL, and Response::redirect_chain lists the
URLs the session left, in order. See
Responses.
Limit or stop redirects
RedirectPolicy::limited(n) sets the limit; limited(10) is the default.
RedirectPolicy::none() follows no redirect. At the limit, the session
returns the last 3xx response, not an error. Set the policy on the session
with SessionBuilder::redirect, or on one request with
RequestBuilder::redirect. Session::execute reads a RedirectPolicy from
the request extensions, and Session::with_redirect derives a session with
another policy.
#![allow(unused)]
fn main() {
use leyline::{Browser, RedirectPolicy, Session};
async fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.redirect(RedirectPolicy::limited(3))
.build()?;
let resp = session
.post("https://example.com/login")
.redirect(RedirectPolicy::none())
.await?;
println!("{} {:?}", resp.status(), resp.header("location"));
Ok(())
}
}
Decide each redirect
RedirectPolicy::custom calls your function for each redirect with a
RedirectAttempt: the status, the current URL, the Location value as a
string, and the URLs followed so far. The URLs carry no user name or
password. Return RedirectAction::Follow or RedirectAction::Stop; on
Stop, the session returns the 3xx response. A custom policy follows at
most 32 redirects; the next one fails with Kind::Redirect.
#![allow(unused)]
fn main() {
use leyline::{Browser, RedirectAction, RedirectPolicy, Session};
fn run() -> leyline::Result<()> {
let policy = RedirectPolicy::custom(|attempt| {
let same_host = attempt
.location
.and_then(|location| attempt.url.join(location).ok())
.is_some_and(|next| next.host_str() == attempt.url.host_str());
if same_host && attempt.previous.len() < 5 {
RedirectAction::Follow
} else {
RedirectAction::Stop
}
});
let session = Session::builder()
.browser(Browser::default())
.redirect(policy)
.build()?;
let _ = session;
Ok(())
}
}
Retries and timeouts
A RetryPolicy decides whether a whole request runs again, and a
TimeoutConfig bounds how long each part of it may take. Retries are off by
default: RetryPolicy::default() is RetryPolicy::none(), so a request is
sent once unless you ask for more. Every timeout except total and connect
is off by default.
The transient policy
RetryPolicy::transient() covers the failures that usually pass on their own:
connection errors, timeouts, and status 429, 502, 503, and 504. It allows 3
retries, so 4 attempts in total, with exponential backoff from 100 ms to 1 s,
factor 2, and full jitter. A server can ask for a wait of up to 60 s;
max_retry_after changes that cap.
#![allow(unused)]
fn main() {
use leyline::RetryPolicy;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.retry(RetryPolicy::transient())
.build()?;
let resp = session
.get("https://example.com/flaky")
.retry(RetryPolicy::transient().max_retries(4))
.await?;
let _ = resp;
Ok(())
}
}
SessionBuilder::retry sets the session default. RequestBuilder::retry
replaces it for one request.
Custom triggers
RetryTrigger has four variants:
| Trigger | Matches |
|---|---|
Status(u16) | A response with that status |
ServerError | A response with any 5xx status |
Timeout | An error for which Error::is_timeout() is true |
ConnectionError | Any other error for which Error::is_retryable() is true: a failed connect, a reset, aborted, or closed connection, an I/O error on the way to a proxy, and a proxy answer of 502, 503, or 504 to CONNECT |
A CONNECT answer ends the request with an error, not a response, so only
ConnectionError matches it. Other CONNECT answers, such as 407, and a
SOCKS5 reply from 3 to 6, are not retried. No other error is retried.
Start from none() or transient(). retry_on(triggers) replaces the
trigger set and on_status(code) adds one status. Every other setting has a
setter named after it: max_retries, initial_backoff, max_backoff,
backoff_factor, jitter, max_retry_after, and allow_non_idempotent.
#![allow(unused)]
fn main() {
use leyline::{RetryPolicy, RetryTrigger};
use std::time::Duration;
let policy = RetryPolicy::none()
.max_retries(5)
.initial_backoff(Duration::from_millis(50))
.max_backoff(Duration::from_secs(2))
.backoff_factor(1.5)
.jitter(true)
.retry_on([RetryTrigger::ServerError, RetryTrigger::ConnectionError]);
let also = RetryPolicy::transient().on_status(408);
let _ = (policy, also);
}
Backoff for attempt n is initial_backoff * backoff_factor.powi(n), capped at
max_backoff. With jitter set, the result is multiplied by a uniform random
factor between 0 and 1 (full jitter). RetryPolicy::backoff(attempt) returns
that value, so a retry loop of your own can wait the same way. With jitter
on, each call draws a new factor.
#![allow(unused)]
fn main() {
use leyline::RetryPolicy;
use std::time::Duration;
let policy = RetryPolicy::transient().jitter(false);
assert!(policy.backoff(1) > policy.backoff(0));
assert_eq!(policy.backoff(10), Duration::from_secs(1));
}
Retry-After wins
When a retried response carries Retry-After, its value replaces the
computed backoff. Leyline reads delta-seconds and an IMF-fixdate with a GMT
or UTC zone. An unparsable value falls back to the computed backoff. A
header set with wait_header
wins over Retry-After.
max_retry_after caps the wait. RetryPolicy::transient() sets it to 60 s,
and RetryPolicy::none() has no cap. When the server asks for longer than
the cap, or a wait or backoff is longer than the time left before the total
timeout, Leyline stops and returns the response, or the last error, as when
retries run out. Error::retries_exhausted() is then true.
A Retry-After on a status that no trigger or retry_if matches is not
acted on. Read it after error_for_status() with Error::retry_after():
#![allow(unused)]
fn main() {
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
match session.get("https://api.example/jobs").error_for_status().await {
Ok(resp) => println!("{}", resp.status()),
Err(err) => match err.retry_after() {
Some(wait) => println!("come back in {wait:?}"),
None => return Err(err),
},
}
Ok(())
}
}
See Status codes are not errors.
Retry on a condition and wait on a header
retry_if(predicate) adds a test on the response head: the status and the
headers. A response is retried when a trigger matches its status or when any
predicate returns true.
wait_header(name, format) names a header that says how long to wait.
WaitFormat says how to read it:
WaitFormat | Value | Example |
|---|---|---|
Seconds | Seconds to wait, whole or fractional | 30 |
UnixSeconds | The Unix time to wait until. A past time means no wait | 1767225600 |
HttpDate | The time to wait until, as an IMF-fixdate | Thu, 01 Jan 2026 00:00:00 GMT |
Leyline reads the wait_header headers in the order you added them, then
Retry-After, and uses the first value that parses. max_retry_after caps
these waits too. They apply to every retried response, whichever trigger or
predicate matched it. A retry after an error has no response, so it uses the
computed backoff.
This policy retries a 403 that reports a used-up quota, and only that one, after the reset time:
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::{RetryPolicy, WaitFormat};
use leyline::http::StatusCode;
const REMAINING: &str = "x-ratelimit-remaining";
const RESET: &str = "x-ratelimit-reset";
async fn run() -> leyline::Result<()> {
let policy = RetryPolicy::transient()
.retry_if(|resp| {
resp.status() == StatusCode::FORBIDDEN && resp.header(REMAINING) == Some("0")
})
.wait_header(RESET, WaitFormat::UnixSeconds)
.max_retry_after(Duration::from_secs(120));
let session = leyline::Session::builder().retry(policy).build()?;
let resp = session.get("https://api.example/repos").error_for_status().await?;
let _ = resp;
Ok(())
}
}
A policy with max_retries(0), such as none(), never retries, so start
from transient() or set max_retries.
Blocks, skip_blocks, and retries through other proxies are covered in
Crawling.
The idempotency rule
Leyline retries only idempotent methods by default: GET, HEAD, OPTIONS, PUT, DELETE, and TRACE (RFC 9110, section 9.2.2). A POST or PATCH is sent once. A retry also needs a body that can be sent again. A streaming request body is retried only when no byte of it was read, for example after a connect error. See Streaming.
retry_unsent(true) retries any method when the error happened before the
request was sent: a DNS, connect, TLS, or proxy error, a connect timeout, an
HTTP/2 REFUSED_STREAM reset, or an HTTP/3 request that the server reports
it did not process. An HTTP/3 request that the server rejects twice is
retried on the same connection. The server never processed the request, so a retry cannot
repeat a write. The error
must still match a trigger. After a redirect got a response, an error on a
later leg does not count: the server may already have processed the
original request, so retry_unsent does not replay it. A 401 digest
challenge is not an answer, so an error on the authorized leg still counts.
#![allow(unused)]
fn main() {
use leyline::RetryPolicy;
fn run() -> leyline::Result<()> {
let policy = RetryPolicy::transient().retry_unsent(true);
let session = leyline::Session::builder().retry(policy).build()?;
drop(session);
Ok(())
}
}
allow_non_idempotent(true) retries every failure of any method. Use it only
when the endpoint is safe to repeat.
Count the attempts
Response::attempts() returns the number of attempts that produced the
response: 1 without retries. When retries run out on a retryable status, the
response carries the full count. Error::attempts() and
trace::Summary::attempts carry the same count. See
Logging and tracing.
Retries and streamed responses
A .stream() request uses the same policy, and the decision is made on the
response head. After send() returns, a failure while you read the body is
not retried. To try again, send a new request.
Resends outside the policy
Connection setup has no retry loop of its own. These cases send a request or
connect again without asking the policy. Each happens at most once for one
attempt, and the result then goes to RetryPolicy like any other:
- HTTP/1.1: a pooled keep-alive connection fails before the response. An idempotent request with a buffered or empty body goes once more on a new connection.
- HTTP/2: a pooled connection fails before the response. A request with a
buffered or empty body goes once more on a new connection when the method
is idempotent, or for any method after a
REFUSED_STREAMreset. - HTTP/3: a pooled connection reports that it did not send the request, for
example after
GOAWAY. A request with a buffered or empty body goes once more on a new connection, for any method. After anH3_REQUEST_REJECTEDreset before the response head, a buffered request goes once more on the same connection. ProtocolPolicy::Race: when both the HTTP/3 and the HTTP/2 connect fail, Leyline falls back toAutoand connects again.
ALPN needs no second send. See One handshake per connection.
The timeouts
TimeoutConfig holds every timeout. Each setter takes a Duration or
None, and None turns that timeout off.
| Setter | Default | Covers |
|---|---|---|
total | 300 s | One send: every redirect, retry, backoff sleep, and buffered body read. For a .stream() response it ends at the response head. Expiry gives Kind::Timeout |
connect | 10 s | One new connection: DNS, TCP connect, proxy negotiation, and TLS, or on HTTP/3 the DNS lookup, UDP setup, and QUIC handshake. Each new connection gets a full window, and total bounds the sum. Pooled reuse is not covered |
response_header | none | From dispatch to the transport response, per redirect, connection setup included. On a buffered response the body arrives inside this window |
read | none | The longest gap between two body chunks of a .stream() response, or of a body that the transport delivers in pieces. It does not apply to a body that the transport buffers; response_header and total bound that body |
body | none | The whole body of a .stream() response, or of a body that the transport delivers in pieces, counted from the first read of the body. It does not apply to a body that the transport buffers; response_header and total bound that body |
error_body | 10 s | The body that RequestBuilder::error_for_status() keeps in a status error. total also bounds it. See Responses |
#![allow(unused)]
fn main() {
use leyline::{Session, TimeoutConfig};
use std::time::Duration;
fn run() -> leyline::Result<()> {
let session = Session::builder()
.timeout(
TimeoutConfig::new()
.total(Duration::from_secs(60))
.connect(Duration::from_secs(5))
.read(Duration::from_secs(15))
.response_header(Duration::from_secs(20))
.body(Duration::from_secs(120)),
)
.build()?;
let _ = session;
Ok(())
}
}
SessionBuilder::timeout takes a Duration or a TimeoutConfig. A
Duration sets total, and the other settings keep their
defaults. A later timeout call replaces the whole configuration, so to
change connect alone, pass TimeoutConfig::new().connect(duration).
Every timeout gives an error whose is_timeout() is true. The kind depends
on which one elapsed; see Fine-grained kinds.
Per-request timeouts
RequestBuilder::timeout merges over the session timeouts, setting by
setting. A setting that the request sets wins, None included, and the rest
keep the session value. A Duration sets total. connect stays
session-wide, because connections are pooled across requests.
#![allow(unused)]
fn main() {
use leyline::TimeoutConfig;
use std::time::Duration;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let quick = session
.get("https://example.com/slow")
.timeout(Duration::from_secs(5))
.await?;
let streamed = session
.get("https://example.com/feed")
.stream()
.timeout(
TimeoutConfig::new()
.total(None)
.read(Duration::from_secs(10))
.response_header(Duration::from_secs(15)),
)
.await?;
let _ = (quick, streamed);
Ok(())
}
}
Bound a whole download
TimeoutConfig::body bounds the whole body from its first read, so it bounds a
.stream() body and RequestBuilder::download, which streams. A plain
Duration passed to .timeout sets only total, which ends at the head of
a streamed response, so set body for a download. No outer timeout is
needed.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::TimeoutConfig;
async fn run(session: leyline::Session) -> leyline::Result<()> {
let written = session
.get("https://example.com/archive.zip")
.timeout(TimeoutConfig::new().body(Duration::from_secs(600)))
.download("archive.zip", None)
.await;
match written {
Ok(bytes) => println!("{bytes} bytes"),
Err(err) if err.is_timeout() => println!("download took too long"),
Err(err) => return Err(err),
}
Ok(())
}
}
A failed or cancelled download removes its temporary .part file and leaves
the target path as it was. See Streaming and
Cancellation.
Next
Read Errors to match on ErrorCategory and Kind.
Errors
Every call returns leyline::Result<T>, and every failure is a
leyline::Error. Start with err.category(), which sorts each error into one
ErrorCategory. Use err.kind() only for the finer detail of
Fine-grained kinds.
leyline::Error implements std::error::Error + Send + Sync + 'static, so
? converts it into Box<dyn std::error::Error + Send + Sync> or
anyhow::Error. It also converts from url::ParseError, as Kind::Url.
Group errors by category
err.category() picks the first group that applies, in this order:
| Order | Test | Category |
|---|---|---|
| 1 | is_timeout() | Timeout, a connect timeout included |
| 2 | is_status() | Status |
| 3 | is_body_limit() | BodyLimit |
| 4 | The TLS-layer cause in err.tls() | Dns, Connect (TCP connect), Proxy, ProxyTarget, Tls (handshake, certificate, host name, pin), or Config (TLS setup) |
| 5 | err.kind() | See Fine-grained kinds |
ErrorCategory is #[non_exhaustive], so a match needs a _ arm.
#![allow(unused)]
fn main() {
use leyline::ErrorCategory;
async fn run() {
let session = leyline::Session::new();
if let Err(err) = session.get("https://api.example/").await {
match err.category() {
ErrorCategory::Timeout | ErrorCategory::Dns | ErrorCategory::Connect => {
eprintln!("network: {err}")
}
ErrorCategory::Proxy | ErrorCategory::ProxyTarget => eprintln!("proxy: {err}"),
ErrorCategory::Url | ErrorCategory::Config | ErrorCategory::Request => {
eprintln!("fix the request: {err}")
}
_ => eprintln!("request failed: {err}"),
}
}
}
}
Config: a value or a combination of settings cannot work, for example a forced HTTP/3 policy with a proxy that cannot carry HTTP/3, or a browser with no identity for the platform.SessionBuilder::build()also returns it when a proxy environment variable is not a valid proxy URL.Connect: the TCP connect failed, or the socket was refused or unreachable. The connect window also covers proxy negotiation, the TLS handshake, and on HTTP/3 the QUIC handshake.Dns: name resolution failed, andis_dns()is true.Proxy,ProxyTarget: the proxy failed, or it could not reach the origin. See Proxy errors.Tls: the TLS handshake or the certificate check failed. A handshake failure can pass on the next try, sois_connect()andis_retryable()are true for it. A certificate, host name, or pin failure stays until the server or your trust settings change, so both are false.Decode: a failed content decode, or aResponse::jsonserde failure (Kind::Json).
ErrorCategory::as_str() returns a stable snake_case label, such as
"timeout", "proxy_target", or "body_limit", for logs, metrics, and
client-facing error bodies. Display prints the same label.
Display of an Error prints the kind, status, message, and URL once. Walk
std::error::Error::source() for the cause.
Attempts and the proxy used
err.attempts() returns the number of attempts, retries included, and 0
for an error that no request produced, such as a build() failure.
err.proxy() returns the proxy URL of the last attempt, with the password
redacted, or None for a direct connection. Both are set for status errors
too.
#![allow(unused)]
fn main() {
async fn run() {
let session = leyline::Session::new();
if let Err(err) = session.get("https://api.example/").error_for_status().await {
eprintln!(
"{} after {} attempts via {}: {err}",
err.category().as_str(),
err.attempts(),
err.proxy().unwrap_or("direct"),
);
}
}
}
Status codes are not errors
A 4xx or 5xx response is Ok. To get a status of 400 or more as an
error, call error_for_status() on the request builder. A failed send and a
bad status then arrive as one Result. The check runs after retries, so it
sees the last response, and the error keeps the start of the decoded body,
within the limits in
Turn a status into an error.
| Method on the error | Returns |
|---|---|
status() | Option<StatusCode>, the response status |
url() | Option<&Url>, the final URL, after redirects |
headers() | Option<&HeaderMap>, the response headers |
header(name) | Option<&str>, one header as text. None when it is missing or not ASCII |
body() | Option<&[u8]>, the start of the body, or None when the read failed |
body_text() | Option<Cow<str>>, the kept body as text, with invalid UTF-8 replaced |
retry_after() | Option<Duration>, the wait before the next try |
retries_exhausted() | bool, true when the retry policy wanted another try and did not make it |
retry_after() returns the wait that the retry policy read, each
wait_header first and then Retry-After, when the error comes from the
request-builder error_for_status(). Otherwise it parses Retry-After, in
seconds or as an HTTP date. It is None for an error that is not a status
error. See Retries and timeouts.
retries_exhausted() is true when the last response still matched a retry
trigger and the policy stopped: no retries were left, the server asked for a
wait longer than max_retry_after, or the wait or backoff was longer than the
time left before the total timeout.
Only a Kind::Status error carries a URL and headers. Display and Debug
show the URL with the password and the query hidden.
#![allow(unused)]
fn main() {
use leyline::http::StatusCode;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
match session.get("https://api.example/users/7").error_for_status().await {
Ok(resp) => println!("found: {}", resp.text().await?),
Err(e) if e.status() == Some(StatusCode::NOT_FOUND) => println!("no such user"),
Err(e) => return Err(e),
}
Ok(())
}
}
Turn a status into an error
For a response you already hold, Response::error_for_status() consumes the
response and error_for_status_ref() borrows it. Both errors keep the
status, URL, and headers, and no body. See
Responses.
Predicates
The predicates look at the kind and at the source error:
| Predicate | True for |
|---|---|
is_timeout() | Kind::Timeout, and I/O or TLS errors with TimedOut |
is_connect() | An error in category Connect or Dns, a TLS handshake failure, or a timeout while connecting. It agrees with category(): a TLS certificate, host name, or pin failure is false |
is_dns() | Name resolution failed. A session with ProtocolPolicy::Http3 reports a DNS failure as a connect failure |
is_status() | Kind::Status |
is_body_limit() | A response body passed max_body_size or a limit you gave. The message names the limit. False for a body already taken, a failed request body stream, and corrupt compressed data |
is_proxy() | The proxy itself failed, before the tunnel was up. See Decide whether to ban a proxy |
is_retryable() | is_timeout(), is_connect(), a reset, aborted, or closed connection, an I/O failure while dialing or talking to a proxy, or a proxy answer of 502, 503, or 504 to CONNECT |
is_shut_down() | The session was shut down with Session::shutdown() |
A RetryPolicy uses is_timeout() and is_retryable() to pick its
triggers. See Retries and timeouts.
Map errors to HTTP statuses
A service that calls an upstream through Leyline must answer its own client.
ErrorCategory::gateway_status() gives a default answer:
| Category | gateway_status() | Reason |
|---|---|---|
Status | None | Pass the upstream status through, from err.status() |
Timeout | 504 Gateway Timeout | The upstream did not answer in time |
Url, Config, Request | 500 Internal Server Error | The service built a bad request |
| Every other category | 502 Bad Gateway | The upstream or the proxy failed |
#![allow(unused)]
fn main() {
use leyline::http::StatusCode;
fn to_status(err: &leyline::Error) -> StatusCode {
err.category()
.gateway_status()
.or_else(|| err.status())
.unwrap_or(StatusCode::BAD_GATEWAY)
}
let _ = to_status;
}
BodyLimit maps to 502, not to 413: the upstream response passed the limit,
not the client’s request. Log Proxy and ProxyTarget apart from upstream
failures, because they point at your proxy. See
Service integration for a full service.
Fine-grained kinds
err.kind() returns a Kind, the layer that failed. The
API map lists each Kind. Kind is #[non_exhaustive],
and Kind::as_str() returns a stable lowercase label.
Because category() tests the timeout, the status, the body limit, and the
TLS-layer cause first, one kind can give more than one category:
| Kind | Category | Other category when |
|---|---|---|
Timeout | Timeout | |
Connect | Connect | Timeout for a connect timeout, Dns for a failed name resolution |
Tls | Tls | Config for a TLS setup or trust store failure |
Proxy | Proxy | ProxyTarget when the proxy could not reach the origin |
Status | Status | |
Body | Body | BodyLimit for a body over a limit |
Decode, Json | Decode | |
Http2, Http3 | Protocol | |
Redirect | Redirect | |
Url | Url | |
Config | Config | |
Request | Request | |
Io | Other | Connect for a refused or unreachable socket, BodyLimit for a body over a limit |
Any kind whose I/O cause is TimedOut gives Timeout. A timeout has more
than one kind:
- A
connecttimeout isKind::Connect, on HTTP/1.1, HTTP/2, and HTTP/3. - A
totalorresponse_headertimeout isKind::Timeout. - A
readstall on a.stream()body isKind::Timeoutfrombytes(),text(), andjson(), andKind::Iofromcopy_toandread_until.
So when you match on the kind, test is_timeout() first:
#![allow(unused)]
fn main() {
use leyline::{Kind, Session};
async fn run() {
let session = Session::new();
match session.get("https://example.com/").await {
Ok(resp) => println!("{}", resp.status()),
Err(err) if err.is_timeout() => eprintln!("timed out: {err}"),
Err(err) => match err.kind() {
Kind::Connect | Kind::Tls | Kind::Proxy => eprintln!("no connection: {err}"),
Kind::Config | Kind::Url => eprintln!("fix the request: {err}"),
_ => eprintln!("request failed: {err}"),
},
}
}
}
Read the source error
err.tls() returns the TlsError, err.h2() the H2Error, and err.io()
the std::io::Error, when that is the source. Match TlsError::Rejected for
a peer that closed or reset the handshake, and
TlsError::Certificate { verify_code, reason, .. } for a failed certificate
check. TlsError::Proxy and TlsError::ProxyTargetUnreachable are described
in Proxy errors.
Error::find(error) walks an error’s source() chain and returns the first
leyline::Error in it, so an error that a tower layer or your own type wraps
still maps through category().
Next
Read Proxies to set proxy rules, bypass lists, and environment discovery.
Proxies
Leyline tunnels through http://, https://, socks5://, and socks5h://
proxies. This page sets one proxy, rules per scheme, bypass lists, and
environment discovery, then rotates proxies with a retry list or a
ProxyPool, and tells a proxy failure from an origin failure.
An http:// proxy receives the CONNECT request and any
Proxy-Authorization credentials in cleartext; an https:// proxy receives
them inside TLS. A plain http:// URL goes to an http:// or https://
proxy as an absolute-form request, and through a SOCKS5 proxy over a
CONNECT tunnel.
Set one proxy
Every proxy setter takes impl Into<ProxyConfig>: a URL string, a
ProxyUrl, or a ProxyConfig. That covers SessionBuilder::proxy,
RequestBuilder::proxy, WebSocketBuilder::proxy, and Session::with_proxy.
A URL applies to every scheme.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.proxy("http://user:pass@proxy.example:8080")
.build()?;
let _ = session;
Ok(())
}
}
build() validates the session proxy. A URL string given to a request, a
WebSocket, or with_proxy is validated when a request picks it, and an
invalid one fails that send() with Kind::Config. To fail early, pass a
ProxyUrl: ProxyUrl::parse refuses any other scheme and a URL with no host.
#![allow(unused)]
fn main() {
use leyline::ProxyUrl;
assert!(ProxyUrl::parse("http://proxy.example:8080").is_ok());
assert!(ProxyUrl::parse("ftp://proxy.example").is_err());
}
Debug of ProxyUrl, ProxyRule, and Session, and Display of
ProxyUrl, replace the password with ***. ProxyUrl implements
Serialize and Deserialize, and serde keeps the full URL, password
included, so protect a file that holds one.
SOCKS5 and HTTPS proxies
A socks5:// or socks5h:// proxy needs the socks feature, which is off by
default. With either scheme the proxy resolves the host name.
[dependencies]
leyline-http = { version = "0.1", features = ["socks"] }
Without the feature, ProxyUrl::parse and build() refuse a SOCKS URL with
Kind::Config, whether it is the session proxy, a ProxyPool entry, or the
proxy from the environment. A SOCKS URL string given to a request or to
with_proxy fails the first request that uses it. The message names the
feature.
An https:// proxy means TLS to the proxy, then CONNECT through it. A
session with certificate pins or a client certificate cannot use one, because
the origin TLS identity must not go to the proxy: build() accepts the
combination, and every request routed through the https:// proxy fails with
Kind::Proxy. See TLS trust.
Rules per scheme
ProxyConfig holds a list of ProxyRules. A rule applies to every scheme
(ProxyRule::all), to http only, or to https only. The first matching
rule wins, in the order you added them. A wss:// WebSocket URL matches as
https://; a ws:// URL fails with Kind::Request before any proxy is
chosen. See WebSocket.
#![allow(unused)]
fn main() {
use leyline::{ProxyConfig, ProxyRule, Session};
fn run() -> leyline::Result<()> {
let proxies = ProxyConfig::new()
.rule(ProxyRule::https("http://secure-proxy.example:8080"))
.rule(ProxyRule::http("http://plain-proxy.example:3128"));
let session = Session::builder().proxy(proxies).build()?;
let _ = session;
Ok(())
}
}
Bypass with NO_PROXY
NoProxy::new(patterns) matches hosts that must not go through a proxy.
#![allow(unused)]
fn main() {
use leyline::{NoProxy, ProxyConfig, ProxyRule};
let bypass = NoProxy::new(["localhost", ".internal.example", "192.0.2.1"]);
let proxies = ProxyConfig::new()
.rule(ProxyRule::all("http://proxy.example:8080"))
.no_proxy(bypass);
let _ = proxies;
}
A pattern matches the host and every subdomain of it. A leading dot is
optional, a trailing :port is stripped, and * matches every host.
Matching ignores case and a trailing dot on the host.
The bypass list applies when you set it with ProxyConfig::no_proxy, and
when the session proxy came from the environment. A per-request proxy()
replaces the session config, bypass list included.
Environment discovery
A session with no explicit proxy reads the environment once, when it is
built. It takes the first non-empty value of HTTPS_PROXY, https_proxy,
HTTP_PROXY, http_proxy, ALL_PROXY, all_proxy, and applies it to every
URL scheme. NO_PROXY or no_proxy seeds the bypass list.
A value that is not a valid proxy URL never falls back to a direct
connection. A valid value has a scheme of http, https, socks5, or
socks5h (the last two only with the socks feature) and a host, so
proxy.example:8080 is invalid. Leyline checks only the value it picked and
does not try a lower variable. The error names the variable, never the value.
SessionBuilder::build()returnsKind::Config.Session::new()andSession::browser(..)return a session in which every request fails withKind::Proxy, hosts inNO_PROXYincluded.
Fix or unset the variable, set a proxy with SessionBuilder::proxy, or turn
discovery off. A proxy set on a request, a WebSocket, or with with_proxy
replaces the failed state too.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.proxy(leyline::ProxyConfig::new().env(false))
.build()?;
let _ = session;
Ok(())
}
}
Uppercase HTTP_PROXY is skipped when the process looks like a CGI handler
(the httpoxy mitigation): when any of GATEWAY_INTERFACE, REQUEST_METHOD,
SERVER_SOFTWARE, SCRIPT_NAME, SCRIPT_FILENAME, PATH_INFO,
QUERY_STRING, SERVER_PROTOCOL, SERVER_NAME, or SERVER_PORT is set.
Leyline then logs a warning on the leyline::env_proxy::cgi target.
Per-request override
RequestBuilder::proxy replaces the session proxy config for one request. A
ProxyConfig::new() with no rule sends that request direct.
Session::with_proxy derives a session that differs only in its proxy. It
shares the cookie jar and the connection pool, and keeps the TLS context and
the identity. See Sessions.
The pool is keyed by host, port, and proxy, so two proxies never share a
connection, and a return to a proxy reuses its warm connections.
Session::fresh_pool derives a session with a new, empty pool and TLS session
cache, so the next request opens a new socket through the same proxy.
#![allow(unused)]
fn main() {
async fn run(proxy: Option<&str>) -> leyline::Result<()> {
let session = leyline::Session::new();
let mut request = session.get("https://example.com/ip");
if let Some(p) = proxy {
request = request.proxy(p);
}
let resp = request.await?;
let rotated = session.with_proxy("http://gateway.example:8080").fresh_pool();
let _ = (resp, rotated);
Ok(())
}
}
Rotate proxies on retry
RetryPolicy::rotate_proxies(list) moves each retry to the next proxy in a
list. The first attempt uses the request’s or the session’s proxy. Retry k,
counted from 1, uses list[(k - 1) % list.len()]. On a session with a
ProxyPool, the pool picks the proxy and rotate_proxies is ignored.
#![allow(unused)]
fn main() {
use leyline::RetryPolicy;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let policy = RetryPolicy::transient()
.on_status(403)
.rotate_proxies(["http://proxy-a.example:8080", "http://proxy-b.example:8080"]);
let resp = session
.get("https://shop.example/item/1")
.retry(policy)
.send()
.await?;
let _ = resp;
Ok(())
}
}
The usual retry rules apply: a trigger must match, the method must be
idempotent or allowed, and the body must be replayable. A Retry-After wait
still runs before the next proxy. When retries run out you get the last
response, for example the 403. See
Retries and timeouts.
Use a proxy pool
For bans and stickiness across many requests, give the session a ProxyPool
with SessionBuilder::proxy_pool. The pool picks a proxy for each request:
- The sticky proxy of the origin, if it is not banned and was picked within
sticky_for. - Else the next healthy proxy, round-robin.
- If every proxy is banned, the proxy whose ban ends first.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::{BlockRules, ProxyPool, Session};
fn run() -> leyline::Result<()> {
let pool = ProxyPool::new([
"http://user:pass@proxy-a.example:8080",
"http://user:pass@proxy-b.example:8080",
])
.sticky_for(Duration::from_secs(300))
.ban_after(3)
.ban_for(Duration::from_secs(60))
.rotate_on_block(BlockRules::statuses([403, 429]));
let session = Session::builder().proxy_pool(pool.clone()).build()?;
let _ = session;
Ok(())
}
}
| Setter | Default | Effect |
|---|---|---|
sticky_for(d) | zero, no stickiness | Keep an origin on the same proxy for d |
ban_after(n) | 3 | Strikes before a ban. The smallest value is 1 |
ban_for(d) | 60 s | Length of a ban |
rotate_on_block(rules) | none | A response the rules report as a block is a strike |
| Outcome of a request | Effect on its proxy |
|---|---|
An error with is_proxy() | A strike. The origin loses its sticky proxy |
A block under rotate_on_block | A strike. The origin loses its sticky proxy |
| Any other response | The strikes reset |
Any other error, ProxyTarget included | No change |
After ban_after strikes, the proxy is banned for ban_for, and every origin
that stuck to it loses its sticky proxy. Each retry picks the next healthy
proxy and skips the one that just failed. A request with its own .proxy(..)
does not use the pool. Blocks and BlockRules are described in
Crawling.
ProxyPool is Clone, and clones share the health state. stats() returns
one ProxyHealth per proxy, with proxy (no password), in_use,
failures, and banned_until, which can be in the past when the ban has
ended.
#![allow(unused)]
fn main() {
use std::time::Instant;
fn run(proxies: leyline::ProxyPool) {
let now = Instant::now();
for health in proxies.stats() {
let banned = health.banned_until.is_some_and(|until| until > now);
println!(
"{} in_use={} failures={} banned={banned}",
health.proxy, health.in_use, health.failures
);
}
}
}
Pin one identity to each proxy
ProxyPool::identified takes (proxy, Identity) pairs. A request sent
through a proxy uses a session with that proxy’s identity, so one proxy IP
never shows two browsers.
#![allow(unused)]
fn main() {
use leyline::{Browser, Family, Identity, Platform, ProxyPool, Session};
fn run() -> leyline::Result<()> {
let proxies = (1..=20).map(|n| format!("http://user:pass@proxy-{n}.example:8080"));
let identities = [
Identity::locked(Browser::latest(Family::Chrome), Platform::Windows),
Identity::locked(Browser::latest(Family::Firefox), Platform::MacOS),
];
let pool = ProxyPool::identified(proxies.zip(identities.into_iter().cycle()));
let session = Session::builder()
.browser(Browser::default())
.proxy_pool(pool)
.build()?;
let _ = session;
Ok(())
}
}
On first use of a proxy, the pool derives a session for it from the request’s
session with Session::with_identity, and keeps it. That session shares the
connection pool (in its own partition) and the host limits, and has its own
TLS session cache. Unlike a plain with_identity session, it gets its own
cookie jar, which starts empty. Each proxy has its own jar, also when two
proxies share an Identity, so cookies set through one proxy never go out
through another. When an origin moves to another proxy, its cookies stay in
the old proxy’s jar.
The session must impersonate a browser. A session with ProxyPool::identified
and no browser, profile, or identity fails at build() with Kind::Config.
Proxy errors
A failure in the proxy dial, the proxy handshake, or CONNECT is
Kind::Proxy, and err.tls() returns TlsError::Proxy, whose status
holds the proxy’s answer to CONNECT, for example 407.
When the proxy works but cannot reach the origin, the error is also
Kind::Proxy, its category is ProxyTarget, and err.tls() returns
TlsError::ProxyTargetUnreachable { reply, detail }. reply is a
ProxyReply: HttpStatus(502) or HttpStatus(504) for a CONNECT answer,
or Socks5(n) for a SOCKS5 reply from 3 to 6. detail holds the text.
RetryTrigger::ConnectionError retries an I/O failure on the way to the proxy
and a CONNECT answer of 502, 503, or 504, and nothing else here. See
Custom triggers.
Decide whether to ban a proxy
Error::is_proxy() is true when the proxy itself failed, before the tunnel
was up:
- The proxy dial was refused or timed out. A timeout is also
is_timeout(). - The proxy answered
CONNECTwith an error such as 407, 403, or 503. - The SOCKS5 handshake or authentication failed.
- The TLS handshake with an
https://proxy failed.
It is false for ProxyTargetUnreachable and for every error after the tunnel
is up, so it takes a proxy out of rotation without blaming it for the origin.
A ProxyPool applies this rule for you.
#![allow(unused)]
fn main() {
use leyline::TlsError;
async fn run(session: leyline::Session, proxy: &str) -> leyline::Result<()> {
match session.get("https://example.com/").proxy(proxy).send().await {
Ok(resp) => println!("{}", resp.status()),
Err(err) if err.is_proxy() => println!("ban {proxy}: {err}"),
Err(err) => match err.tls() {
Some(TlsError::ProxyTargetUnreachable { reply, .. }) => {
println!("origin unreachable through the proxy: {reply:?}")
}
_ => println!("origin error: {err}"),
},
}
Ok(())
}
}
HTTP/3 through a SOCKS5 proxy
With the socks feature, HTTP/3 runs over a SOCKS5 proxy. Leyline opens a
TCP control connection, authenticates, and sends UDP ASSOCIATE (RFC 1928,
section 7). Each QUIC datagram goes to the relay with the SOCKS5 UDP header,
which names the target host, so the proxy resolves it. When the control
connection closes, the QUIC connection fails and leaves the pool. The connect
timeout covers the control connection, UDP ASSOCIATE, and the QUIC
handshake.
An http:// or https:// proxy cannot carry HTTP/3, and neither can a SOCKS
proxy without the socks feature. build() rejects ProtocolPolicy::Http3
when such a proxy takes every URL, and an Http3 request fails with
Kind::Config when the rules pick such a proxy for its URL. Under
ProtocolPolicy::Race, a request through such a proxy takes the Auto path.
MASQUE (CONNECT-UDP, RFC 9298) is not supported.
Many proxies do not support UDP ASSOCIATE. When an HTTP/3 connection
through a proxy fails, Race skips HTTP/3 for that origin and proxy for 5
minutes. To keep every request on TCP, set ProtocolPolicy::Auto. See
HTTP/3.
Next
Read Cookies.
Crawling
Session::crawl crawls a site for you: it follows links in scope, obeys each
page’s own rules, drops copies, and reads robots.txt when you ask it to. The rest of this page
covers the session settings every crawl uses: per-host limits, a proxy pool,
retries, block detection, capped downloads, one log line per request, and
counters for the whole crawl. Every task that shares the session shares the
limits and the proxy health.
Crawl a site
session.crawl(seed) returns a Crawl, a lazy stream of CrawlPages. The
defaults are polite, so the short form is a complete crawler:
#![allow(unused)]
fn main() {
use leyline::Session;
async fn run() -> leyline::Result<()> {
let mut crawl = Session::new().crawl("https://docs.example/");
while let Some(page) = crawl.next().await {
let page = page?;
println!("{} {} words", page.url(), page.text().split_whitespace().count());
}
Ok(())
}
}
That crawl stays on the seed’s host, sends one request at a time to each host
and at most one a second, pauses a host that answers 429 or 503 and tries
that page up to three times, reads the
site’s sitemaps, follows redirects through the same checks as links, skips
error pages, files, and block pages, and drops copies. It obeys robots.txt
and each page’s robots rules unless you call ignore_robots(). Five
settings change the rest:
#![allow(unused)]
fn main() {
use leyline::{HostLimits, Respect, Scope, Session};
async fn run() -> leyline::Result<()> {
let mut crawl = Session::new()
.crawl("https://docs.example/guide/")
.respect(Respect::AiTraining)
.scope(Scope::Prefix)
.limit(500)
.host_limits(HostLimits::new().max_in_flight(2).per_second(2.0));
while let Some(page) = crawl.next().await {
match page {
Ok(page) => println!("{}", page.url()),
Err(err) => eprintln!("{}: {}", err.url().map_or("-", |url| url.as_str()), err.category()),
}
}
println!("{}", crawl.stats());
Ok(())
}
}
| Setter | Default | Effect |
|---|---|---|
respect(Respect) | Robots | Robots obeys robots.txt Disallow and Crawl-delay, the page’s noindex and nofollow, and rel="nofollow". AiTraining does all of that and also obeys the site’s AI-training opt-outs |
ignore_robots() | off | Obeys no robots rules: no Disallow, Crawl-delay, noindex, or nofollow |
scope(Scope) | Host | Host: the seed’s host. Domain: its registrable domain. Prefix: its host and directory; a last path segment with a dot and a letter after it is a file, so end a directory seed with a slash |
limit(n) | none | Stops after n kept pages |
host_limits(HostLimits) | one in flight, one a second, and a 429/503 pause per host, wherever the session’s own limits leave them unset | The crawl’s pace per host, on top of the session’s own limits, which every crawl and request of the session share. The value is used as given; HostLimits::new() adds no limits |
Unless you call ignore_robots(), the crawl reads each site’s robots.txt
once, and again after 24 hours, obeys its rules, and reads its Sitemap
lines. Robots groups are
matched for the * agent. To read the group for a named crawler, call
Session::robots(url, agent). At most 16 requests are in flight across all
hosts.
For each URL, in order, the crawl:
- Skips a URL that is not
httporhttps, is out of scope, or was queued before. URLs are compared after the fragment is dropped, the query parameters are sorted, and percent-encoding is normalized; the request goes out as written. - Unless robots are ignored, skips a URL that robots.txt disallows, and uses the site’s
Crawl-delayas the least spacing between its requests, up to 30 seconds. WithRespect::AiTraining, it skips a URL whose site refuses AI training in aContent-Signalline (Cloudflare’s format) or aContent-Usageline (the IETF aipref draft). - Sends the request without following redirects. A redirect is queued like a link, so its target passes the same checks; a seed redirected to another host of the same site adds the scope of its target, and a seed redirected to another site yields only that page. A chain longer than five redirects is skipped.
- Skips a block page (
Response::block()), an error status, and a response whoseContent-Typeis not HTML. A response without aContent-Typeis kept when its body starts like HTML, by the MIME Sniffing rules. - Queues the page’s links. Unless robots are ignored, it reads the page’s directives
with
html::Directives::read, queues nothing from anofollowpage, and skips links markedrel="nofollow". - Unless robots are ignored, skips a
noindexpage. WithRespect::AiTraining, it also skips anoaipage and a page whoseContent-Usageheader refuses AI training.noaiis a convention some sites use, not a standard. A page that names another in-scope page as its canonical URL waits until the crawl has that page: it is dropped when the canonical page is kept, and kept when the canonical page is skipped or cannot be fetched. A held page counts towardlimit; when the limit leaves no room to fetch the canonical page, the held page is kept. A page of 20 words or more is skipped when its text, compared word by word, matches a kept page.
The crawl reads the sitemaps that each site’s robots.txt lists on the same
site, or
/sitemap.xml when it lists none (and, unless robots are ignored, when robots.txt
allows it). Unless robots are ignored, it opens only sitemap files that their own
site’s robots.txt allows, and reads that robots.txt first for a sitemap on
another host. It follows a sitemap index to the sitemaps it
names on the same host, reads gzipped and plain-text sitemaps, and reads at most 50 sitemap files and 50 MiB per file;
with a limit, it opens no new sitemap file once it has queued that many
URLs. A sitemap that redirects is queued again at its new address and
passes the same checks. Each <loc> URL passes the same checks as a link; image, video, and
news <loc> entries in a sitemap’s extension namespaces are not pages and
are skipped.
A request that fails comes out of the stream as an Err, and the crawl goes
on. crawl.stats() returns CrawlStats: requests(), pages(),
errors(), and skipped(SkipReason) for each reason above. Its Display
prints one key=value line. CrawlPage has url(), the address after
redirects, depth(), status(), html(), text() from html::text, and
meta(name). A page body is capped by the session’s
CompressionConfig::max_body_size, 100 MiB by default.
Session::shutdown() ends the crawl: requests in flight fail with the
shut-down error and nothing new starts. Dropping the Crawl cancels its
requests. Session::crawl needs the html feature, which is on by default.
Build the crawl session
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::trace::{Fanout, Metrics, TracingTrace};
use leyline::{
BlockRules, Browser, HostLimits, ProxyPool, RetryPolicy, Session, TimeoutConfig,
};
fn run() -> leyline::Result<()> {
let blocks = {
let mut rules = BlockRules::builtin().clone();
rules.extend(BlockRules::statuses([403, 429]));
rules
};
let proxies = ProxyPool::new([
"http://user:pass@proxy-a.example:8080",
"http://user:pass@proxy-b.example:8080",
"http://user:pass@proxy-c.example:8080",
])
.sticky_for(Duration::from_secs(600))
.ban_after(3)
.ban_for(Duration::from_secs(120))
.rotate_on_block(blocks.clone());
let metrics = Metrics::new();
let session = Session::builder()
.browser(Browser::default())
.host_limits(
HostLimits::new()
.max_in_flight(4)
.per_second(2.0)
.max_total_in_flight(64)
.pause_on([429, 503]),
)
.proxy_pool(proxies.clone())
.retry(RetryPolicy::transient().skip_blocks(blocks))
.timeout(TimeoutConfig::new().body(Duration::from_secs(60)))
.trace(Fanout::new().with(TracingTrace).with(metrics.clone()))
.build()?;
let _ = session;
Ok(())
}
}
Clone the session into each worker task. A clone is an Arc clone: it shares
the connection pool, the cookie jar, the host limits, and the proxy pool.
TimeoutConfig::body bounds each body from its first read, so a slow page fails with a
timeout error. See
Retries and timeouts.
Limit each host
HostLimits applies per origin (scheme, host, and port), across HTTP/1.1,
HTTP/2, and HTTP/3. A host name with a trailing dot is the same host as the
name without it. HostLimits::default() has no limits.
HostLimits is configuration. Each session that you build from it keeps its
own counters, queues, and pauses. Clones and derived sessions of that session
share them; a second session built from the same HostLimits value does not.
| Setter | Effect |
|---|---|
max_in_flight(n) | At most n requests to the origin at the same time. The smallest value is 1 |
per_second(rate) | Requests to the origin start at least 1 / rate seconds apart. A rate of 0 or less turns the spacing off |
max_total_in_flight(n) | At most n requests at the same time across all hosts. The smallest value is 1 |
host(host, limits) | Replaces max_in_flight and per_second for one host. *.example.com matches every subdomain, not example.com itself. An exact host wins over a wildcard, and a longer wildcard over a shorter one |
pause_on(statuses) | A response with one of these statuses stops new requests to the origin that sent it, for the wait the server asks for. Requests in flight continue |
pause_for(duration) | The pause when the response asks for no wait. The default is 60 s |
max_pause(duration) | The longest pause a server can ask for. A longer request is cut to this value. The default is 24 h |
The server’s wait comes from the same headers the retry policy reads: each
RetryPolicy::wait_header, then Retry-After. See
Retries and timeouts.
#![allow(unused)]
fn main() {
use leyline::HostLimits;
let limits = HostLimits::new()
.max_in_flight(4)
.per_second(2.0)
.host("api.shop.example", HostLimits::new().max_in_flight(1).per_second(0.5))
.host("*.cdn.example", HostLimits::new().max_in_flight(16))
.max_total_in_flight(64);
let _ = limits;
}
An override uses only the max_in_flight and per_second you pass; a
setting it leaves out has no limit for that host. The global cap applies to
every host.
A request is admitted in this order:
- It waits for an in-flight slot of its origin.
- It waits while the origin is paused, then for the origin’s next rate slot.
- It waits for a global slot. A request that loses its turn while it waits for the global slot goes back to step 2.
No request holds a global slot while it waits for a pause or the rate spacing. The rate spacing is measured when a request is admitted, and a pause is checked again just before admission.
The wait counts against the request deadline, and ends in Kind::Timeout
when the deadline passes. Each retry waits its turn again. Each redirect hop
is admitted against the origin it goes to, and releases the slot of the hop
before. pause_on and the proxy pool’s strikes apply to the origin that sent
the response, which after a redirect is not the origin of the first URL. A buffered request releases its slot when
its body has been read; a .stream() request keeps it until the body ends,
fails, or is dropped. A streamed body ends at its first error and releases
the slot. A failed attempt releases its slot before the retry wait. Dropping
a waiting request removes it from the queue.
max_in_flight counts requests to one origin across every proxy. The
connection pool then applies PoolConfig::max_h1_conns_per_host to each
host, port, scheme, proxy, and transport. For HTTP/1.1 through one proxy, or
direct, the smaller limit bounds the requests in flight. HTTP/2 and HTTP/3
multiplex on one connection, so only max_in_flight bounds them. See
Connection pool.
Obey robots.txt
Session::robots(url, agent) fetches /robots.txt for the origin of url
and returns Robots, the rules for the product token agent. Pass "*" to
read only the group for every crawler. The rules follow RFC 9309:
| Robots.txt fetch | Robots::source() | Result |
|---|---|---|
| 2xx | Parsed | The rules of the matching groups |
| 4xx | Unavailable | Every path is allowed |
| 5xx, or the request failed | Unreachable | Every path is disallowed |
A crawl sends the session’s Authorization header (including
bearer_auth) and a Cookie default header only to the seed’s origin;
requests to other hosts go without them. Cookies from the session’s cookie jar
follow cookie rules, which match by host and path but not by port (RFC 6265,
section 8.5). Other default headers go to every host, so do not put a
credential in a custom default header for a crawl.
A crawl request waits for the session’s own host limits and the crawl’s pace
together, and is sent only when both allow it. A 429 or 503 that the crawl gets pauses the
host in both, so the session’s own requests to that host wait out the same
pause. A host that is paused, waiting
for its next turn, or out of free slots holds no crawl slot. When a page or a
sitemap fails in a way the retry policy retries (the session’s
RetryPolicy, or three tries on transient errors, 429, and 503 when it has
none), the crawl puts it back in the queue and tries again after the policy’s
wait, without holding a slot. A redirect that leaves the scope counts as
SkipReason::Scope. A URL longer than 2048 bytes, or more than 64 links from
the seed, is dropped without a count.
A crawl that obeys robots rules holds a site’s pages until its robots.txt is
read, and retries an unreachable robots.txt four times, 1, 2, 4, and 8
seconds apart, before it skips the site. A crawl with ignore_robots() never
asks for robots.txt; it reads /sitemap.xml directly. A missing sitemap is not
an error; a sitemap that fails after its retries counts in errors().
Up to five redirects are followed, and the rules apply to the origin you
asked about. The first 512 KiB of the file are parsed. Session::robots
returns an error only for an invalid URL or a shut-down session.
The groups that name agent, compared without case, are merged; with no such
group, the * groups apply. A path matches the longest rule, and Allow
wins a tie. * matches any run of characters, $ ends a pattern, and both
sides are compared with percent-encoding normalized. /robots.txt itself is
always allowed.
#![allow(unused)]
fn main() {
use leyline::{Purpose, Url};
async fn run(session: leyline::Session) -> leyline::Result<()> {
let robots = session.robots("https://shop.example/", "examplebot").await?;
let page = Url::parse("https://shop.example/private/report.html")?;
if robots.allows(&page) && robots.allows_use(&page, Purpose::AiTrain) {
let _ = session.get(page.as_str()).send().await?;
}
println!("delay {:?}, sitemaps {:?}", robots.crawl_delay(), robots.sitemaps());
Ok(())
}
}
Robots::parse(text, agent) reads a file you already have.
allows_use(url, purpose) reads the AI-use preferences of the matching
group. A Content-Signal line uses search, ai-input, and ai-train with
yes or no; a Content-Usage line, from the IETF aipref draft, uses
search, ai-use, and train-ai with y or n, and can start with a path
pattern that limits it to matching URLs. Both map to Purpose. For a
use, the line with the longest path pattern that matches the URL decides;
on a tie, no wins, and a use no line names is allowed. Unknown labels are
ignored. The aipref labels are a draft and may
change. crawl_delay() returns the group’s Crawl-delay; Session::crawl
applies it unless robots are ignored. sitemaps() lists every Sitemap line in
the file.
Rotate proxies with a pool
A ProxyPool keeps an origin on one proxy, strikes and bans failing proxies,
and moves each retry to another healthy proxy. Its settings, strike rules,
stats(), and ProxyPool::identified are described in
Use a proxy pool.
Detect a block page
A block is a response, not an error. Response::block() checks it against
built-in vendor rules and returns a BlockSignal with a vendor and a
kind. A bare 403 or 429 is not a block signal.
| Vendor rule | Status | Header | Kind |
|---|---|---|---|
| Cloudflare | any | cf-mitigated: challenge | BlockKind::Challenge |
| AWS WAF | 202 | x-amzn-waf-action: challenge | BlockKind::Challenge |
| AWS WAF | 405 | x-amzn-waf-action: captcha | BlockKind::Captcha |
BlockRules::from_toml adds rules of your own. Each [[rule]] table has
vendor and kind (challenge, captcha, or block), and optional
status, header, and value. The value match ignores ASCII case.
BlockRules::statuses([403, 429]) makes bare statuses count as blocks, with
BlockKind::Block and vendor "status". Rules match in order, so add them
after the vendor rules. BlockRules::builtin() returns a reference: clone it
before extend. rules.check(&resp) checks a response against a set. Clones
are cheap, because the rules sit behind an Arc.
#![allow(unused)]
fn main() {
use leyline::{BlockKind, BlockRules};
async fn run(session: leyline::Session) -> leyline::Result<()> {
let mut rules = BlockRules::builtin().clone();
rules.extend(BlockRules::from_toml(
r#"
[[rule]]
vendor = "shop"
kind = "block"
status = 403
header = "x-shop-block"
value = "1"
"#,
)?);
rules.extend(BlockRules::statuses([403, 429]));
let resp = session.get("https://shop.example/item/1").send().await?;
match rules.check(&resp) {
Some(signal) if signal.kind == BlockKind::Captcha => println!("captcha from {}", signal.vendor),
Some(signal) => println!("blocked by {}", signal.vendor),
None => println!("{} bytes", resp.text().await?.len()),
}
Ok(())
}
}
Retry a block through another proxy
One BlockRules value drives three settings, in this order on each attempt:
ProxyPool::rotate_on_blocksees every attempt. A block is a strike on the proxy that carried it and moves the origin off that proxy.RetryPolicydecides whether to send again, through another proxy.skip_blocks(rules)returns a block at once, without a retry, even when its status is a trigger such as 429. Calling it again adds rules.rules.check(&resp)in your code reports the block on the last response.
Use skip_blocks when a retry only repeats the block, as in
Build the crawl session: the pool moves the
origin and your code decides what to do. A status that the retry policy
retries never reaches your code while retries remain. A Cloudflare challenge
is a 403, so a policy with on_status(403) retries it before you see it.
Without a pool, RetryPolicy::rotate_proxies with on_status(403) sends
each retry through the next proxy in a list. See
Rotate proxies on retry.
Read the text of a page
html::text(document) returns the text a reader sees, one block per line:
each heading, paragraph, list item, and table cell starts a new line, and
<br> breaks a line. Runs of whitespace collapse to one space, except inside
<pre>, which keeps its lines and indentation. Entities are decoded. The text
of <head>, <script>, <style>, <noscript>, <template>, <svg>, and
<textarea> is left out. It needs the html feature, which is on by default.
#![allow(unused)]
fn main() {
use leyline::html;
async fn run(session: leyline::Session) -> leyline::Result<()> {
let page = session.get("https://shop.example/about").send().await?.text().await?;
for line in html::text(&page).lines() {
println!("{line}");
}
Ok(())
}
}
html::text keeps navigation, footers, and other text that repeats on every
page of a site. To drop it, compare lines across pages and remove the lines
that most pages share.
Obey a page’s own rules
A page can carry rules of its own, in <meta name="robots"> tags and in
X-Robots-Tag headers. html::Directives::read(document, headers, agent)
reads both and returns Directives:
| Method | false when the page says |
|---|---|
index() | noindex or none: do not keep the page |
follow() | nofollow or none: do not follow its links |
ai() | noai: keep the page out of AI datasets |
A meta tag named robots, or named for agent without case, applies. An
X-Robots-Tag value that starts with a product token and a colon, such as
examplebot: noindex, applies only to that token. Other directives, such as
noarchive or max-snippet, are read and ignored.
html::canonical(document) returns the href of the first
<link rel="canonical">, as written in the page. Join it with
Response::url() before you compare URLs.
#![allow(unused)]
fn main() {
use leyline::html::{self, Directives};
async fn run(session: leyline::Session) -> leyline::Result<()> {
let resp = session.get("https://shop.example/item/1").send().await?;
let url = resp.url().clone();
let headers = resp.headers().clone();
let page = resp.text().await?;
let rules = Directives::read(&page, &headers, "examplebot");
if rules.index() && rules.ai() {
let canonical = html::canonical(&page).and_then(|href| url.join(&href).ok());
println!("keep {}", canonical.unwrap_or(url));
}
Ok(())
}
}
Group errors
Error::category() tells you what to do with a failed URL. See
Errors for every category.
| Category | Typical action |
|---|---|
Timeout, Connect, Tls | Retry later |
Proxy | The proxy failed. The pool already counted a strike |
ProxyTarget | The proxy works, but the origin is not reachable through it |
Dns | Drop the URL or the host |
Status | An error status from error_for_status or download |
BodyLimit | The body passed max_body_size or your limit |
| Other categories | Log the error and drop the URL |
#![allow(unused)]
fn main() {
use leyline::{ErrorCategory, Session};
enum Next {
Done(String),
RetryLater,
Drop,
}
async fn crawl_one(session: &Session, url: &str) -> Next {
let resp = match session.get(url).tag("listing").send().await {
Ok(resp) => resp,
Err(err) => {
eprintln!(
"{url}: {} after {} attempts via {}",
err.category(),
err.attempts(),
err.proxy().unwrap_or("direct"),
);
return match err.category() {
ErrorCategory::Timeout
| ErrorCategory::Connect
| ErrorCategory::Tls
| ErrorCategory::Proxy
| ErrorCategory::ProxyTarget => Next::RetryLater,
_ => Next::Drop,
};
}
};
if resp.block().is_some() {
return Next::RetryLater;
}
match resp.text().await {
Ok(text) => Next::Done(text),
Err(_) => Next::Drop,
}
}
}
Download files with a cap
RequestBuilder::download(path, limit) streams the decoded body to a
temporary file in the same directory, then renames it to path. A status of
400 or more becomes an error with the status, the URL, the headers, and the
start of the body, within the
status-error limits. The limit is the smaller of limit and max_body_size.
When the body has no content coding and its Content-Length is over the
limit, the download fails with a body-limit error before it reads the body.
Any error or cancel removes the temporary file, so path never holds a
partial file. See Streaming.
#![allow(unused)]
fn main() {
use std::time::Duration;
async fn run(session: leyline::Session) -> leyline::Result<()> {
let written = session
.get("https://shop.example/catalog.csv")
.tag("catalog")
.timeout(Duration::from_secs(300))
.download("catalog.csv", Some(50 * 1024 * 1024))
.await;
match written {
Ok(bytes) => println!("{bytes} bytes"),
Err(err) if err.is_timeout() => println!("download took too long"),
Err(err) => return Err(err),
}
Ok(())
}
}
Log each request with a tag
RequestBuilder::tag(tag) names a request, and TracingTrace writes one line
per request with the tag, the redacted proxy, and the browser of the session
that sent it. With ProxyPool::identified, that is the identity of the
proxy.
INFO leyline::trace: request id=12 method="GET" url=https://shop.example/item/1 redirects=0 status=200 version=Http2 attempts=1 elapsed_ms=210 streamed=false tag="listing" proxy="http://user:***@proxy-a.example:8080/" browser=Chrome154 outcome="ok"
A .stream() request logs its body end as a separate event with the same
id. See Logging and tracing.
Count requests and watch the queues
trace::Metrics counts what a session does. Metrics::new() returns an
Arc<Metrics>; pass a clone to the session, through trace::Fanout when you
also log, and keep one to read. snapshot() returns a MetricsSnapshot:
| Method | Count |
|---|---|
requests(), attempts() | Requests, and attempts with retries |
status_class(n) | Responses with status nxx |
errors(category), errors_total() | Errors per ErrorCategory, and all errors |
bodies_complete(), bodies_failed(), bodies_dropped() | Streamed body outcomes |
latency() | (Option<Duration>, u64) pairs: the upper bound of each bucket and its count |
The latency bounds are 10, 50, 100, 250, 500, 1000, 2500, 5000, and 10000 ms;
a request falls in the first bucket whose bound is at least its elapsed time,
and the last pair, with bound None, counts every request over 10 s.
Display of a snapshot prints one key=value line.
Session::host_stats() returns one HostStats per origin that the host
limits track, with origin(), in_flight(), and waiting(). A paused
origin’s requests count as waiting. Session::pool_stats() returns
PoolStats with busy and idle connections. An HTTP/2 or HTTP/3
connection is busy while it has an open stream; on HTTP/1.1, busy counts
the requests that hold a connection permit.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::ErrorCategory;
use leyline::trace::Metrics;
async fn run(session: leyline::Session, metrics: std::sync::Arc<Metrics>) {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
let snapshot = metrics.snapshot();
println!("{snapshot}");
if snapshot.errors(ErrorCategory::Proxy) > 100 {
break;
}
for host in session.host_stats() {
println!("{} in_flight={} waiting={}", host.origin(), host.in_flight(), host.waiting());
}
}
}
}
Rotate identities
Session::with_identity derives a session with another browser identity.
Use one session per identity and spread the URLs over them.
#![allow(unused)]
fn main() {
use leyline::{Browser, Family, Identity, Platform};
fn run(session: leyline::Session) -> leyline::Result<()> {
let firefox = session.with_identity(Identity::locked(
Browser::latest(Family::Firefox),
Platform::Windows,
))?;
let mac_chrome = session.with_identity(Identity::locked(
Browser::latest(Family::Chrome),
Platform::MacOS,
))?;
let _ = (firefox, mac_chrome);
Ok(())
}
}
A derived session shares its parent’s connection pool, in a partition of its
own, so an identity never reuses another identity’s connection. One
PoolConfig budget covers every identity, and pool_stats() counts them
all. The cookie jar, the HSTS store, the Alt-Svc knowledge, the host
limits, and the proxy pool are shared too. Each identity has its own TLS
session cache. To give an identity its own cookies, add with_cookie_jar, or
use ProxyPool::identified. See Sessions.
Stop the crawl
Session::shutdown() stops every clone and derived session: requests in
flight, new requests, and the next read of a body being streamed fail with
Kind::Request and the message “session shut down”. Error::is_shut_down()
tells this error apart, and Session::is_shut_down() reports the state.
Shutdown does not close the pooled connections. To stop a single request,
drop its future or its streamed body. See Cancellation.
buffer_unordered(n) from futures_util keeps at most n request futures
alive, so a large URL list does not hold one task per URL, and
max_total_in_flight keeps the session’s cap across every caller. Check
is_shut_down() before each URL to stop reading the list:
#![allow(unused)]
fn main() {
use futures_util::future;
use futures_util::stream::{self, StreamExt};
async fn run(session: leyline::Session, urls: Vec<String>) {
let stop = session.clone();
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
stop.shutdown();
}
});
let open = session.clone();
let mut results = stream::iter(urls)
.take_while(move |_| future::ready(!open.is_shut_down()))
.map(|url| {
let session = session.clone();
async move { (session.get(&url).await, url) }
})
.buffer_unordered(32);
while let Some((result, url)) = results.next().await {
match result {
Ok(resp) => println!("{url}: {}", resp.status()),
Err(err) if err.is_shut_down() => {}
Err(err) => eprintln!("{url}: {}", err.category()),
}
}
}
}
A URL stream built with stream::unfold over an async reader is not Unpin.
Pin the combined stream with std::pin::pin! before you call .next().
Memory bounds
| Item | Bound |
|---|---|
| Buffered body | CompressionConfig::max_body_size, 100 MiB by default |
| Body kept in a status error | CompressionConfig::max_error_body, 64 KiB by default |
| Idle connections | PoolConfig::idle_timeout, 300 s by default |
| Connections | PoolConfig::max_connections per pool, 2048 by default |
| HTTP/1.1 connections per host | PoolConfig::max_h1_conns_per_host, 256 by default |
| Requests that wait for an HTTP/2 stream | 1024 per connection |
| Origins remembered as HTTP/3 | 1024 per pool |
| Origins remembered as HTTP/1.1-only | 1024 per pool |
Stream large bodies with download or copy_decoded_to, so only one chunk is
in memory.
Next
Read Accounts.
Cookies
Every session owns a Jar. It sends the matching cookies on each request and
stores what each response sets, with the rules Chrome applies. This page
covers reading and seeding the jar, sharing it, and saving it between runs.
Read and seed the jar
Session::cookies() borrows the jar. Every jar method that takes a URL takes
a parsed &Url and returns no Result. leyline::Url is the url crate’s
Url, so parse once with leyline::Url::parse(..)? and pass the same value
to each call.
#![allow(unused)]
fn main() {
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let session = leyline::Session::new();
session.get("https://www.example.com/login").await?;
let jar = session.cookies();
let url = leyline::Url::parse("https://www.example.com/")?;
if let Some(id) = jar.get_cookie(&url, "session_id") {
println!("session_id={id}");
}
if let Some(header) = jar.cookie_header(&url) {
println!("cookie: {header}");
}
jar.store_set_cookie("token=abc; Domain=.example.com; Path=/; Secure", &url);
assert_eq!(jar.remove(&url, "token"), 1);
Ok(())
}
}
| Method | Does |
|---|---|
get_cookie(url, name), set_cookie(url, name, value) | Read or set one cookie scoped to a URL |
cookie_header(url) | The Cookie header the jar sends to a URL |
store_set_cookie(set_cookie, url) | Stores a full Set-Cookie value, with Domain, Path, Secure, HttpOnly, SameSite, Max-Age, and Expires, through the parser the session uses. Max-Age=0 deletes the matching cookie |
load_cookies(header, url) | Loads a Cookie header string |
remove(url, name) | Deletes every cookie of that name the host of url receives, on every path. Returns the number removed |
remove_named(name) | Deletes every cookie of that name on every host. Returns the number removed |
clear() | Deletes every cookie |
all_cookies() | Every stored cookie, sorted by domain then name, expired ones included |
snapshot(), extend_from(other) | Fork and merge. See Share or fork a jar |
A Cookie has the public fields name, value, domain, path, secure,
http_only, same_site (Strict, Lax, or None), expires,
creation_time, and host_only. Cookie::is_expired() tells whether the
expiry has passed.
Read the cookies a response set
Response::cookies() parses the Set-Cookie headers of the final response
into Cookie records. It does not read the jar, so it also lists a deletion.
It skips a header that does not parse and a cookie whose Domain is a public
suffix or does not cover the host. To read what the session sends, ask the
jar.
#![allow(unused)]
fn main() {
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let session = leyline::Session::new();
let resp = session.get("https://shop.example/login").await?;
for cookie in resp.cookies() {
println!("set {}={} for {}", cookie.name, cookie.value, cookie.domain);
}
Ok(())
}
}
Share or fork a jar
Jar is a handle over shared state: jar.clone() and session clones see the
same cookies, so a cookie one task receives reaches every other task at once.
SessionBuilder::cookie_jar(jar) starts a session from a jar you built.
session.with_cookie_jar(jar) derives a session that shares the pool and
every other setting but reads and writes the given jar; use it for a second
identity on warm connections. See Sessions.
snapshot() is a fork: a new store with a copy of every cookie and all its
attributes. A write to the copy does not reach the original.
extend_from(&other) merges the cookies of other with their attributes; on
the same name, domain, and path, the cookie from other wins. Together they
try a step without touching a stored login:
#![allow(unused)]
fn main() {
async fn run(session: leyline::Session) -> leyline::Result<()> {
let probe_jar = session.cookies().snapshot();
let probe = session.with_cookie_jar(probe_jar.clone());
if probe.get("https://example.com/check").await?.status().is_success() {
session.cookies().extend_from(&probe_jar);
}
Ok(())
}
}
What the jar does
The jar follows Chrome:
- A cookie is stored under the exact domain the
Set-Cookienames: itsDomain=value, or the request host. Securecookies go only over HTTPS.SameSiteuses thesec-fetch-sitevalue the request sends. Leyline computes it from the initiator and every URL in the redirect chain, schemes included, sohttp://tohttps://on one host is cross-site. The initiator is the request’sReferer; without one, the session’sRefererwhen it is an absolute URL; otherwise the first URL. A preset or header that setssec-fetch-sitesets it for cookies too.- A cross-site request sends no
Strictcookie, and sendsLaxcookies only on a top-level navigation (sec-fetch-dest: document, or none) withGETorHEAD. Firefox profiles also treat a request as cross-site when its redirect chain crosses sites. - A cookie lives at most 400 days. A later
Max-AgeorExpires, or anExpirespast the range of the platform clock, is cut to 400 days. - A cookie whose name and value together pass 4096 octets is refused. An
attribute whose value passes 1024 octets is ignored, so an oversized
Pathfalls back to the default path, which comes from the request URL and has no cap. A cookie whose stored domain passes 1024 octets is refused.extend_fromand loading a saved jar apply the same limits and skip a cookie that fails them. - The limits are 180 cookies per exact domain and 3300 overall. Crossing a limit evicts the least recently accessed entries, 30 per domain or 300 overall.
- An expired cookie stays in memory but is never sent.
get_cookieskips it;all_cookies()andsnapshot()include it.
Domain scoping uses the Mozilla Public Suffix List, compiled into the crate.
A cookie with Domain=example.co.uk reaches shop.example.co.uk; a host-only
cookie set by www.example.co.uk reaches that host only. A Set-Cookie with
Domain=co.uk, or with a domain that has no dot, is refused.
Save the jar to a file
Jar and Cookie implement Serialize and Deserialize, with times as
unix milliseconds. Jar::save_to(path) writes the jar as JSON and
Jar::load_from(path) reads it back. The write is atomic: a temporary file,
synced, renamed over path, and the directory synced. On Unix the file has
mode 0600; on Windows it takes the default ACLs of its directory.
The file is a versioned object, { "version": 1, "cookies": [..] }.
load_from refuses a file with another version, or with no version, with
Kind::Json.
- Saving writes every cookie that has not expired, session cookies included, as a browser that restores its last session does.
- Loading skips expired cookies and cookies past the size limits, keeps the newest of two cookies with the same name, path, and domain, applies the 180 and 3300 limits, and cuts a stored expiry to at most 400 days after the load.
- Each cookie keeps its last-access time, so eviction order survives a restart. A jar saved without that field loads with the load time.
Jar::autosave(path, interval) starts one writer task and returns a
JarAutosave handle. It saves at most once per interval after a change.
It panics outside a Tokio runtime.
| Event | What the writer does |
|---|---|
| A cookie changes | Saves when interval has passed since the first unsaved change, so a burst of Set-Cookie headers gives one write. Later changes do not move the save |
flush().await | Saves now and returns the result |
shutdown().await | Saves, stops the task, and returns the result |
| The handle is dropped | Saves once more in the background. The runtime must still run |
| The runtime shuts down | The writer task saves unsaved changes as it stops |
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::cookie::Jar;
async fn run() -> leyline::Result<()> {
let jar = if std::path::Path::new("cookies.json").exists() {
Jar::load_from("cookies.json")?
} else {
Jar::new()
};
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.cookie_jar(jar)
.build()?;
let saver = session.cookies().autosave("cookies.json", Duration::from_secs(2));
session.get("https://shop.example/account").await?;
saver.shutdown().await?;
Ok(())
}
}
Call shutdown().await before the program exits, so the last save is not
lost.
Keep a logged-in device between runs
A site can tie a login to the browser, platform, proxy, and languages as well
as the cookies. Device saves all of them with the jar, and
Device::autosave replaces Jar::autosave. See Accounts.
Save the connection state
TLS session tickets, Alt-Svc entries, and the HSTS store live in
SessionState, beside the jar. See
Accounts.
Next
Read WebSocket.
Accounts
This page keeps one logged-in account the way a browser profile keeps it:
the same browser, platform, languages, proxy, cookies, and connection state
on every run. A Device holds all of them in one file. Device::autosave
keeps the file current, a Tab keeps the current page, and leyline::html
reads the login form.
What a device holds
| Field | Holds |
|---|---|
identity, platform, brand | The browser the session sends. Device::capture stores Some(ChromiumBrand::Chrome) for a Chrome session with no .brand(), and None for another family |
profile_toml | The frozen profile, after pin_profile |
profile_id | The id that check compares |
user_agent | The User-Agent of the session |
languages | The tags from SessionBuilder::languages |
proxy | The proxy URL, password included. Debug and Display mask the password |
proxy_password_env | An environment variable that holds the proxy password |
env_proxy | Whether to read proxy environment variables. Off by default |
strict | When true, check refuses a device with no profile_id, or with no proxy while env_proxy is false. Off by default |
jar_path, jar | The jar file, or a jar kept in the device file |
state | The SessionState. See Keep the connection state |
page | The current page of the account’s tab |
app | Data of your own, a BTreeMap<String, serde_json::Value> |
| Call | Does |
|---|---|
Device::capture(&session, proxy) | Reads the device from a session, with the proxy URL you pass |
pin_profile(&session) | Stores the profile TOML in the device |
save_to(path), load_from(path) | Writes and reads the device as JSON, { "version": 1, .. }. The write is atomic, and on Unix the file has mode 0600 |
open() | Builds the session, runs check, loads the jar, and restores state |
session_builder() | Returns the builder, for more settings before build() |
check(&session) | Returns Kind::Config when the session differs from the device |
tab(&session) | Returns a Tab whose current page is page |
autosave(&session, path, options) | Saves the device, its jar, and the session state as they change |
Create the device once
Pin a browser variant, set the languages and one proxy, and turn the proxy environment variables off. Then capture the device and freeze its profile.
#![allow(unused)]
fn main() {
use leyline::{Browser, Device, Platform, ProxyConfig, ProxyUrl, Session};
fn run() -> leyline::Result<()> {
let proxy = ProxyUrl::parse("http://user:secret@proxy.example:8080")?;
let session = Session::builder()
.browser(Browser::Chrome154)
.platform(Platform::Windows)
.languages(["de-DE", "de", "en"])
.proxy(ProxyConfig::from(proxy.clone()).env(false))
.build()?;
let mut device = Device::capture(&session, Some(proxy));
device.pin_profile(&session)?;
device.strict = true;
device.jar_path = Some("account/cookies.json".into());
device.save_to("account/device.json")?;
Ok(())
}
}
| Choice | Why |
|---|---|
Browser::Chrome154, not Browser::latest | latest moves to newer captures in patch releases |
pin_profile | A crate upgrade cannot change the ClientHello or the headers: a pinned device loads its own profile TOML and keeps its profile_id. It fails for a session whose TLS hello comes from another browser. See Profile data stability |
env_proxy stays false | The device never picks up HTTPS_PROXY or ALL_PROXY from another machine, and a device without a proxy goes direct |
strict | A strict device cannot go direct by mistake |
Session::proxy_url() returns the configured proxy, password included, so
Device::capture(&session, session.proxy_url()) captures it too. check
compares the configured URL, not the exit IP the proxy assigns: a rotating
proxy behind one URL passes with a new IP. To pin the exit IP, fetch it from
your own IP echo service and keep it in Device::app.
Keep the proxy password out of the file
ProxyUrl serializes the full URL, password included. To keep the password
out of the device file, set proxy_password_env to the name of an
environment variable. save_to and Device::autosave then write the proxy
URL without its password, and open and session_builder read it back
from the variable. With proxy_password_env set, serializing the Device
never writes the password. When the variable is not set in the
environment, open and session_builder return Kind::Config with a
message that names it.
#![allow(unused)]
fn main() {
use leyline::{Browser, Device, Platform, ProxyConfig, ProxyUrl, Session, Url};
const PROXY: &str = "http://user@proxy.example:8080";
const PASSWORD_VAR: &str = "ACCOUNT_PROXY_PASSWORD";
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut url = Url::parse(PROXY)?;
url.set_password(Some(&std::env::var(PASSWORD_VAR)?))
.map_err(|()| "the proxy URL cannot carry a password")?;
let proxy = ProxyUrl::parse(url.as_str())?;
let session = Session::builder()
.browser(Browser::Chrome154)
.platform(Platform::Windows)
.proxy(ProxyConfig::from(proxy).env(false))
.build()?;
session.get("https://shop.example/login").await?;
let mut device = Device::capture(&session, session.proxy_url());
device.proxy_password_env = Some(PASSWORD_VAR.to_owned());
device.pin_profile(&session)?;
device.save_to("account/device.json")?;
Ok(())
}
}
Open the device on each run
Device::open builds the session, runs check, loads the cookies, and
restores the saved connection state. A jar_path whose file does not exist
yet gives an empty jar. A device with no jar_path uses the jar in the
device file, or an empty jar. Then start Device::autosave and call its
shutdown before the process exits.
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::Device;
async fn run() -> leyline::Result<()> {
let device = Device::load_from("account/device.json")?;
let session = device.open()?;
let tab = device.tab(&session);
let autosave = device.autosave(&session, "account/device.json", Duration::from_secs(2));
autosave.track(&tab);
tab.follow("/account/orders").await?;
autosave.shutdown().await?;
Ok(())
}
}
To add settings before the build, call device.session_builder()?, change
the builder, build the session, then call device.check(&session)? and
device.state.restore_into(&session).
Saved TLS sessions are bound to the trust that the session loaded when it was
built: the pins, the contents of every root it loaded from add_ca_der,
add_ca_file, SSL_CERT_FILE, and SSL_CERT_DIR, the contents of the client
certificate and key files, and whether system roots are on. A saved TLS
session resumes only in a session that loaded the same trust. Otherwise the
connection makes a full handshake and checks the certificate again.
Session::fresh_pool keeps the trust of the session it copies. Changes to the
operating system’s root store are not part of the binding. The saved
state contains TLS session secrets, so store it with the same protection as
credentials.
check returns Kind::Config and names each field that differs:
| Field | Compared with |
|---|---|
profile_id | The session’s profile id, when the device has one. It covers the profile data, the platform, and the brand |
identity, platform, brand | The session’s HTTP profile, TLS profile, platform, and brand. Only without a profile_id |
user_agent | The session’s User-Agent, when the device has one and no profile_id |
languages | The session’s languages |
proxy | Session::proxy_url(), unless env_proxy is set. The scheme, user name, host, and port must match; the password is not compared |
strict | When true: a profile_id is present, and a proxy is present unless env_proxy is set |
Error::is_profile_changed() is true when the profile differs: the
profile_id, or, without one, the identity, platform, brand, or user agent.
It is false when only the languages, the proxy, or a strict gap differ.
Save the device as it changes
Device::autosave(&session, path, options) starts one writer task and
returns a DeviceAutosave handle. options is a DeviceAutosaveOptions, or
a Duration that sets its interval:
| Option | Default | Effect |
|---|---|---|
interval | Set by DeviceAutosaveOptions::new(interval) | After a change, saves at most once per interval |
state_interval | 300 s | Saves at this period, so the connection state stays current |
Each save writes the device file with the
session’s current SessionState. With a jar_path, it also writes the jar
file when a cookie changed; without one, the jar goes into the device file.
For a jar without a device, use Jar::autosave; see
Cookies.
| Event | Effect |
|---|---|
A cookie changes, or update or track runs | A save runs when interval has passed since the first unsaved change. Later changes do not move the save |
state_interval passes | A save runs, so new TLS tickets, Alt-Svc, and HSTS entries reach the file |
flush().await | Saves now and returns the result |
shutdown().await | Saves and stops the writer |
| The handle is dropped | A last save runs in the background. The runtime must still run |
| The runtime shuts down | The writer task saves unsaved changes as it stops |
Each save writes a temporary file, syncs it, and renames it, so a crash never
leaves a half-written file. On Unix the files have mode 0600; on Windows
they take the default ACLs of their directory. autosave panics outside a Tokio runtime.
The writer keeps its own copy of the device. Change it with
DeviceAutosave::update(|device| ..); the change reaches the file with the
next save. A change to the Device you called autosave on does not.
DeviceAutosave::device() returns a snapshot of the writer’s copy.
DeviceAutosave::track(&tab) stores the tab’s current page in
Device::page on each save. The writer keeps one tab: a second track
replaces the first. On the next run, Device::tab(&session) returns a tab
on that page, so the next request carries it as its Referer.
Keep your own data
Device::app holds your own data in the same atomic save, such as the
account email or values a site keeps in localStorage. Leyline does not
read it.
#![allow(unused)]
fn main() {
use leyline::Device;
use serde_json::json;
fn run(mut device: Device) -> leyline::Result<()> {
device.app.insert("email".into(), json!("me@example.com"));
device.app.insert("local_storage".into(), json!({ "cart_id": "c-1842" }));
let email = device.app.get("email").and_then(|v| v.as_str());
let _ = email;
device.save_to("account/device.json")?;
Ok(())
}
}
Leyline keeps no HTTP cache, localStorage, sessionStorage, IndexedDB, or
service workers, because it runs no scripts. Send if-none-match or
if-modified-since yourself.
Keep the connection state
Session::state() returns a SessionState, which implements Serialize and
Deserialize. state.restore_into(&session) loads it, and is_empty() is
true when there is nothing to save.
| Part | Effect after a restore |
|---|---|
| TLS session tickets that have not expired | The first handshake resumes, as a returning browser does |
HTTP/3 Alt-Svc entries and their expiry | HTTP/3 is possible on the first request to a known origin |
| The HSTS store | http:// requests to a known host go to https. See Network |
Device::autosave refreshes device.state on each save. Before a manual
save_to, set device.state = session.state(). A ticket key holds the
proxy URL without its user name and password, and a hash of them, so the saved
state never holds proxy credentials.
Browse with a tab
A Tab sends each request with the current page as its initiator, so the
Referer, Origin, and sec-fetch-site headers match a real click.
Requests lists the calls. For an
account:
xhr,fetch,post_json, andsubresourceon a tab with no page fail withKind::Request(“the tab has no page; open one first”) and send nothing. Open a page first.- A
.stream()navigation changes the page at the response head.current()reads the page, andset_current(..)sets it. - A page script sets its own
accept. Set it on thexhrbuilder when the site’s script does. See Fingerprints.
#![allow(unused)]
fn main() {
use leyline::Preset;
async fn run(session: leyline::Session) -> leyline::Result<()> {
let tab = session.tab();
tab.open("https://shop.example/account").await?;
let orders = tab
.xhr("/api/orders?page=1")
.header("accept", "application/json")
.send()
.await?;
let logo = tab.subresource("/static/logo.png", Preset::Image).send().await?;
println!("{} {}", orders.status(), logo.status());
Ok(())
}
}
Log in with the page’s form
The html feature, on by default, adds leyline::html and turns on the
multipart feature. html::forms(document) returns every <form> as an
html::Form with the values a browser would submit:
- Hidden inputs, checked checkboxes, text areas, and selected options.
- One checked radio button per name: the last one, as a browser keeps it.
- Controls outside the
<form>element that name it withform="id". When two elements share the id, the first one is the form. - No disabled controls, and no controls inside a disabled
<fieldset>except in the first<legend>that is a direct child of that fieldset. - No options inside a disabled
<optgroup>. A list box (<select size>) with no selected option sends nothing. - Line breaks in names and values converted to CRLF, as a browser sends them.
- Values with every HTML named character reference decoded, such as
&andé.
Form::find(document, key) returns the first form whose id or name is
key.
Tab::follow and Tab::submit_form drop the session credentials when the
target origin differs from the current page: the Authorization, Cookie,
and Proxy-Authorization defaults and the session bearer token, as a
redirect does.
| Call | Effect |
|---|---|
action(), method() | The action attribute and the FormMethod, Get or Post |
enctype() | The FormEnctype: UrlEncoded, Multipart, or TextPlain |
base() | The href of the page’s <base> element, if any |
fields(), field(name) | Every (name, value) pair, or the first value of one field |
set(name, value) | Replaces every value of the field with one value, or adds the field |
buttons(), press(name) | The submit button names; press adds one button’s name and value. It returns false for an unknown name, and adds name.x and name.y for an image button |
Tab::submit_form(&form) sends form.fields() as they are, with no submit
button: call form.press(name) first when the site reads the button. The
action resolves against the page’s <base href>, or the current page when
there is none. A Post form sends the fields with Preset::FormNavigate,
encoded as its enctype() says: application/x-www-form-urlencoded,
multipart/form-data, or text/plain. A Get form replaces the action’s
query with the fields. Both are navigations that carry
the page as the Referer and change the current page.
The session redirect policy applies: after a login POST, a 301, 302, or
303 continues as a GET without the body, and a 307 or 308 repeats the
POST. The response and the current page are those of the final URL.
#![allow(unused)]
fn main() {
use leyline::html::{self, Form};
async fn run(tab: leyline::Tab) -> leyline::Result<()> {
let page = tab.open("https://shop.example/login").await?.text().await?;
let token = html::meta(&page, "csrf-token").unwrap_or_default();
if let Some(mut form) = Form::find(&page, "login") {
form.set("email", "me@example.com").set("password", "secret");
form.press("sign_in");
let resp = tab.submit_form(&form).await?;
println!("{} at {}", resp.status(), resp.url());
}
let cart = tab.xhr("/api/cart").header("x-csrf-token", token).send().await?;
println!("{}", cart.status());
Ok(())
}
}
html::meta(document, name) returns the content of the first <meta>
whose name or property equals name, without case; many sites put a CSRF
token there. html::links(document) returns every <a href> as an
html::Anchor with href(), text(), and rel(). href() is the raw
attribute; pass it to Tab::follow, which resolves it.
Check the login, log in again
Leyline cannot tell whether a site considers you logged in. Request an
endpoint that only a logged-in user can read, and log in again when it says
no. The jar and Device::autosave keep the new cookies.
#![allow(unused)]
fn main() {
use leyline::html::Form;
use leyline::http::StatusCode;
async fn run(tab: leyline::Tab) -> leyline::Result<()> {
let me = tab.xhr("https://shop.example/api/me").send().await?;
if me.status() == StatusCode::UNAUTHORIZED {
let page = tab.open("https://shop.example/login").await?.text().await?;
if let Some(mut form) = Form::find(&page, "login") {
form.set("email", "me@example.com").set("password", "secret");
tab.submit_form(&form).await?;
}
}
Ok(())
}
}
When several tasks share the session, each can see the 401 at once. Hold one
lock, such as a tokio::sync::Mutex, around the check and the login, and
check again after you take it, so one task logs in and the others reuse its
cookies. examples/device.rs combines the device, autosave, the login form,
and the tab.
Run tasks at the same time
Session and Tab are Clone, Send, and Sync. Session clones share the
pool, the jar, the identity, and the state, so one Device::autosave covers
every task. Tab clones share the current page, so give each parallel page its
own session.tab().
#![allow(unused)]
fn main() {
async fn run(session: leyline::Session) -> leyline::Result<()> {
let orders = session.tab();
let messages = session.tab();
let (a, b) = tokio::join!(
orders.open("https://shop.example/account/orders"),
messages.open("https://shop.example/account/messages"),
);
println!("{} {}", a?.status(), b?.status());
Ok(())
}
}
To stop every task of the account, call session.shutdown(), then
autosave.shutdown().await for the last save. See
Sessions.
Next
Read Proxies.
WebSocket
The websocket feature is on by default. A WebSocket uses the session’s TLS
fingerprint, proxy rules, and cookie jar. Use wss://: any other scheme,
ws:// included, fails with Kind::Request before Leyline picks a proxy or
connects.
Connect, send, and receive
Session::websocket(url) returns a WebSocketBuilder. Call connect(), or
await the builder. send takes a WsMessage: Text, Binary, Ping,
Pong, or Close.
#![allow(unused)]
fn main() {
use leyline::WsMessage;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let mut ws = session.websocket("wss://example.com/live").connect().await?;
ws.send(WsMessage::Text("ping".to_owned())).await?;
ws.send(WsMessage::Binary(vec![1, 2, 3])).await?;
if let Some(msg) = ws.recv().await? {
println!("{msg:?}");
}
ws.close().await?;
Ok(())
}
}
When the peer closes the connection, recv first returns
Some(WsMessage::Close(frame)), where frame is an Option<CloseFrame>
with the close code and reason. The next recv returns Ok(None).
close sends a close frame and shuts the connection down.
A send, recv, or close that fails on the transport or on a closed
connection gives Kind::Io. A protocol violation, or a message over the
WebSocketConfig limits, gives Kind::Body.
Set handshake options
| Builder method | Effect |
|---|---|
config(WebSocketConfig) | Limits and transport for this connection |
header(name, value), headers(pairs) | Append handshake headers. A repeated name adds a value. An invalid name or value fails connect() with Kind::Request. A user-agent or origin header replaces the one Leyline sends |
proxy(url) | Overrides the session proxy |
The handshake sends the cookies that the jar holds for the URL, matched as
for an https:// request. A Cookie header from the session default headers
or from header() replaces them. A Set-Cookie header in the handshake
response does not update the jar.
A wss:// URL follows the proxy rules of an https:// URL, including
environment discovery and NO_PROXY. See Proxies.
Shutdown and host limits
The handshake takes a host-limit slot for its origin, the same slot an
https:// request to that host takes, and releases it when the handshake
completes or fails. An open connection holds no slot. Session::shutdown()
stops a handshake in flight and fails new ones with the shut-down error. It
does not close an established connection.
HTTP/2 or HTTP/1.1
WebSocketConfig::prefer_http2 defaults to true: Leyline first tries RFC
8441 extended CONNECT on a pooled HTTP/2 connection. It falls back to the
RFC 6455 HTTP/1.1 upgrade on a fresh connection in two cases, and logs the
reason:
- The TLS handshake does not select
h2. Later WebSockets to that origin skip the HTTP/2 attempt. - The peer does not advertise
SETTINGS_ENABLE_CONNECT_PROTOCOL.
Any other error is returned, not retried. prefer_http2(false) skips the
HTTP/2 attempt. protocol() returns the subprotocol the origin selected.
#![allow(unused)]
fn main() {
use leyline::WebSocketConfig;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let ws = session
.websocket("wss://example.com/live")
.config(
WebSocketConfig::new()
.max_message_size(1 << 20)
.prefer_http2(false),
)
.connect()
.await?;
println!("protocol={:?}", ws.protocol());
Ok(())
}
}
Split the connection
split() returns a WsSink with send and close, and a WsStream with
recv, so one task can write while another reads.
#![allow(unused)]
fn main() {
use leyline::WsMessage;
async fn run() -> leyline::Result<()> {
let session = leyline::Session::new();
let ws = session.websocket("wss://example.com/live").connect().await?;
let (mut sink, mut stream) = ws.split();
let reader = tokio::spawn(async move {
while let Ok(Some(msg)) = stream.recv().await {
println!("{msg:?}");
}
});
sink.send(WsMessage::Text("hello".to_owned())).await?;
sink.close().await?;
reader.await.expect("reader task panicked");
Ok(())
}
}
Next
Read HTTP/3.
HTTP/3
The http3 feature is on by default. It adds the Http3 and Race
protocol policies and the QUIC stack. Build with default-features = false
to drop the QUIC stack. This page covers when a request uses HTTP/3, the
QUIC fingerprint keys of a profile, and HTTP/3 through a proxy.
Protocol policy
SessionBuilder::protocol sets the ProtocolPolicy.
| Variant | Behavior |
|---|---|
Auto | HTTP/1.1 for http://. For https://, one TLS handshake that offers the profile’s ALPN list; the server’s choice decides HTTP/2 or HTTP/1.1 |
Http1 | Force HTTP/1.1 |
Http2 | Force HTTP/2 over TLS |
Http3 | Force HTTP/3 over QUIC. Needs the http3 feature |
Race | Race QUIC against TCP and TLS for origins known to speak HTTP/3. Needs the http3 feature |
Without a protocol call, a session selects Race when its profile sets
race = true in [h3], as the bundled Chrome profiles do, and Auto
otherwise. Session::browser(Browser::default()) selects Race;
Session::new() and a build without the http3 feature select Auto.
Keep that default for a browser session: Race uses HTTP/3 where a browser
would and falls back to TCP. Http3 forces QUIC on every request, the first
one included, and fails where QUIC is blocked; use it only to test that an
origin speaks HTTP/3. build() fails with Kind::Config when Http3 or
Race meets a profile with no [h3] table, such as the bare profile.
The Alt-Svc gate
The first request to an origin never uses HTTP/3. As in Chrome, Race opens
no speculative QUIC connection: it races only an origin that advertised h3
in an Alt-Svc response header. The first request goes over HTTP/2 or
HTTP/1.1 and records the header; the next one can race.
Response::version() returns the HttpVersion that carried each response.
#![allow(unused)]
fn main() {
use leyline::{Browser, HttpVersion, Session};
async fn run() -> leyline::Result<()> {
let session = Session::browser(Browser::default());
let url = "https://www.cloudflare.com/";
let first = session.get(url).send().await?;
println!("first: {:?}", first.version());
let second = session.get(url).send().await?;
if second.version() == HttpVersion::Http3 {
println!("second: HTTP/3");
}
Ok(())
}
}
The second request can still use HTTP/2: the first transport to hand back a
connection carries the request, and a pooled HTTP/2 connection is often
ready first. The request is sent once. If both transports fail, the Auto
path runs. examples/http3.rs
runs this sequence.
A request is raced only when all of these hold; otherwise it takes the
Auto path:
- The scheme is
httpsand the origin is known to speak HTTP/3. - Neither the request body nor the response is streamed.
- No proxy applies, or the proxy is
socks5://orsocks5h://and thesocksfeature (not a default feature) is on.
When an HTTP/3 connection to an origin fails, Race skips HTTP/3 for that
origin and proxy pair for 5 minutes. A successful HTTP/3 connection to the
pair ends the skip.
Session::state() saves the known origins, so a restored session can race
on its first request. See
Accounts.
How Alt-Svc is read
Only an h3= entry for the same port counts, and its host must be empty or
equal to the origin’s host. The origin is kept until the longest ma of the
matching entries, less the response’s Age, ends; without ma, 24 hours.
Each Alt-Svc value replaces the one before it: ma=0 on every matching
entry, clear, or a value with no usable h3= entry removes the origin. An
entry whose ma is not a number is ignored. The pool keeps at most 1024
origins and drops the one that expires first to make room.
Body limits and cancellation
HTTP/3 uses CompressionConfig::max_body_size, as HTTP/1.1 and HTTP/2 do.
It caps a buffered body and fails with Kind::Body; a .stream() response
has no cap. See Responses.
A request that ends early resets its stream with the code Chrome sends:
| Cause | Stream halves reset | Code |
|---|---|---|
| The caller drops the response or its body | Both | H3_REQUEST_CANCELLED |
A buffered body passes max_body_size | Both | H3_REQUEST_CANCELLED |
| The request body cannot be sent | Both | H3_REQUEST_CANCELLED |
| The response ends, or the peer resets it, before the request body is sent | The request half | H3_REQUEST_CANCELLED |
| The response is malformed | Both | H3_GENERAL_PROTOCOL_ERROR |
| A request body stream fails | Both | H3_GENERAL_PROTOCOL_ERROR |
When the pool drops an idle connection, Leyline sends no CONNECTION_CLOSE frame, as Chrome does not.
QUIC fingerprint
The [h3] table of a profile sets the QUIC transport parameters and the
HTTP/3 settings: the flow-control limits, max_idle_timeout_secs,
max_udp_payload_size, and active_connection_id_limit. These keys shape
the QUIC Initial and the HTTP/3 control stream. dcid_length is required;
without the other keys, a profile keeps quiche’s defaults.
| Key | Effect |
|---|---|
[h3.tls] | A full [tls] table for the QUIC ClientHello. Without it, QUIC uses [tls]. Its extension_permutation may list quic_transport_parameters (57). permute_extensions = true with extension_tail = [57, 65037] shuffles the extensions on each connection and keeps the listed IDs last, in that order. tls12_extensions = true sends extended_master_secret and renegotiation_info although QUIC is TLS 1.3 only |
dcid_length | The initial destination connection ID length: a number, or { weights = [[length, weight], ...] } for a weighted random length |
scid_length | The source connection ID length, 0 to 20. The default is dcid_length |
transport_parameters | The exact transport parameter list. { id = N } sends the value from the [h3] fields. { id = N, varint = V } and { id = N, hex = "..." } send a fixed value. { id = 17, versions = { chosen, available, grease } } sends version_information, with a GREASE version first or at a random position. { grease = { id_bits, max_len } } sends a reserved parameter with a random ID 31 * N + 27 (N below 2^id_bits) and 0 to max_len random bytes |
initial_datagram_size | The size of each client datagram that carries an Initial packet during the handshake, 1200 to max_udp_payload_size, at most 1232 over IPv6. Each first-flight Initial is padded with PADDING frames to this size; after a loss timeout with no reply, the size falls back to 1200. Without it, Initials are not padded and the datagram is filled with zeros to 1200 bytes |
initial_crypto_split | fill (default) or even. With even, the first Initial packet carries an even share of the ClientHello and no other packet shares its datagram; the next carries the rest |
initial_crypto_reorder | none (default) or sni_midpoint, which needs initial_crypto_split = "even". The ClientHello is cut at the midpoint of the server name. The first Initial carries the end of the ClientHello in one CRYPTO frame, then the start up to the cut in a second; the next Initial carries the middle |
transport_order | fixed, shuffle (a new random order per connection), or rotate (the list rotated by a random offset). Entries with pinned = true keep their place |
max_ack_delay_ms | The max_ack_delay value, when the list sends parameter 11 |
settings | The exact SETTINGS list, in order. { id, value } sends a fixed setting. { grease = { id_bits, value_bits } } sends a reserved setting 31 * N + 33 with a random value. Known settings in the list also configure the connection |
control_grease_frame | A reserved frame { id_bits, max_len } sent on the control stream after SETTINGS |
pseudo_order | The pseudo-header order, as in [h2]. The default is method, scheme, authority, path |
priority_update | Sends a PRIORITY_UPDATE frame with the request’s priority header value |
Leyline speaks QUICv1 and QUICv2 (RFC 9369). It sends its first Initial in
the chosen version of version_information and follows a server’s
compatible version negotiation (RFC 9368) to another version in available.
A transport parameter list that sends max_datagram_frame_size (32) turns on
QUIC DATAGRAM receipt. Leyline reads RESET_STREAM_AT as RESET_STREAM and
ignores ACK_FREQUENCY and IMMEDIATE_ACK, so a profile can advertise
reset_stream_at and min_ack_delay.
QPACK
The QPACK decoder keeps a dynamic table. The settings list sets the
advertised QPACK_MAX_TABLE_CAPACITY and QPACK_BLOCKED_STREAMS; a profile
without a settings list uses qpack_max_table_capacity,
qpack_blocked_streams, and max_field_section_size. The decoder applies
the peer’s encoder stream, holds a field section that waits for inserts (up
to the advertised blocked streams), and sends Section Acknowledgment, Insert
Count Increment, and Stream Cancellation. The encoder does not use the
dynamic table.
Proxies
HTTP/3 goes through a socks5:// or socks5h:// proxy with the socks
feature, which relays the QUIC datagrams with SOCKS5 UDP ASSOCIATE. An
http:// or https:// proxy cannot carry QUIC: under Race such a request
takes the Auto path, and Http3 with such a proxy fails with
Kind::Config. When the cause is a missing socks feature, the message says
so. See Proxies.
Next
Read TLS trust.
TLS trust
Leyline verifies every server certificate. All trust settings live in one
TlsTrustConfig that you pass to SessionBuilder::tls_trust: the roots, the
pins, the client certificate, and the TLS version floor. The browser profile
decides what the ClientHello looks like; trust settings decide which
certificates the session accepts.
Choose the roots
By default a session trusts the system trust store and the files named by
SSL_CERT_FILE and SSL_CERT_DIR when they are set. add_ca_file adds a
PEM file and add_ca_der adds the DER bytes of one certificate; both add to
the default roots. Turn a default source off with system_roots(false) or
env_roots(false). With both off, the session trusts only the roots you add.
#![allow(unused)]
fn main() {
use leyline::{Browser, Family, Session, TlsTrustConfig};
fn run() -> leyline::Result<()> {
let trust = TlsTrustConfig::new()
.env_roots(false)
.system_roots(false)
.add_ca_file("/etc/myorg/ca.pem");
let chrome = Session::builder()
.browser(Browser::default())
.tls_trust(trust.clone())
.build()?;
let firefox = Session::builder()
.browser(Browser::latest(Family::Firefox))
.tls_trust(trust)
.build()?;
let _ = (chrome, firefox);
Ok(())
}
}
TlsTrustConfig is a value: build it once and pass a clone to each builder.
tls_trust replaces the builder’s trust settings, so put every setting in
one config. A file or certificate that cannot be parsed fails with
Kind::Config.
Pin a certificate
add_pinned_leaf_sha256 takes the SHA-256 digest of the server’s leaf
certificate in DER form. The handshake succeeds only when the chain verifies
and the leaf matches a pin. A pin adds no root.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
let pin: [u8; 32] = [0; 32];
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.tls_trust(leyline::TlsTrustConfig::new().add_pinned_leaf_sha256(pin))
.build()?;
let _ = session;
Ok(())
}
}
Pins apply to every host the session contacts, so give a pinned host its own
session. Add a pin for each certificate you expect, including the next one
before a rotation. To read the digest of a live certificate, hash the
peer_cert_der field of Response::tls().
A saved TLS session resumes only in a session that loaded the same pins, roots, and client certificate, compared by content; any other session makes a full handshake.
A pin failure is a Kind::Tls error, and err.tls() returns the TlsError.
With ProtocolPolicy::Http3 it is a Kind::Http3 error with a message and
no source, so err.tls() returns None.
Present a client certificate
For mutual TLS, give client_identity a PEM certificate chain and its
private key.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.tls_trust(
leyline::TlsTrustConfig::new()
.client_identity("/etc/myorg/client.pem", "/etc/myorg/client.key"),
)
.build()?;
let _ = session;
Ok(())
}
}
A session with pins or a client certificate cannot use an https:// proxy.
A request through one fails with Kind::Proxy.
Set a TLS version floor
min_tls_version takes a TlsMinVersion: Tls10 (the default), Tls12,
or Tls13. The handshake minimum is the higher of the profile’s minimum and
the floor. A profile that declares no minimum starts at TLS 1.2.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.browser(leyline::Browser::Safari18)
.platform(leyline::Platform::MacOS)
.tls_trust(
leyline::TlsTrustConfig::new().min_tls_version(leyline::TlsMinVersion::Tls12),
)
.build()?;
let _ = session;
Ok(())
}
}
A floor above the profile’s minimum changes the ClientHello, so the session no longer matches the browser it impersonates:
- Some Safari and CFNetwork profiles declare a TLS 1.0 minimum. A
Tls12floor removes TLS 1.0 and 1.1 fromsupported_versions. - A
Tls13floor removes TLS 1.2 fromsupported_versionsin every profile, with the TLS 1.2 cipher suites and the extensions only TLS 1.2 and earlier use.
A server that cannot meet the floor fails the handshake with Kind::Tls;
an https:// proxy that cannot meet it fails with Kind::Proxy.
Response::tls() reports the negotiated version in version. HTTP/3 always
uses TLS 1.3, so the floor does not change a QUIC handshake.
Turn verification off
danger_accept_invalid_certs(true) accepts any certificate on TCP
connections (HTTP/1.1 and HTTP/2). Use it against a local test server only.
On those connections it also skips pin checks and turns off TLS session
resumption. It has no effect on HTTP/3: QUIC always verifies the certificate
and applies pins.
Next
Read Network.
Network
This page covers the layer under TLS: how a host name becomes an address, how Leyline picks between IPv4 and IPv6, which socket options it sets, and how the connection pool and the HSTS store work.
Map a host to an address
DnsConfig::resolve_host sends one host to a fixed list of addresses without
a DNS lookup. Leyline replaces the port of each address with the port of the
request URL, so the port you give is a placeholder. The host name still goes
into SNI and the Host header, so the server sees a normal request.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use leyline::DnsConfig;
let dns = DnsConfig::new().resolve_host(
"api.example.com",
[
"203.0.113.10:0".parse().unwrap(),
"203.0.113.11:0".parse().unwrap(),
],
);
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.dns(dns)
.build()?;
let _ = session;
Ok(())
}
}
Hosts without an override go to the resolver. A later call for the same host
replaces its list, and an empty list removes the override. Put every override
in one DnsConfig: a second dns call replaces the first.
Replace the resolver
The default is tls::SystemResolver. To use your own, implement
tls::Resolver and pass it to SessionBuilder::dns as an
Arc<dyn Resolver>. DnsConfig holds the resolver and the overrides as one
value, so several builders can share it.
Happy Eyeballs
Leyline connects to the addresses of a host IPv6 first, then alternating
between the families. Inside one family, the order stays as the resolver or
resolve_host gave it. Leyline starts one attempt, and starts the next when
resolve_delay passes with no connection, or at once when every running
attempt has failed. The first connection to complete wins.
This applies to every TCP connection, mapped hosts included. A direct HTTP/3 connection uses one address: the first IPv4 address, or the first address when the host has none.
tls::HappyEyeballsConfig | Default | Meaning |
|---|---|---|
resolve_delay | 250 ms | Wait before Leyline starts the next address |
attempt_limit | 8 | Most addresses tried for one connect |
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use std::time::Duration;
use leyline::SocketConfig;
use leyline::tls::HappyEyeballsConfig;
let he = HappyEyeballsConfig::new()
.resolve_delay(Duration::from_millis(100))
.attempt_limit(4);
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.socket(SocketConfig::new().happy_eyeballs(he))
.build()?;
let _ = session;
Ok(())
}
}
Happy Eyeballs cannot be turned off; attempt_limit(1) tries one address
only. happy_eyeballs(None) keeps the defaults.
Socket options
SocketConfig sets options on each TCP socket before it connects.
SessionBuilder::socket replaces the whole value, so start from
SocketConfig::new(), which carries the defaults.
| Method | Default | Effect |
|---|---|---|
tcp_nodelay | unset | TCP_NODELAY |
tcp_keepalive | 60 s | Idle time before the first keepalive probe |
tcp_keepalive_interval | 30 s | Time between probes |
tcp_keepalive_retries | 3 | Failed probes before the socket closes |
tcp_user_timeout | unset | TCP_USER_TIMEOUT: how long sent data may stay unacknowledged (Linux, Android) |
send_buffer_size, recv_buffer_size | unset | SO_SNDBUF, SO_RCVBUF |
local_address | unset | Source IP to bind |
local_ipv4, local_ipv6 | unset | Source IP to bind for that address family |
strict | false | Fail when the platform does not support an option |
happy_eyeballs | see above | Happy Eyeballs settings |
The timing and size setters take a value or None, which clears it. Leyline
sets TCP keepalive when any of its three values is set; to turn it off, pass
None to all three.
#![allow(unused)]
fn main() {
fn run() -> leyline::Result<()> {
use std::time::Duration;
use leyline::SocketConfig;
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.socket(
SocketConfig::new()
.tcp_nodelay(true)
.tcp_user_timeout(Duration::from_secs(20))
.tcp_keepalive(Duration::from_secs(30))
.tcp_keepalive_interval(Duration::from_secs(10)),
)
.build()?;
let _ = session;
Ok(())
}
}
Set tcp_user_timeout when a pooled connection can die without a FIN or RST,
for example when a proxy or NAT drops its state. Without it, the kernel
retransmits for many minutes and the request waits for its total timeout.
With it, the kernel closes the socket, the request fails, and a retry policy
can use a fresh connection.
Where an option is unsupported, Leyline logs one warning for it on the
leyline::socket target and continues. With strict(true), the connect fails
with an io::ErrorKind::Unsupported error. tcp_keepalive_retries falls back
to the operating system default where it cannot be set.
SocketConfig is separate from TcpProfile, which sets the values of the
JA4T fingerprint and comes from the session platform. See
Fingerprints.
Connection pool
Each session has a connection pool. PoolConfig sets its limits, and every
limit applies to one pool.
| Setting | Default | Effect |
|---|---|---|
max_connections | 2048 | Pool entries. The least recently used entry leaves first |
max_h1_conns_per_host | 256 | HTTP/1.1 connections per host, port, scheme, proxy, and transport. A request over the limit waits for a free slot |
idle_timeout | 300 s | An idle connection closes after this time |
keepalive | true | Keep connections open for reuse |
h2_ping_after_idle | 10 s | An idle HTTP/2 connection gets a PING after this time. None turns it off |
h2_ping_timeout | 2 s | The wait for the answer to that PING |
#![allow(unused)]
fn main() {
use leyline::PoolConfig;
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.pool(PoolConfig::new().max_connections(512).max_h1_conns_per_host(8))
.build()?;
let _ = session;
Ok(())
}
}
Clones and every derived session share the pool, except fresh_pool, which
starts a new, empty one. A with_identity session keeps its connections in a
partition of its own, inside the shared pool and its limits. See
Sessions.
When every stream slot of an HTTP/2 connection is in use, one request waits inside the connection and later requests wait in a queue of at most 1024. The request’s timeout covers the wait, and a request dropped while it waits is never sent.
One handshake per connection
An HTTPS connection always sends the profile’s ClientHello, with its ALPN
list, for example h2, http/1.1, and its ALPS extension. The server’s ALPN
choice decides HTTP/2 or HTTP/1.1 on that same connection. Leyline never
dials again to change the protocol, so a streaming request body to an
HTTP/1.1-only origin works on the first request.
When an origin negotiates HTTP/1.1, the pool remembers it for 10 minutes, for
at most 1024 origins. The next connection still sends the full ClientHello,
and if the origin then answers h2, Leyline uses HTTP/2 and forgets the
entry.
ProtocolPolicy::Http2 sends every request over HTTP/2 with the same
ClientHello; when the server picks another protocol, the request fails with
Kind::Http2. ProtocolPolicy::Http1 and a WebSocket over HTTP/1.1 offer
http/1.1 only.
Limit requests per host
HostLimits limits the requests to each origin, apart from the pool limits,
which count connections. See Limit each host.
HSTS
Leyline keeps an HSTS store per session (RFC 6797). It records the first
Strict-Transport-Security header of each https response. The header needs
max-age; includeSubDomains also covers the subdomains, and max-age=0
removes the host. IP addresses are ignored. A session that turns off
certificate verification records nothing, because it cannot trust the
header.
A later http:// request or redirect to a recorded host, or a covered
subdomain, goes to https instead, and an explicit port 80 becomes 443.
WebSocket requests are not upgraded.
Derived sessions, with_identity included, share the store; only
fresh_pool starts with an empty one. Session::state() saves the store,
and SessionState::restore_into loads it. See
Accounts.
Next
Read Logging and tracing. Timeouts are covered in Retries and timeouts.
Logging and tracing
Leyline logs through the tracing crate and
installs no subscriber. Your application installs one, for example
tracing_subscriber::fmt, and filters by target. For one event per request,
or counters, give the session a Trace hook.
Targets
Every target starts with leyline.
| Target | Events |
|---|---|
leyline::session | Session setup, for example the notice that a session defaults to Windows |
leyline::env_proxy::cgi | An HTTP_PROXY value that the CGI guard ignored |
leyline::pool | Connection pool events |
leyline::socket | A SocketConfig option that the platform does not support |
leyline::tcp | A TcpProfile socket option that the operating system rejected, once per option |
leyline::tls::trust | Loading and checking the trust store |
leyline::h2, leyline::h2::flood_guard | HTTP/2 connection events and flood protection |
leyline::quic | HTTP/3 and QUIC events |
leyline::profile | Profile load warnings, for example a missing captured_against |
leyline::digest | Digest authentication |
leyline::trace | TracingTrace: one summary per request, the end of each streamed body, and the request phases |
With a subscriber that reads RUST_LOG, this filter shows one line per
request and the warnings from the rest of the crate. Use
leyline::trace=debug to add the request phases.
RUST_LOG=leyline=warn,leyline::trace=info cargo run
Log each request
leyline::trace::TracingTrace writes one INFO event per request on the
leyline::trace target, with the message request, and each phase as a
DEBUG event: DNS, connect, TLS, send, response head, and completion.
#![allow(unused)]
fn main() {
use leyline::trace::TracingTrace;
use leyline::{Browser, Session};
async fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.trace(TracingTrace)
.build()?;
let resp = session
.get("https://shop.example/item/1")
.tag("listing")
.send()
.await?;
println!("{} via {}", resp.status(), resp.proxy().unwrap_or("direct"));
Ok(())
}
}
The INFO event has the fields id, method, url, redirects, status,
version, attempts, elapsed_ms, streamed, tag, proxy, browser,
and outcome. tag, proxy, and browser appear only when they have a
value. A failed request adds kind and error. The URL is redacted: the
password, or a user name without a password, becomes ***, the whole query
becomes ***, and the fragment is removed.
INFO leyline::trace: request id=7 method="GET" url=https://***@example.com/a?*** redirects=1 status=200 version=Http2 attempts=1 elapsed_ms=84 streamed=false outcome="ok"
INFO leyline::trace: request id=8 method="GET" url=https://shop.example/item/1 redirects=0 status=200 version=Http2 attempts=2 elapsed_ms=310 streamed=false tag="listing" proxy="http://user:***@proxy-a.example:8080/" browser=Chrome154 outcome="ok"
RequestBuilder::tag(tag) names a request, for example by job, account, or
identity. The proxy field holds the proxy the request used, from the
request, the session, or a ProxyPool, with the password replaced by ***.
Response::proxy() returns the same value for your own log lines.
One event per request
Trace::summary receives a trace::Summary once per request, after all
redirects and retries.
| Field | Value |
|---|---|
id | The request id, the same as in the phase events |
method | The request method |
url | The final URL. On an error, the parsed original URL, or None when it does not parse |
original_url | The URL as the caller gave it |
redirects | The number of redirects followed |
status | The final status, None on an error |
version | The protocol of the final response, None on an error |
attempts | The number of attempts, retries included |
elapsed | The time from the start of the request |
outcome | Ok(()), or Err(&Error) |
streamed | true for a .stream() request |
tag | The tag from RequestBuilder::tag, or None |
proxy | The proxy the request used, redacted, or None |
browser | The Browser of the session that sent the request, or None on a plain session. With ProxyPool::identified, the browser of the proxy’s identity session |
For a .stream() request, summary fires at the response head, so it does
not cover the body read. It does not fire for a dropped request future. The
done phase event fires once per attempt, so a request with two retries
gives three done events and one summary.
url and original_url hold the raw URLs, with credentials and query
values. Redact them with leyline::redact_url, which TracingTrace uses,
before you log them.
#![allow(unused)]
fn main() {
use leyline::trace::{Summary, Trace};
struct RequestLog;
impl Trace for RequestLog {
fn summary(&self, ev: &Summary<'_>) {
let host = ev.url.and_then(|url| url.host_str()).unwrap_or("-");
let status = ev.status.map_or(0, |status| status.as_u16());
let tag = ev.tag.unwrap_or("-");
let proxy = ev.proxy.as_deref().unwrap_or("direct");
println!(
"{} {} {} attempts={} {} ms tag={} proxy={}",
ev.method,
host,
status,
ev.attempts,
ev.elapsed.as_millis(),
tag,
proxy
);
}
}
fn run() -> leyline::Result<()> {
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.trace(RequestLog)
.build()?;
let _ = session;
Ok(())
}
}
A session takes one hook. trace::Fanout::new().with(a).with(b) sends each
event to several, and trace::Metrics counts them. See
Count requests and watch the queues.
Streamed bodies
Trace::body receives a trace::BodyEnd once for each .stream() response,
when its body ends. A buffered response has no body event, because its
summary already covers the body read.
| Field | Value |
|---|---|
id | The id of the request’s Summary |
bytes | The body bytes that the caller received, after decompression |
elapsed | The time from the start of the request, not from the head |
outcome | BodyOutcome::Complete, Failed(&io::Error) when a read failed, or Dropped when the caller dropped the response or body stream before the end |
BodyOutcome is #[non_exhaustive], so a match needs a _ arm.
TracingTrace writes the event at INFO with the message body and the fields
id, bytes, elapsed_ms, and outcome (complete, error, or
dropped). An error adds kind and error.
INFO leyline::trace: body id=9 bytes=1048576 elapsed_ms=930 outcome="complete"
To write one line per request, keep the head of each streamed request by
id and write the line when its BodyEnd arrives. Write other requests from
summary:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::sync::Mutex;
use leyline::trace::{BodyEnd, BodyOutcome, Summary, Trace};
#[derive(Default)]
struct OneLine {
heads: Mutex<HashMap<u64, String>>,
}
impl Trace for OneLine {
fn summary(&self, ev: &Summary<'_>) {
let head = format!("{} {:?} {:?}", ev.method, ev.status, ev.browser);
if ev.streamed && ev.outcome.is_ok() {
self.heads.lock().unwrap().insert(ev.id, head);
} else {
println!("{} {head} {} ms", ev.id, ev.elapsed.as_millis());
}
}
fn body(&self, ev: &BodyEnd<'_>) {
let head = self.heads.lock().unwrap().remove(&ev.id).unwrap_or_default();
let outcome = match ev.outcome {
BodyOutcome::Complete => "complete".to_owned(),
BodyOutcome::Failed(err) => format!("failed: {err}"),
BodyOutcome::Dropped => "dropped".to_owned(),
_ => "other".to_owned(),
};
println!("{} {head} {} bytes {} ms {outcome}", ev.id, ev.bytes, ev.elapsed.as_millis());
}
}
let _ = OneLine::default();
}
Phase events
To handle the phases in your own code, implement the other Trace methods.
The sent event carries the method and the path with the query. The head
event carries the status and the response headers as received, before
decompression, as &http::HeaderMap. See
Trace the request lifecycle.
Next
Read Service integration to use a session inside a web service.
Service integration
This page shows how to run Leyline inside a web service or a long-running worker: share sessions, relay upstream bodies, answer with the right status, use the Tower adapter, and shut down cleanly.
Share one session
Build each session once at startup, keep it in the state your framework
gives to each handler, and clone it per handler. Building resolves the
profile, the TLS context, and the trust store; a clone is an Arc clone that
shares the pool, the cookie jar, and the identity, so it keeps the warm
connections.
Session, Response, BodyStream, and Error are Send + Sync. Body
and RequestBuilder are Send. leyline::http is the http 1.x crate, so
its StatusCode, HeaderMap, and Request pass to and from axum 0.8 and
hyper 1 with no conversion.
A service often needs a plain session for its own APIs and a browser session for sites that check the fingerprint:
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::trace::TracingTrace;
use leyline::{Browser, Platform, Session};
#[derive(Clone)]
struct AppState {
plain: Session,
browser: Session,
}
impl AppState {
fn new() -> leyline::Result<Self> {
let plain = Session::builder()
.base_url("https://api.example/v1/")
.timeout(Duration::from_secs(30))
.trace(TracingTrace)
.build()?;
let browser = Session::builder()
.browser(Browser::default())
.platform(Platform::Windows)
.trace(TracingTrace)
.build()?;
Ok(Self { plain, browser })
}
}
fn run() -> leyline::Result<()> {
let state = AppState::new()?;
let for_handler = state.clone();
let _ = for_handler;
Ok(())
}
}
See Sessions for what each kind sends. A buffered body is
capped at 100 MiB by default; set CompressionConfig::max_body_size(bytes) to
what your service accepts from an upstream. See
Responses.
Each request can override the session timeout and proxy with .timeout(..)
and .proxy(..). See
Retries and timeouts and
Proxies.
Relay an upstream body
Send with .stream() so that send() returns at the response head, then
pass the body on as a BodyStream, a
Stream<Item = Result<Bytes, std::io::Error>> that most frameworks accept
as a response body. Forward the headers from relay_headers, which drops
the hop-by-hop headers of RFC 9110, section 7.6.1.
| Body | Headers to forward |
|---|---|
into_stream(): the bytes as sent, still encoded, no size cap | relay_headers(RelayBody::AsReceived) keeps content-encoding and content-length |
into_decoded_stream(limit): decoded, capped at limit or max_body_size | relay_headers(RelayBody::Decoded) drops both when the response was encoded |
Once your service sends its status and headers, a body error can only abort
the body, and the client sees a truncated response. To answer 502 for an
upstream body over your limit, check content_length() before you send the
head, and relay with into_decoded_stream(limit) so that a body with no
declared length still stops at the limit:
#![allow(unused)]
fn main() {
use leyline::{RelayBody, Session};
const RELAY_LIMIT: u64 = 8 * 1024 * 1024;
async fn run(session: &Session) -> leyline::Result<()> {
let resp = session.get("https://upstream.example/file").stream().send().await?;
if resp.content_length().is_some_and(|len| len > RELAY_LIMIT) {
println!("answer 502");
return Ok(());
}
let status = resp.status();
let headers = resp.relay_headers(RelayBody::Decoded);
let body = resp.into_decoded_stream(Some(RELAY_LIMIT))?;
drop((status, headers, body));
Ok(())
}
}
After the head, read bounds each chunk and TimeoutConfig::body bounds
the whole body. When your client goes away, drop the stream: Leyline resets
the upstream stream or closes the HTTP/1.1 connection. See
Streaming and Cancellation.
Map errors to HTTP statuses
ErrorCategory::gateway_status() gives the status a gateway answers for an
error: None for Status (pass the upstream status through), 504 for
Timeout, 500 for Url, Config, and Request, and 502 for the rest.
#![allow(unused)]
fn main() {
use leyline::http::StatusCode;
fn status_for(err: &leyline::Error) -> StatusCode {
err.category()
.gateway_status()
.or_else(|| err.status())
.unwrap_or(StatusCode::BAD_GATEWAY)
}
fn error_body(err: &leyline::Error) -> String {
format!(r#"{{"error":"{}"}}"#, err.category().as_str())
}
let _ = (status_for, error_body);
}
ErrorCategory::as_str() is a stable snake_case label that leaks no
upstream detail. Log the failed URL with leyline::redact_url, which masks
the password and the query and drops the fragment. See
Map errors to HTTP statuses for
the reasons behind each status.
Log one line per request
TracingTrace logs one INFO event per request on the leyline::trace
target, after redirects and retries:
INFO leyline::trace: request id=7 method="GET" url=https://***@example.com/a?*** redirects=1 status=200 version=Http2 attempts=1 elapsed_ms=84 streamed=false outcome="ok"
For a .stream() request the event fires at the head, so elapsed_ms
leaves out the body. To feed your own metrics, implement Trace::summary;
wrap the hook in an Arc to share it between sessions. See
Logging and tracing.
Use the Tower adapter
The tower feature adds LeylineService, a
tower_service::Service<http::Request<leyline::Body>> whose response is
http::Response<leyline::Body> and whose error is leyline::Error. It sends
each request through the session, with its cookies, redirects, retries, and
presets, and returns at the response head.
- The body streams decoded and capped at
max_body_size. When Leyline decodes it, the response has nocontent-encodingand nocontent-length. Trailers are not exposed. - Every other upstream header stays, hop-by-hop headers included. Filter
them with
leyline::relay_headers(&headers, RelayBody::AsReceived): the headers already describe the decoded body. - Request extensions set per-request policy:
Preset,TimeoutConfig,RetryPolicy,RedirectPolicy, andProxyConfig. AProxyConfigbypasses the session’sProxyPool.Session::executereads the same extensions and returns a bufferedResponse. - The response carries the final
leyline::Url, theHttpVersion, and theResponseTimingas extensions, and setsversion().
#![allow(unused)]
fn main() {
#[cfg(feature = "tower")]
async fn run() -> leyline::Result<()> {
use std::time::Duration;
use futures_util::StreamExt;
use leyline::http::Request;
use leyline::{Body, HttpVersion, LeylineService, Session, TimeoutConfig, Url};
use tower_service::Service;
let mut svc = LeylineService::new(Session::new());
let mut req = Request::builder()
.uri("https://example.com/")
.body(Body::default())
.expect("valid request");
req.extensions_mut()
.insert(TimeoutConfig::new().total(Duration::from_secs(10)));
let resp = svc.call(req).await?;
let final_url = resp.extensions().get::<Url>().map(Url::as_str);
let version = resp.extensions().get::<HttpVersion>();
println!("{} {:?} {:?}", resp.status(), final_url, version);
let mut body = std::pin::pin!(resp.into_body());
let mut size = 0usize;
while let Some(chunk) = body.next().await {
size += chunk?.len();
}
println!("{size} bytes");
Ok(())
}
}
Enable it with features = ["tower"]; the example also needs
tower-service = "0.3". The response body is a leyline::Body, a
Stream<Item = std::io::Result<Bytes>>, so a framework that takes a byte
stream, such as axum with Body::from_stream, forwards it as it is. To
forward the response, keep the status and the body and filter the headers:
#![allow(unused)]
fn main() {
#[cfg(feature = "tower")]
mod example {
use leyline::http::{Request, Response};
use leyline::{Body, LeylineService, RelayBody};
use tower_service::Service;
pub async fn relay(
mut svc: LeylineService,
req: Request<Body>,
) -> leyline::Result<Response<Body>> {
let resp = svc.call(req).await?;
let (mut parts, body) = resp.into_parts();
parts.headers = leyline::relay_headers(&parts.headers, RelayBody::AsReceived);
Ok(Response::from_parts(parts, body))
}
}
}
Limit and time out upstream calls
LeylineService is Clone, and a clone shares the session. Build it once,
store it in the application state, and clone it in each handler. To limit
the upstream calls in flight, share one tokio::sync::Semaphore in the same
state. To bound one call, put a TimeoutConfig in the request extensions; a
timeout is then a leyline::Error like any other, and category() maps it
to a status:
#![allow(unused)]
fn main() {
#[cfg(feature = "tower")]
mod example {
use std::sync::Arc;
use std::time::Duration;
use leyline::http::{Request, StatusCode};
use leyline::{Body, LeylineService, Session, TimeoutConfig};
use tokio::sync::Semaphore;
use tower_service::Service;
const MAX_IN_FLIGHT: usize = 32;
const UPSTREAM_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone)]
pub struct AppState {
upstream: LeylineService,
slots: Arc<Semaphore>,
}
impl AppState {
pub fn new(session: Session) -> Self {
Self {
upstream: LeylineService::new(session),
slots: Arc::new(Semaphore::new(MAX_IN_FLIGHT)),
}
}
}
pub async fn handler(state: AppState, url: String) -> StatusCode {
let Ok(mut request) = Request::builder().uri(url).body(Body::default()) else {
return StatusCode::BAD_REQUEST;
};
request
.extensions_mut()
.insert(TimeoutConfig::new().total(UPSTREAM_TIMEOUT));
let Ok(_slot) = state.slots.acquire().await else {
return StatusCode::SERVICE_UNAVAILABLE;
};
let mut upstream = state.upstream.clone();
match upstream.call(request).await {
Ok(response) => response.status(),
Err(err) => err
.category()
.gateway_status()
.or_else(|| err.status())
.unwrap_or(StatusCode::BAD_GATEWAY),
}
}
}
}
LeylineService is a tower_service::Service, so tower layers can wrap it
too. A layer returns tower::BoxError and can wrap the leyline::Error in
its own error; Error::find walks the source() chain and returns the first
leyline::Error. See Map errors to HTTP statuses.
Call a service on localhost
localhost usually resolves to ::1 and 127.0.0.1. Happy Eyeballs tries
IPv6 first and IPv4 after resolve_delay (250 ms by default) or when the
IPv6 attempt fails. For a service that listens only on IPv4, put
127.0.0.1 in the URL. See Network.
Shut down
Call Session::shutdown(), then drop the session. Requests in flight and
new requests on the session, its clones, and its derived sessions fail with
Kind::Request, and a streamed body fails on its next read.
Error::is_shut_down() identifies this error, so a handler can answer 503.
The session holds nothing to flush, and the pooled connections stay open.
#![allow(unused)]
fn main() {
async fn run(session: leyline::Session) {
let worker = session.clone();
let task = tokio::spawn(async move { worker.get("https://api.example/jobs").await });
session.shutdown();
match task.await {
Ok(Err(err)) if err.is_shut_down() => println!("stopped"),
Ok(result) => println!("finished: {}", result.is_ok()),
Err(join) => println!("task failed: {join}"),
}
drop(session);
}
}
Session::shutdown() is a plain call. The autosave handles of a cookie jar
and a device are separate: JarAutosave::shutdown() and
DeviceAutosave::shutdown() are async, save the last changes, and return
the result of that save. Stop the server first so that the open requests
finish, then stop the sessions, then await each autosave handle:
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::{Browser, Session};
async fn run() -> leyline::Result<()> {
let session = Session::browser(Browser::default());
let cookies = session.cookies().autosave("cookies.json", Duration::from_secs(2));
let worker = session.clone();
let jobs = tokio::spawn(async move { worker.get("https://api.example/jobs").await });
tokio::signal::ctrl_c().await?;
session.shutdown();
drop(jobs.await);
cookies.shutdown().await?;
Ok(())
}
}
For a device, Device::autosave returns a DeviceAutosave; await its
shutdown() in the same place. See Accounts.
Test the service
leyline::testing::TestServer stands in for the upstream. See
Testing, which also shows it standing in for a forward proxy.
Testing
Test code that uses Leyline against a local server. Leyline needs no mock
layer: point a session at 127.0.0.1 and check what the server received.
Choose the session for a test
| Session | Use it when |
|---|---|
Plain, Session::new() | The test checks your own logic: URLs, methods, bodies, headers, error handling. Its default headers are fixed: Host, user-agent: leyline/<version>, accept: */*, accept-encoding, and Connection on HTTP/1.1, plus accept-language only after .languages(..). |
Browser, Session::browser(..) | The test checks what the production session sends. Headers and their order come from the profile data, so an exact header assertion can change with the profile. |
Build the session as in production and change only the trust settings and
the base URL. A browser session sends HTTP/1.1 to an http:// URL and
HTTP/3 only to an origin that advertised it in Alt-Svc, so a local server
sees HTTP/1.1 or HTTP/2.
Add the test server
The test-util feature adds leyline::testing. List the crate again under
[dev-dependencies] with the feature; Cargo unifies the features, so test
builds get test-util and release builds do not:
[dependencies]
leyline-http = "0.1"
[dev-dependencies]
leyline-http = { version = "0.1", features = ["test-util"] }
| Item | Does |
|---|---|
TestServer::http(handler) | Starts an HTTP/1.1 server on 127.0.0.1 on a free port |
TestServer::https(handler) | The same over TLS with a private CA, http/1.1 only |
TestServer::http_on(listener, handler) | An HTTP/1.1 server on a std::net::TcpListener you bound, for a fixed port or address. Call it inside a Tokio runtime |
queue([..]) | A handler that answers with each TestResponse in turn, then 503 |
url(path) | http://127.0.0.1:<port>/path, or https://. url("") is the origin with a trailing /, for a base_url |
addr() | The bound address |
trust() | A TlsTrustConfig that trusts only the server’s CA |
ca_der() | The CA certificate as DER, for an HTTPS server |
requests().await | Every request so far, in order |
next_request().await | The next request, when it arrives |
shutdown().await | Stops the server; dropping it does too |
A handler is any Fn(&RecordedRequest) -> TestResponse + Send + Sync + 'static. It runs on a blocking thread, so a handler that blocks delays only
its own response.
RecordedRequest | Holds |
|---|---|
method, target, body | The method, the request target, and the body |
headers | Every header in wire order, with the case the client sent |
request_line | The first line, such as GET /items HTTP/1.1 |
raw | The request head as received: request line, headers, and the empty line, byte for byte |
text() | raw as text, with invalid UTF-8 replaced |
header(name) | The first value of a header. The name match ignores case |
header_values(name) | Every value of a header, in wire order |
header_count(name) | How many times a header was sent |
Use raw or header_count to check what a parsed map hides: header case,
order, and duplicates.
TestResponse | Does |
|---|---|
new(status) | A response with this status and an empty body |
header(name, value) | Adds a header |
body(bytes) | Sets the body, sent with content-length |
close() | Adds connection: close. The server closes the connection after the response |
delay(duration) | Waits before the status line and headers |
chunks(parts, pause) | Sends a chunked body with pause between parts. It replaces body, skips empty parts, and adds transfer-encoding: chunked unless you set content-length or transfer-encoding |
See the leyline::testing reference
for every item.
Assert what was sent
Put the body of run in a #[tokio::test] function. Use server.url(..),
which holds 127.0.0.1: with localhost, Leyline tries IPv6 first, where
the server does not listen.
#![allow(unused)]
fn main() {
#[cfg(feature = "test-util")]
async fn run() -> leyline::Result<()> {
use leyline::testing::{TestResponse, TestServer};
let server = TestServer::http(|request| {
let id = request.header("x-request-id").unwrap_or("none").to_owned();
TestResponse::new(201).header("x-echo-id", id)
})
.await?;
let session = leyline::Session::new();
let resp = session
.post(server.url("/items"))
.header("x-request-id", "test-1")
.body("hello")
.send()
.await?;
assert_eq!(resp.status(), 201);
assert_eq!(resp.header("x-echo-id"), Some("test-1"));
let sent = server.requests().await;
assert_eq!(sent.len(), 1);
assert_eq!(sent[0].method, "POST");
assert_eq!(sent[0].target, "/items");
assert_eq!(sent[0].body, b"hello");
Ok(())
}
}
The server sees the bytes that reached it, which is the strongest check of
what Leyline sent. To see what a session prepared without a server, build it
with .audit(true) and read Response::request_headers(); see
Fingerprints.
Answer with a sequence
queue answers each request with the next response. Use it for retries and
error handling:
#![allow(unused)]
fn main() {
#[cfg(feature = "test-util")]
async fn run() -> leyline::Result<()> {
use leyline::RetryPolicy;
use leyline::testing::{TestResponse, TestServer, queue};
let server = TestServer::http(queue([
TestResponse::new(503),
TestResponse::new(200).body("ok"),
]))
.await?;
let session = leyline::Session::builder()
.retry(RetryPolicy::transient())
.build()?;
let resp = session.get(server.url("/flaky")).send().await?;
assert_eq!(resp.status(), 200);
assert_eq!(resp.attempts(), 2);
Ok(())
}
}
Test timeouts
delay holds back the head, so it trips total and response_header.
chunks holds back the body, so it trips read on a .stream() response.
#![allow(unused)]
fn main() {
#[cfg(feature = "test-util")]
async fn run() -> leyline::Result<()> {
use std::time::Duration;
use leyline::TimeoutConfig;
use leyline::testing::{TestResponse, TestServer};
const LIMIT: Duration = Duration::from_millis(100);
const STALL: Duration = Duration::from_millis(500);
let server = TestServer::http(|request| match request.target.as_str() {
"/slow-head" => TestResponse::new(200).delay(STALL),
_ => TestResponse::new(200).chunks(["first", "second"], STALL),
})
.await?;
let session = leyline::Session::new();
let err = session
.get(server.url("/slow-head"))
.timeout(LIMIT)
.await
.expect_err("the head arrives after the limit");
assert!(err.is_timeout());
let resp = session
.get(server.url("/slow-body"))
.stream()
.timeout(TimeoutConfig::new().read(LIMIT))
.await?;
let err = resp.text().await.expect_err("the second chunk arrives after the limit");
assert!(err.is_timeout());
Ok(())
}
}
Test over TLS
TestServer::https makes a private CA and a leaf certificate for
localhost and 127.0.0.1. trust() trusts that CA only, with the system
and environment roots off. The server negotiates HTTP/1.1, so a browser
session uses HTTP/1.1 on it.
#![allow(unused)]
fn main() {
#[cfg(feature = "test-util")]
async fn run() -> leyline::Result<()> {
use leyline::testing::{TestResponse, TestServer};
let server = TestServer::https(|_| TestResponse::new(200).body("secure")).await?;
let session = leyline::Session::builder()
.browser(leyline::Browser::default())
.tls_trust(server.trust())
.build()?;
let resp = session.get(server.url("/health")).send().await?;
assert_eq!(resp.text().await?, "secure");
Ok(())
}
}
Use your own TLS server
For a server of your own, for example one that speaks HTTP/2, generate a
test CA and a leaf certificate (the rcgen crate does this) and trust only
that CA:
#![allow(unused)]
fn main() {
use leyline::{Session, TlsTrustConfig};
async fn run(ca_der: Vec<u8>, port: u16) -> leyline::Result<()> {
let trust = TlsTrustConfig::new()
.system_roots(false)
.env_roots(false)
.add_ca_der(ca_der);
let session = Session::builder().tls_trust(trust).build()?;
let resp = session
.get(format!("https://127.0.0.1:{port}/health"))
.send()
.await?;
assert_eq!(resp.status(), 200);
Ok(())
}
}
- Put
127.0.0.1as an IP address, orlocalhostas a DNS name, in the leaf’s subject alternative names. To use a host name and stay on IPv4, map it withDnsConfig::resolve_host; see Network. - Give the CA a subject name of its own, for example
CN=Test CA. When the leaf’s issuer equals its subject, BoringSSL reads the leaf as self-signed and the handshake fails with verify code 18,self signed certificate. Certificate generators often give both the same default name.
See TLS trust.
Stand in for a forward proxy
TestServer can act as a forward proxy for http:// URLs. Set its URL as
the proxy: the session sends the request in absolute form, so
RecordedRequest::target holds the full upstream URL, and the upstream host
needs no DNS entry. An https:// URL needs a CONNECT tunnel, which
TestServer does not open.
#![allow(unused)]
fn main() {
#[cfg(feature = "test-util")]
async fn run() -> leyline::Result<()> {
use leyline::Session;
use leyline::testing::{TestResponse, TestServer};
let proxy = TestServer::http(|_| TestResponse::new(200)).await?;
let session = Session::builder().proxy(proxy.url("")).build()?;
session.get("http://upstream.test/path").await?;
let seen = proxy.requests().await;
assert_eq!(seen[0].target, "http://upstream.test/path");
Ok(())
}
}
Observe requests with a trace hook
A Trace hook sees every request, once, after redirects and retries.
Collect the summaries to check the method, the status, and the attempt
count, for example to prove that a retry ran:
#![allow(unused)]
fn main() {
use std::sync::{Arc, Mutex};
use leyline::trace::{Summary, Trace};
#[derive(Default)]
struct Recorder {
seen: Mutex<Vec<(String, Option<u16>, u32)>>,
}
impl Trace for Recorder {
fn summary(&self, ev: &Summary<'_>) {
let status = ev.status.map(|s| s.as_u16());
if let Ok(mut seen) = self.seen.lock() {
seen.push((ev.method.to_owned(), status, ev.attempts));
}
}
}
async fn run(addr: std::net::SocketAddr) -> Result<(), Box<dyn std::error::Error>> {
let recorder = Arc::new(Recorder::default());
let session = leyline::Session::builder()
.trace(Arc::clone(&recorder))
.build()?;
let resp = session.get(format!("http://{addr}/")).send().await?;
assert_eq!(resp.status(), 200);
let seen = recorder.seen.lock().map_err(|_| "poisoned")?;
assert_eq!(seen.len(), 1);
assert_eq!(seen[0].0, "GET");
Ok(())
}
}
See Logging and tracing.
Next
Read Fingerprints to see what a browser session sends on each layer.
Fingerprints
A browser profile is a recorded description of how one browser build looks on the wire. Leyline replays it across four layers. This page covers what a profile pins, how to audit and check a session’s fingerprint, which headers are safe to override, and how to mix and rotate identities.
What a profile pins
| Layer | Holds |
|---|---|
TLS (TlsProfile) | The cipher list, supported groups, signature algorithms, certificate compression, and extension behavior: GREASE, ECH GREASE, extension permutation or a fixed order, ALPS, record size limit, delegated credentials, OCSP stapling, signed certificate timestamps, session tickets, pre-shared key, padding, trust anchor requests, and the minimum TLS version |
HTTP/2 (H2Profile) | The SETTINGS values and their order, the pseudo-header order, the default priority frame, and per-platform overrides |
| Headers | The User-Agent and sec-ch-ua of each platform block. The header style in profiles/headers.toml and the Preset decide the other headers and their order. See Requests |
TCP (TcpProfile) | The socket options that shape the SYN: TTL, MSS, window size, window scale, the don’t-fragment bit, and TCP_NODELAY |
The session takes its TcpProfile from profiles/platforms.toml through
Platform::tcp_profile; SessionBuilder::tcp_profile overrides it. Each
OS applies what it can:
| OS | Applies |
|---|---|
| Linux | TTL, MSS, don’t-fragment, and a TCP_WINDOW_CLAMP derived from the window fields |
| macOS | TTL, MSS, and don’t-fragment |
| Windows | TTL and don’t-fragment |
Every OS sets TCP_NODELAY when the profile asks for it. The SYN option
order in options is chosen by the kernel and cannot be set from a socket,
so only the audit’s JA4T reads it. platforms.toml has rows for Windows,
macOS, Linux, and iOS. Android has none, so its TcpProfile is empty and the
kernel defaults apply.
Audit a session
Build the session with .audit(true), then read Response::audit().
Without the flag, audit() returns None.
#![allow(unused)]
fn main() {
use leyline::{Browser, Platform, Session};
async fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::Chrome152)
.platform(Platform::Linux)
.audit(true)
.build()?;
let resp = session.get("https://example.com/").await?;
if let Some(a) = resp.audit() {
println!("JA4 {} JA4H {} H2 {}", a.ja4, a.ja4h, a.h2_fingerprint);
}
Ok(())
}
}
AuditData field | What it is |
|---|---|
ja3 | JA3, the MD5 form. It hashes the extensions in the order sent |
ja4 | JA4. It sorts the extensions before it hashes them |
ja4t | JA4T from the session’s TcpProfile values, including fields the running OS does not apply |
ja4h | JA4H over the request line and headers of this response’s request. Computed on the first audit() call and cached |
h2_fingerprint | The Akamai HTTP/2 fingerprint |
permutes_extensions | true when the profile shuffles the TLS extension order on each connection, as Chrome does |
request_headers | The headers the session prepared for the request, in order, before the transport sent them |
The connection fields are computed once per session from the profile. They report what Leyline built; a packet capture shows what the connection sent, and the kernel decides what the SYN carries.
Check a fingerprint with an echo service
A TLS echo service answers with the fingerprints of the connection it
received. AuditData::compare checks them against the audit of the same
session.
audit::Observed::from_json reads tls.ja4, tls.ja3_hash,
http2.akamai_fingerprint, and the echoed request headers from
http2.sent_frames[*].headers and http1.headers: the shape that
https://tls.peet.ws/api/all returns. A missing field stays None. For
another service, build it with
Observed::new().ja4(..).ja3(..).h2_fingerprint(..).header(name, value).
#![allow(unused)]
fn main() {
use leyline::audit::{FieldOutcome, Observed};
use leyline::{Browser, Session};
async fn run() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.audit(true)
.build()?;
let resp = session.get("https://tls.peet.ws/api/all").send().await?;
let Some(audit) = resp.audit().cloned() else {
return Ok(());
};
let observed = Observed::from_json(&resp.text().await?)?;
let report = audit.compare(&observed);
println!("match: {}", report.is_match());
if let FieldOutcome::Mismatch { expected, observed } = &report.ja4 {
println!("ja4 sent {expected}, service saw {observed}");
}
for header in &report.headers {
if let FieldOutcome::Mismatch { expected, observed } = &header.outcome {
println!("{} sent {expected}, service saw {observed}", header.name);
}
}
Ok(())
}
}
compare returns a FingerprintReport with a FieldOutcome for ja4,
ja3, h2_fingerprint, and header_order, and a HeaderOutcome
(lowercase name and outcome) in headers for each header the session
sent, pseudo-headers excluded. header_order compares the order of the
headers that both lists hold.
FieldOutcome | Meaning |
|---|---|
Match | The values are equal, ignoring ASCII case |
Mismatch { expected, observed } | The values differ, and they must not |
Informational { expected, observed } | The values differ, and they can |
NotReported | The service did not report the field, or reported no headers at all |
Absent { expected } | The session sent the header and the service reports headers, but not this one. It counts as a mismatch |
JA4 and the Akamai HTTP/2 fingerprint are fixed for every profile, so a
difference is a Mismatch. JA3 hashes the extensions in the order sent, and
Chrome shuffles that order, so for a profile with permutes_extensions a
JA3 difference is Informational; for a fixed-order profile such as Firefox
or Safari it is a Mismatch. A header that a proxy or middlebox rewrites
shows as a Mismatch. A proxy that ends TLS changes the result.
is_match() is true when nothing is a Mismatch or Absent;
FieldOutcome::is_mismatch() tests one field. Both types implement
Display: println!("{report}") prints one line per field and header.
Override headers safely
On a browser session, a header you set replaces the profile’s header of the same name in its position; Requests gives the placement of a new name.
| Header | Effect of an override |
|---|---|
accept-language | Safe. Prefer languages |
referer | Safe. RequestBuilder::initiator is better: it also sets origin and sec-fetch-site |
authorization, content-type, application headers such as x-api-key | Safe |
cookie | Safe. The jar adds no cookies to that request |
user-agent | Changes the fingerprint. The sec-ch-ua* hints still come from the profile, so the two can disagree |
sec-ch-ua*, accept-encoding, header order | Changes the fingerprint |
accept | Changes the fingerprint on a navigation. See Set accept on a script request |
sec-fetch-* | Your value replaces the computed one. Use preset and initiator instead |
content-length | Computed by Leyline. Your value is replaced |
host | Computed from the URL. Your value is kept on HTTP/1.1 and dropped on HTTP/2 and HTTP/3 |
Set accept on a script request
Keep the profile’s accept on open, follow, and the navigation presets.
A page script sets its own accept on a fetch() or an XHR, for example
application/json. The Xhr preset sends what a fetch() without an
explicit accept sends. When the site’s script sets one, set the same value:
the browser sends it too, so the fingerprint does not change.
#![allow(unused)]
fn main() {
async fn run(session: leyline::Session) -> leyline::Result<()> {
let tab = session.tab();
tab.open("https://shop.example/cart").await?;
let cart = tab
.xhr("/api/cart")
.header("accept", "application/json")
.send()
.await?;
let _ = cart;
Ok(())
}
}
Mix and rotate identities
An Identity holds two browsers and a platform. The tls() browser supplies
the ClientHello, the HTTP/2 and HTTP/3 settings, and the ja3, ja4, and
h2_fingerprint audit values. The http() browser supplies the
User-Agent, sec-ch-ua, Accept-Language, the header shape, and the
header order. The platform selects the OS that the User-Agent names, the
HTTP/2 platform overrides, and the TcpProfile.
| Function | tls() side | http() side | Kind::Config when |
|---|---|---|---|
Identity::locked(browser, platform) | browser | browser | Never; the build checks the platform |
rotate_tls(tls) | tls | unchanged | tls is of another Family than http() |
rotate_hello() | the next ClientHello of the family | unchanged | The family has one ClientHello |
switch_family(dest) | dest | dest | dest is of the same Family, or has no identity for the platform |
rotate_hello moves through Browser::all() order and wraps. A profile can
share its ClientHello with another version; its [meta] table names that
version in hello, tls() returns it, and rotate_hello counts shared
ClientHellos once.
Build a session from an identity
SessionBuilder::identity(identity) sets both browsers and the platform.
This session sends the headers of Chrome 154 on Linux over the ClientHello,
HTTP/2 settings, and HTTP/3 settings of Chrome 152:
#![allow(unused)]
fn main() {
use leyline::{Browser, Identity, Platform, Session};
fn run() -> leyline::Result<()> {
let identity = Identity::locked(Browser::Chrome154, Platform::Linux)
.rotate_tls(Browser::Chrome152)?;
let session = Session::builder().identity(identity).build()?;
assert_eq!(session.identity().browser(), Some(Browser::Chrome154));
Ok(())
}
}
browser, profile, and identity replace each other; the last call wins.
browser and profile keep the platform in either order. identity sets
the platform too, so a platform call before it has no effect and one after
it replaces the identity’s platform. build() returns Kind::Config when
the http() browser has no identity for the platform, for example Safari 26
on Windows. A session built with SessionBuilder::profile sends that one
profile on both sides.
Change the identity of a session
Session::with_identity(identity) derives a session that sends another
identity; the parent keeps its own. The derived session takes the
ClientHello, the HTTP/2 and HTTP/3 settings, the headers, and the audit
values from the identity, as SessionBuilder::identity does.
| Part | In the derived session |
|---|---|
| Connection pool | Shared with the parent, in its own partition, so connections never cross identities |
HSTS store and Alt-Svc knowledge | Shared with the parent |
| TLS session cache | Its own, empty |
| Cookie jar | The parent’s. Give it another with with_cookie_jar |
Brand, proxy, timeouts, TcpProfile, other settings | Kept |
A user-agent header from SessionBuilder::headers still replaces the
identity’s User-Agent. Session::identity() reports the identity the
derived session sends. See Sessions.
#![allow(unused)]
fn main() {
use leyline::{Browser, Family, Identity, Platform, Session};
fn run() -> leyline::Result<()> {
let chrome = Identity::locked(Browser::latest(Family::Chrome), Platform::Linux);
let session = Session::builder().identity(chrome).build()?;
let next_hello = session.with_identity(chrome.rotate_hello()?)?;
let firefox = chrome.switch_family(Browser::latest(Family::Firefox))?;
let switched = session.with_identity(firefox)?;
let _ = (next_hello, switched);
Ok(())
}
}
with_identity returns Kind::Config when:
- The session has no
Browser: a plain session, or one built withSessionBuilder::profile. - The
http()browser has no identity for the platform. - The session brand, Edge or Opera, has no overlay for the
http()browser on the platform. Only Chromium browsers have one. - The policy is
ProtocolPolicy::Http3orRaceand thetls()browser has no[h3]table. UnderAuto,Http1, orHttp2that is accepted. See HTTP/3.
Read and test the bundled profiles
ProfileRegistry::global().get(name, version) looks up a bundled profile;
Browser::profile() is the short path. The
profile reference lists what ships and how to load your own.
#![allow(unused)]
fn main() {
use leyline::Browser;
use leyline::profile::ProfileRegistry;
let profile = ProfileRegistry::global()
.get("chrome", 152)
.expect("chrome 152 is bundled");
assert_eq!(profile.meta.version, 152);
assert_eq!(Browser::Chrome152.profile().meta.browser, "chrome");
}
The offline conformance test checks each profile’s JA4 and HTTP/2 values
against its recorded reference values without the network. It needs the
bench-internals feature:
cargo test -p leyline-http --features bench-internals --test it fingerprint_conformance::
It checks the profile data against the reference values; it does not capture a browser.
Next
Read Features and targets.
Features and targets
This page lists the Cargo features, how Leyline’s BoringSSL coexists with other TLS crates, and how the documentation builds. For the supported targets and building BoringSSL from source, see Supported platforms.
Cargo features
The package is leyline-http and the library is leyline. The cookie jar,
system trust-store loading, and streaming bodies always compile.
| Feature | Default | What it changes |
|---|---|---|
charset | yes | Adds encoding_rs. Without it, Response::text decodes lossy UTF-8 and ignores the Content-Type charset. |
compression-gzip | yes | gzip bodies and the zlib certificate decompressor. Pulls in flate2. |
compression-brotli | yes | Brotli bodies and the Brotli certificate decompressor. Pulls in brotli. |
compression-deflate | yes | deflate bodies and the zlib certificate decompressor. Pulls in flate2. |
compression-zstd | yes | zstd bodies and the zstd certificate decompressor. Pulls in zstd. |
multipart | yes | Adds leyline::multipart and RequestBuilder::multipart. |
websocket | yes | Adds Session::websocket and the Ws* types. Pulls in tungstenite. |
http3 | yes | Adds the Http3 and Race policies and leyline-quiche. |
html | yes | Adds leyline::html, Session::crawl, and Tab::submit_form. See Accounts. |
socks | no | SOCKS5 proxies, and HTTP/3 through a SOCKS5 proxy. Without it, a socks5:// or socks5h:// session proxy, ProxyPool entry, or environment proxy fails build() with Kind::Config, and one set on a request or with with_proxy fails that request with Kind::Proxy. |
tower | no | Adds LeylineService. See Service integration. |
test-util | no | Adds leyline::testing. Enable it in [dev-dependencies] only. See Testing. |
bench-internals | no | Makes internal items public for Leyline’s own tests, benches, and fuzz targets. Outside the semver promise. |
full | no | Every feature above except test-util and bench-internals. |
Each compression feature gates both the body codec and the matching RFC 8879
certificate decompressor. Certificate compression is part of the
fingerprint, so a profile that lists an algorithm whose feature is off fails
at build() with a message that names the algorithm and the feature.
Request compression with the codec compiled out fails the same way at
send().
To start from nothing and add back what you need:
[dependencies]
leyline-http = { version = "0.1", default-features = false, features = ["compression-brotli", "socks"] }
Build with RUSTFLAGS="--cfg leyline_unstable_bssl", or enable
bench-internals, to reach the BoringSSL SslContextBuilder behind
TlsContext. Both are outside the semver promise.
Symbol prefixing and openssl-sys
Leyline links next to openssl-sys or boring-sys in one binary:
- BoringSSL is built with
-DBORINGSSL_PREFIX=LEYLINE, so every C export isLEYLINE_<name>. The C++ structs behindSSLandSSL_SESSIONareLEYLINE_ssl_standLEYLINE_ssl_session_st, and the internal C++ code is in thebssl::LEYLINEnamespace. The bindings keep the plain Rust names and carry a#[link_name]for the prefixed export. leyline-bssl-sysdeclareslinks = "leyline_bssl", its own key, so it does not collide with a crate that claimsboringssl.- The build reads only
LEYLINE_BSSL_*variables, so a value set forboring-sysnever reaches Leyline.
The crates have no fips, mlkem, or rpk feature.
Documentation build
docs.rs builds leyline-http for x86_64-unknown-linux-gnu only, which
keeps the build within the docs.rs limits, with features = ["full"]. The
docs cover every feature except test-util and bench-internals.
The minimum Rust version is in MSRV.
Supported platforms
This page lists the targets Leyline builds for and the tools the BoringSSL build needs.
Supported targets
leyline-bssl-sys compiles BoringSSL from source with the cc crate, from
the source lists BoringSSL ships in gen/sources.json, and uses the Rust
bindings committed for the target. A build needs no CMake, Go, Perl, or
libclang.
| Target | Notes |
|---|---|
x86_64-unknown-linux-gnu | glibc Linux |
aarch64-unknown-linux-gnu | glibc Linux |
x86_64-unknown-linux-musl | musl Linux, static binaries |
aarch64-unknown-linux-musl | musl Linux, static binaries |
aarch64-apple-darwin | Apple silicon macOS |
x86_64-pc-windows-msvc | MSVC toolchain |
On any other target, including Intel macOS (x86_64-apple-darwin), 32-bit
targets, and windows-gnu, the build stops with an error that names the
supported targets.
The platform a session claims is separate from the build target: a Linux build can claim Windows or macOS. See Choose a platform.
Build BoringSSL
The build compiles the BoringSSL source that ships in leyline-bssl-sys.
LEYLINE_BSSL_SOURCE_PATH points the build at another BoringSSL source tree.
The build applies Leyline’s patches to that tree in place.
LEYLINE_BSSL_PATH links a BoringSSL that you built yourself. The build
applies no patches to it, so it must carry Leyline’s patches and be built
with -DBORINGSSL_PREFIX=LEYLINE.
Other variables:
LEYLINE_BSSL_ASSUME_PATCHEDskips the patches for a tree that already carries them. It needsLEYLINE_BSSL_PATHorLEYLINE_BSSL_SOURCE_PATH.LEYLINE_BSSL_RUST_CPPLIBreplaces the C++ standard library that the build links: by defaultc++on macOS,stdc++on Linux, none on Windows.LEYLINE_BSSL_SYSROOTgivesbindgena sysroot when thebindgenfeature is on.
The committed bindings match the headers that ship in leyline-bssl-sys.
LEYLINE_BSSL_INCLUDE_PATH needs the bindgen feature of
leyline-bssl-sys; without it the build stops with an error. When
LEYLINE_BSSL_PATH links a BoringSSL whose headers differ, turn on the
bindgen feature too.
The build needs:
- A C and C++ compiler: Xcode Command Line Tools on macOS, GCC or Clang on
Linux, the MSVC build tools on Windows. The
cccrate finds it, andCC,CXX,CFLAGS, andCXXFLAGSwork as they do for anyccbuild. - Git. Every build runs
git applyfor Leyline’s patches unlessLEYLINE_BSSL_ASSUME_PATCHEDis set. A source checkout also uses Git to fetch the BoringSSL submodule.
On Windows, the x86_64 assembly is NASM source. When NASM is on PATH, or
NASM names it, the build assembles it. Otherwise it links the objects that
ship in prebuilt/x86_64-pc-windows-msvc/, which CI assembles with NASM
2.16.01 from the same source and checks on every run.
LEYLINE_BSSL_SOURCE_PATH needs NASM, because the shipped objects match only
the bundled BoringSSL.
The build uses the macOS deployment target that Rust uses
(MACOSX_DEPLOYMENT_TARGET, 11.0 by default), honors +crt-static on
MSVC, and maps the source and output directories to /build, so the
libraries embed no local paths. The first build compiles BoringSSL; later
builds reuse it.
Windows
Install Visual Studio Build Tools with the C++ workload. A Developer Command Prompt is not required, and NASM is optional.
With the bindgen feature, also install LLVM and set LIBCLANG_PATH to
its bin directory, so bindgen finds libclang.dll.
musl
BoringSSL has C++ code, so a musl build needs a musl C and C++
toolchain. The musl-gcc wrapper from musl-tools has no C++ compiler;
use a full cross toolchain, such as the musl.cc builds or the one cross
ships.
-
Put the toolchain on
PATH, for examplex86_64-linux-musl-cross/bin. -
Set the Rust linker to the musl compiler:
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=x86_64-linux-musl-gcc export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc -
Build:
cargo build -p leyline-http --target x86_64-unknown-linux-musl
The build finds <arch>-linux-musl-gcc and <arch>-linux-musl-g++ on
PATH; set CC_<target> and CXX_<target> to use another compiler. The
musl libstdc++ links statically.
Next
Read the Profile reference for each bundled profile and the capture it comes from.
Browser profiles
A profile is one TOML file under
crates/leyline/profiles/<family>/<version>.toml that describes one browser
build: its TLS ClientHello, HTTP/2 SETTINGS, HTTP/3 transport, and
per-platform identity. Leyline compiles the bundled profiles into the crate
and indexes them in ProfileRegistry; SessionBuilder::browser selects
one. This page lists what ships and where each value comes from, then covers
profile stability and your own profiles.
Bundled profiles
| Profile | Browser variant | captured_against | JA4 |
|---|---|---|---|
| Chrome 145 | Chrome145 | chrome-145.0.7632.160 | estimated |
| Chrome 146 | Chrome146 | chrome-146.0.7680.178 | estimated |
| Chrome 147 | Chrome147 | chrome-147.0.7727.138 | estimated |
| Chrome 148 | Chrome148 | chrome-148.0.7778.216 | estimated |
| Chrome 149 | Chrome149 | chrome-149.0.7827.201 | estimated |
| Chrome 150 | Chrome150 | chrome-150.0.7871.187 | estimated |
| Chrome 151 | Chrome151 | chrome-151.0.7922.174 | estimated |
| Chrome 152 | Chrome152 | chrome-152.0.7977.83 | estimated |
| Chrome 153 | Chrome153 | chrome-153.0.8010.53 | estimated |
| Chrome 154 | Chrome154 | chrome-154.0.8037.58 | estimated |
| Chrome 155 | Chrome155 | chrome-155.0.8059.40 | estimated |
| Brave (Chromium 146) | Brave146 | brave-146.1.88.138 | estimated |
| Brave (Chromium 154) | Brave154 | brave-154.1.96.59 | estimated |
| Brave (Chromium 155) | Brave155 | brave-155.1.97.56 | estimated |
| Firefox 148 | Firefox148 | firefox-148.0.2 | gated |
| Firefox 149 | Firefox149 | firefox-149.0.2 | gated |
| Firefox 150 | Firefox150 | firefox-150.0 | gated |
| Firefox 151 | Firefox151 | firefox-151.0.4 | gated |
| Firefox 152 | Firefox152 | firefox-152.0.6 | gated |
| Firefox 153 | Firefox153 | firefox-153.0.4 | gated |
| Firefox 154 | Firefox154 | firefox-154.0.1 | gated |
| Firefox 155 | Firefox155 | firefox-155.0.1 | gated |
| Firefox 156 | Firefox156 | firefox-156.0.1 | gated |
| Firefox 157 | Firefox157 | firefox-157.0 | gated |
| Firefox 158 | Firefox158 | firefox-158.0 | gated |
| Safari 18 | Safari18 | safari-18.6-20621.3.11.11.3 | gated |
| Safari 26 | Safari26 | safari-26.6.2-21624.5.1.11.3 | gated |
| Safari 27 | Safari27 | safari-27.0-21625.1.29.18.28 | gated |
| Safari iOS 17 | SafariIOS17 | safari-ios-17.5-21F79-simulator | gated |
| Safari iOS 18 | SafariIOS18 | safari-ios-18.6-22G86-simulator | gated |
| Safari iOS 27 | SafariIOS27 | safari-ios-27.0-iphone16,2-device | gated |
| OkHttp4 Android 10+ | OkHttpAndroid10 | okhttp-4.12.0 | gated |
| CFNetwork iOS 18 | CfnetworkIOS18 | cfnetwork-3826.600.41-ios18.6-simulator | gated |
| CFNetwork iOS 27 | CfnetworkIOS27 | cfnetwork-3892.100.1-ios27.0-device | gated |
| CFNetwork macOS 26 | CfnetworkMacOS26 | cfnetwork-3860.700.1-macos26.6.2-vm | gated |
Without .platform(), a session claims the first platform its profile
covers, in the order Windows, macOS, Linux, Android, iOS, so a mobile-only
browser such as SafariIOS27 claims iOS. See
Sessions.
Browser::latest(family) returns the newest profile of a family whose
capture is in the family’s latest_capture list in families.toml. The
list defaults to browser; Safari iOS and OkHttp add emulator, and
CFNetwork uses native and emulator. The build fails when a family has no
such profile. A platform_browser target resolves the same way.
Browser::default() is Browser::latest(Family::Chrome).
Chromium-family profiles do not store sec-ch-ua. Leyline derives it from
the major version and the ch_ua_brand field in [meta], with Chromium’s
GREASE brand, version, and order rule.
What the JA4 column means
A profile records a JA4 reference value under [tls.fingerprint] and an
Akamai HTTP/2 reference value under [h2.fingerprint]. The offline
fingerprint_conformance test computes both from the profile’s [tls] and
[h2] tables and compares them with the reference values. It gates the
HTTP/2 value of every profile.
- Gated. The profile sets
extension_permutation, so the extension order is fixed and the computed JA4 is exact. The test fails on a difference. - Estimated. The profile leaves the extension order to BoringSSL, as Chrome shuffles it on every connection, so the computed JA4 is an estimate. The test reports a difference and passes. JA4 sorts the extensions, so a live session still sends one JA4.
The live tls_peet suite sends a request with each profile to a fingerprint
echo server and compares the reported JA4 and HTTP/2 values with the
reference values. Neither test captures a browser, and a matching JA4 does
not prove that every ClientHello field is equal.
Provenance
The reference values come from the capture that the profile’s capture and
captured_against keys in [meta] name. A profile without
captured_against logs a tracing warning when it loads.
capture | Source |
|---|---|
browser | A capture of the named browser |
native | A capture of an OS HTTP stack, such as URLSession for CFNetwork |
headless-shell, webview | A related build that is not the shipped browser, such as chrome-headless-shell or a WKWebView host |
emulator | The shipped app on an Android emulator or iOS simulator: real app and OS TLS stack, not a physical phone |
inferred | No capture of this version. Values come from a neighboring version |
self-referential | The reference values are Leyline’s own past output |
| Profile | capture | Source |
|---|---|---|
| Chrome 145 | browser | chrome-145.0.7632.160 macOS and 145.0.7632.160 Windows headful; 145.0.7632.159 Linux headful |
| Chrome 146 | browser | chrome-146.0.7680.178 macOS and 146.0.7680.178 Windows headful; 146.0.7680.177 Linux headful |
| Chrome 147 | browser | chrome-147.0.7727.138 macOS and 147.0.7727.138 Windows headful; 147.0.7727.137 Linux headful |
| Chrome 148 | browser | chrome-148.0.7778.216 macOS and 148.0.7778.217 Windows headful; 148.0.7778.215 Linux headful |
| Chrome 149 | browser | chrome-149.0.7827.201 macOS and 149.0.7827.201 Windows headful; 149.0.7827.200 Linux headful |
| Chrome 150 | browser | chrome-150.0.7871.187 macOS and 150.0.7871.187 Windows headful; 150.0.7871.186 Linux headful |
| Chrome 151 | browser | chrome-151.0.7922.174 macOS and 151.0.7922.174 Windows headful; 151.0.7922.173 Linux headful |
| Chrome 152 | browser | chrome-152.0.7977.83 macOS and 152.0.7977.83 Windows headful; 152.0.7977.82 Linux headful |
| Chrome 153 | browser | chrome-153.0.8010.53 macOS and 153.0.8010.53 Windows headful; 153.0.8010.52 Linux headful |
| Chrome 154 | browser | chrome-154.0.8037.58 macOS and 154.0.8037.58 Windows headful; 154.0.8037.57 Linux headful |
| Chrome 155 | browser | chrome-155.0.8059.40, macOS, Linux, and Windows --headless=new |
| Brave (Chromium 146) | browser | brave-146.1.88.138, macOS, Windows, and Linux headful |
| Brave (Chromium 154) | browser | brave-154.1.96.59, macOS, Windows, and Linux headful |
| Brave (Chromium 155) | browser | brave-155.1.97.56, macOS, Linux, and Windows --headless=new |
| Firefox 148 | browser | firefox-148.0.2, macOS and Linux --headless, Windows headful |
| Firefox 149 | browser | firefox-149.0.2, macOS and Linux --headless, Windows headful |
| Firefox 150 | browser | firefox-150.0, macOS and Linux --headless, Windows headful |
| Firefox 151 | browser | firefox-151.0.4, macOS and Linux --headless, Windows headful |
| Firefox 152 | browser | firefox-152.0.6, macOS and Linux --headless, Windows headful |
| Firefox 153 | browser | firefox-153.0.4, macOS and Linux --headless, Windows headful |
| Firefox 154 | browser | firefox-154.0.1, macOS and Linux --headless, Windows headful |
| Firefox 155 | browser | firefox-155.0.1, macOS and Linux --headless, Windows headful |
| Firefox 156 | browser | firefox-156.0.1, macOS and Linux --headless, Windows headful |
| Firefox 157 | browser | firefox-157.0, macOS and Linux --headless |
| Firefox 158 | browser | firefox-158.0, macOS, Linux, and Windows --headless |
| Safari 18 | browser | safari-18.6-20621.3.11.11.3, Safari.app on macOS 15.7.7 in a VM through safaridriver |
| Safari 26 | browser | safari-26.6.2-21624.5.1.11.3, Safari.app on macOS 26.6.2 in a VM through safaridriver |
| Safari 27 | browser | safari-27.0-21625.1.29.18.28, Safari.app 27.0 on macOS 26.6.2 in a VM through safaridriver |
| Safari iOS 17 | emulator | safari-ios-17.5-21F79-simulator, Mobile Safari in the iOS 17.5 simulator |
| Safari iOS 18 | emulator | safari-ios-18.6-22G86-simulator, Mobile Safari in the iOS 18.6 simulator |
| Safari iOS 27 | browser | safari-ios-27.0-iphone16,2-device, Mobile Safari 27.0 on an iPhone 15 Pro Max (iOS 27.0); the iOS 27.0 simulator capture matches it |
| OkHttp4 Android 10+ | emulator | okhttp-4.12.0, test app on the platform TLS stack of an Android 17 emulator |
| CFNetwork iOS 18 | emulator | cfnetwork-3826.600.41-ios18.6-simulator, URLSession test binary in the iOS 18.6 simulator |
| CFNetwork iOS 27 | native | cfnetwork-3892.100.1-ios27.0-device, URLSession through Shortcuts on an iPhone 15 Pro Max (iOS 27.0) |
| CFNetwork macOS 26 | native | cfnetwork-3860.700.1-macos26.6.2-vm, URLSession test binary on macOS 26.6.2 in a VM; macOS 26.2 on hardware sends the same ClientHello |
Capture notes
The facts behind the bundled profiles, from their captures in
crates/leyline/profiles/captures/.
- Chrome 145 to 154. Branded Google Chrome builds from Google’s update
server and apt repository, captured on 2026-09-25. macOS and Windows ran
headful with no user agent override, after the variations seed arrived.
Linux ran headful under Xvfb with no user agent override, from the official
.debpackages extracted without installation. The Linux captures back the user agent (X11; Linux x86_64),sec-ch-ua-platform: "Linux", the navigate header order, and the TLS and H2 values. Every build sends the four core SETTINGS (1, 2, 4, 6) on every platform, withoutmax_concurrent_streamsand without setting 8. The ClientHello has three forms: 145 to 149 send no ML-DSA signature schemes and no Trust Anchor Identifiers (t13d1516h2_8daaf6152771_d8a2da3f94cd); 150 and 151 add the ML-DSA schemes (t13d1516h2_8daaf6152771_806a8c22fdea); 152 and later also send Trust Anchor Identifiers (0xCA34) with an empty list (t13d1517h2_8daaf6152771_cb7bf5808d99). Captures with a fresh profile and with the variations seed are the same. - Chrome 150. Chrome 150 puts the ML-DSA signature schemes (0x0904,
0x0905, 0x0906) before the classical list. It needs a BoringSSL revision
with
SSL_SIGN_ML_DSA_*(3a9254f or later). Chrome changes the extension order and the GREASE values on each connection, so the profile keeps BoringSSL permutation and random GREASE. Each request HEADERS frame carries PRIORITY with the exclusive bit and weight 256, sent as 255. - Brave 146 and 154. Brave 1.88.138 (Chromium 146) and 1.96.59
(Chromium 154), captured headful on macOS and Windows with no user agent
override. Brave sends the four core SETTINGS and the Chrome user agent form.
Brave 154 sends the Chromium 150 ClientHello (ML-DSA, no Trust Anchor
Identifiers) and moves
accept-languageafteraccept-encoding. Brave changes theaccept-languageq value between requests (macOS 0.9 and 0.5, Windows 0.8 and 0.6). The profiles use the value that both versions sent on each platform: 0.9 on macOS and 0.8 on Windows. On Linux, headful under Xvfb, Brave 146 sent 0.8 and 0.6. Brave 154 sent 0.6 in three navigations (brave-154.1.96.59-linux-headful.jsonand two other runs) and 0.9 and 0.5 in the preset captures. The Brave 154 Linux row uses 0.6, the value seen most. Brave picks the q value at random, so any fixed value matches only some requests. - Edge 154 and Opera 136 on Linux. Edge 154.0.4258.37 and Opera
136.0.6008.52 (Chromium 152), captured headful under Xvfb with no user agent
override. They send the Chrome user agent form with
Edg/154.0.0.0andOPR/136.0.0.0,sec-ch-ua-platform: "Linux", and the Chrome navigate header order. - Edge 145 to 152 on Windows. Edge 145.0.3800.97, 146.0.3856.117,
147.0.3912.98, 148.0.3967.96, 149.0.4022.98, 150.0.4078.105,
151.0.4129.107, and 152.0.4191.66 come from the Microsoft Edge Enterprise
x64 MSI packages on the Microsoft download CDN. Each MSI matches the SHA-256
in the winget-pkgs manifest. The embedded installer matches the SHA-256 in
the offline manifest of the MSI. The MSI, the installer, and
msedge.execarry a valid Authenticode signature from Microsoft Corporation. Each build ran unpacked, headful, twice, from an empty profile, with no user agent override (edge-<version>-windows-run<n>.json). Each build sends the ClientHello, HTTP/2 settings, and header order of Chrome on Windows for the same major,Edg/<major>.0.0.0, and theMicrosoft Edgebrand insec-ch-ua. Edge 152 does not send the Trust Anchor Identifiers extension that Chrome 152 sends. - Edge 145 to 152 on macOS. Edge 145.0.3800.97, 146.0.3856.97,
147.0.3912.98, 148.0.3967.96, 149.0.4022.98, 150.0.4078.105,
151.0.4129.107, and 152.0.4191.66 come from the macOS DMGs on the Microsoft
download CDN. Each DMG matches its published SHA-256, and Gatekeeper accepts
each app as notarized Developer ID, Microsoft Corporation (UBF8T346G9). Each
build ran headful, twice, from an empty profile
(
edge-<version>-macos-run<n>.json), and sends the ClientHello and HTTP/2 settings of Chrome on macOS for the same major, withEdg/<major>.0.0.0. - Opera 129 to 135 on Linux. Opera 129.0.5823.65, 130.0.5847.92,
131.0.5877.116, 132.0.5905.114, 133.0.5932.85, 134.0.5954.66, and
135.0.5973.142 from get.geo.opera.com. Each
.rpmmatches Opera’s.sha256sum, and its header signature verifies with Opera’s RPM repository key6C86BE214648376680CA957B11EE8C00B693A745. Each build ran headful under Xvfb twice, from an empty profile, with no user agent override. Each sends the ClientHello, HTTP/2 settings, and header order of Chrome on Linux for the same Chromium major (145 to 151),OPR/<major>.0.0.0, theOperabrand insec-ch-ua, andsec-ch-ua-platform: "Linux"(opera-<version>-linux-headful-run<n>.json). - Firefox 157. Firefox 157.0 captured on macOS and Linux with
--headlesson 2026-10-04 sends the same ClientHello, HTTP/2 frames, and header order as Firefox 156.0.1. Over QUIC it drops the ML-DSA signature schemes that 156 sends. No Windows or session resumption capture of 157 exists; those values come from Firefox 156. - Firefox 148 to 156. Official Mozilla builds captured on macOS and
Linux with
--headlesson 2026-09-25, and on Windows headful from thewin64installers, with the SHA256SUMS signature and the Authenticode signature checked. The Windows builds sendWindows NT 10.0; Win64; x64and the same TLS and H2 values as macOS and Linux. Every build sends three key shares (X25519MLKEM768, X25519, P-256) and a HEADERS PRIORITY with weight 42 and no exclusive bit, sent as 41. - Firefox 150. Firefox 150.0 sends 17 cipher suites
(
t13d1717h2_5b57614c22b0_3cbfd9057e0d), and the profile follows that capture. Firefox 150.0.3 on Android and Firefox 151 to 153 on desktop send 16, withoutTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA. No desktop capture of a Firefox 150 point release exists, so the cipher list of desktop 150.0.3 is unconfirmed. - Firefox 151 to 153. The TLS values are the same in all three. The
cipher list drops
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA. - Firefox 156. Firefox 156 drops
ffdhe2048andffdhe3072from the supported groups. The JA4 is the same as Firefox 154 and 155. - Request header presets. tls.peet.ws records only the top-level
navigation. The other presets come from a local HTTPS server that logs
each HTTP/2 HEADERS frame in wire order. Its page loads a
<script src>, sends a JSONPOST(xhr), a form-encodedfetch()POST(form), afetch()to a sibling subdomain (same-site) and to another site (cross-origin), and then a clicked formPOST(form-navigate). Chrome 154.0.8037.57, Edge 154.0.4258.37, Brave 1.96.59, and Firefox 156.0.1 ran headful on Linux, twice each, with the same result both times. Edge sends the same headers as Chrome. Brave addssec-gpc: 1afteraccepton navigations, scripts, and plain fetches, and aftersec-ch-ua-mobileon CORS fetches, and dropsapplication/signed-exchangefrom the navigateaccept. A parser-blocking script carriespriority: u=1in Chrome andu=2in Firefox. Firefox sendspriority: u=4on eachfetch(). Every browser sends the cookie last. Thenativepreset describes a client that is not a browser, so no browser capture applies to it. The captures arecaptures/presets-<browser>-<version>-linux-run<n>.json. Safari 18.6 (20621.3.11.11.3) on macOS 15.7.7, and Safari 26.6.2 (21624.5.1.11.3) and Safari 27.0 (21625.1.29.18.28) on macOS 26.6.2 ran in VMs through safaridriver, twice each, with the same result both times. Safari 26.6.2 and 27.0 send identical presets. Safari 18.6 differs only in anaccept-encodingwithoutzstd. Safari sendspriority: u=1, ion a script,u=3, ion eachfetch(), and nocache-controlon a formPOST. Mobile Safari 17.5 in the iOS 17.5 simulator (21F79) sends nopriorityheader. Its formPOSTcame from page script, because the simulator’s Web Inspector listed no pages. Mobile Safari 27.0 on an iPhone (presets-safari-ios-27.0-iphone16,2-device.json) sends the macOS Safari 27 order. The iOS 27.0 simulator (24A434) putssec-fetch-sitebeforeoriginon CORS fetches. The Safari captures arecaptures/presets-safari-<version>-<build>-<os>-run<n>.json. - Safari 18. Safari 18.6 on macOS 15.7.7 sends the same ClientHello as
CFNetwork iOS 18: AES_128 first in the TLS 1.3 ciphers, no X25519MLKEM768,
TLS 1.0 and 1.1 in
supported_versions, and the padding extension last. The HEADERS frame carries a priority with weight 256. The capture ran in a VM, so it gives no TCP SYN values for a Mac. The profile takes only its TLS, HTTP/2, and header values from the capture. - Safari 26. Safari 26.6.2 (21624.5.1.11.3) on macOS 26.6.2 sends the
same ClientHello and HTTP/2 SETTINGS as Safari 26.2. Its header list adds
zstdtoaccept-encoding, so the profile uses thewebkit-26header style. Safari 18 and Mobile Safari 17 and 18 keep thewebkitstyle. The capture ran in a macOS 26.6.2 VM, so it gives no TCP SYN values for a Mac. The profile takes only its TLS, HTTP/2, and header values from the capture. Two safaridriver runs againsttls.peet.ws/api/allgave the same JA3, JA4, peetprint, and Akamai HTTP/2 fingerprint. - Safari iOS 27 and CFNetwork iOS 27. The iOS 27.0 simulator loads
CFNetwork,Network,libcoretls, andlibboringsslfrom its own iOS runtime. Its ClientHello is the same as Safari 26 and CFNetwork macOS 26. Mobile Safari and Safari 26.6.2 send thewebkit-26header style, whereaccept-encodingaddszstd. Safari iOS 27 and CFNetwork iOS 27 were then captured on an iPhone 15 Pro Max running iOS 27.0. Mobile Safari matches the simulator on TLS, HTTP/2, HTTP/3, and headers. CFNetwork on the device sendsCFNetwork/3892.100.1 Darwin/27.0.0and a 512 KiB initial stream window. The device request came from the Shortcuts app and the simulator request from a plainURLSessionbinary, so the window difference may belong to the app rather than the device. The simulator also reported the host’s Darwin version. The profile follows the device. Real apps send<App>/<version> CFNetwork/... Darwin/...; prefix the profile’s user agent with your app’s token. - Safari iOS 18 and Safari iOS 17. Mobile Safari 18.6 sends the same
ClientHello and HTTP/2 SETTINGS as Safari 18.6 on macOS. Mobile Safari 17.5
adds
ecdsa_sha1to the signature algorithms, sends no SETTINGS parameter 9, uses them,s,p,apseudo-header order, and a priority weight of 255. Its header order isaccept,sec-fetch-site,accept-encoding,sec-fetch-mode,user-agent,accept-language,sec-fetch-dest. Thewebkit-17header style sends that navigate order; its other presets come from the iOS 17.5 simulator captures. Mobile Safari 17.5 did not use HTTP/3 after an Alt-Svc response in three fresh simulators, so its profile has no[h3]table. The iOS 18.6 simulator ran on a macOS 15 host, soCFNetwork/3826.600.41 Darwin/24.6.0carries the Darwin version of iOS 18. - CFNetwork macOS 26. The ClientHello has one GREASE cipher first, TLS 1.3
ciphers in the order AES_256, CHACHA20, AES_128, and
rsa_pss_rsae_sha384(0x0805) twice in the signature algorithms. The groups are GREASE, then X25519MLKEM768; the key shares are GREASE, X25519MLKEM768, and X25519. Certificate compression is zlib. The ClientHello has no ALPS, no ECH GREASE, and no padding.extension_orderomits the GREASE extensions, which BoringSSL places itself.initial_connection_window_sizeis 10551295 so that the WINDOW_UPDATE increment is 10 MiB. - CFNetwork iOS 18. Compared with macOS 26: no X25519MLKEM768 (groups are
GREASE, X25519, P-256, P-384, P-521; key shares are GREASE and X25519), no
SETTINGS parameter 9, a 2 MiB initial stream window, AES_128 first in the TLS
1.3 ciphers, TLS 1.0 and 1.1 in
supported_versions, and the padding extension (21) last. BoringSSL padding to 512 bytes matches this ClientHello at SNItls.peet.ws. The padding target for a public destination (about 704) has one data point and is not confirmed. The iOS 18 build shares the duplicate 0x0805, zlib compression, the leading GREASE cipher, and them,s,p,apseudo-header order with macOS. - GREASE signature algorithm. Chrome 152 to 154, Brave 1.96.59, Edge
153 and 154, and Opera 136 put one GREASE value first in
signature_algorithmson every captured platform. Chrome 145 to 151 and Brave 1.88.138 do not. The value is random per connection and independent of the cipher GREASE.sigalg_grease = truein[tls]turns it on; it needsgrease = true. Upstream BoringSSL sends the value after a call toSSL_CTX_set_grease_sigalgs_enabled, whichSslContextBuilder::set_grease_sigalgs_enabledwraps. JA4 and JA3 drop GREASE, so only the raw list shows it. The QUIC ClientHello of these builds has no GREASE value, so[h3.tls]leaves it off. - Resumed ClientHello. The second request to
tls.peet.wsin the same browser process resumes withpre_shared_key(41). Official Linux builds, two fresh processes each, on 2026-09-25 (<browser>-<version>-linux-resumed-run<n>.json), and Chrome 154.0.8037.58 and Firefox 156.0.1 on Windows (-windows-resumed-run<n>.json), giveresumed_ja4: Chrome 145 to 149 and Brave 1.88.138t13d1517h2_8daaf6152771_b6f405a00624; Chrome 150, 151 and Brave 1.96.59t13d1517h2_8daaf6152771_a87ad97598a9; Chrome 152 to 154t13d1518h2_8daaf6152771_e2d80978ab2e; Firefox 148 to 150t13d1717h2_5b57614c22b0_e6dcd7ae0a9e; Firefox 151 to 153t13d1617h2_86a278354501_e6dcd7ae0a9e; Firefox 154 to 156t13d1517h2_8daaf6152771_e6dcd7ae0a9e. Firefox dropssession_ticket(35) when it addspre_shared_key. Safari 18.6, 26.6.2, and 27.0 under safaridriver sent nopre_shared_keyon a second connection in two fresh processes each (safari-<version>-<build>-macos-<version>-vm-resumed-run<n>.json), so the Safari profiles keeppre_shared_key = false. - CFNetwork headers. URLSession sends a fixed header list with no
sec-*headers. CFNetwork iOS 18 sendsuser-agent,accept: */*,accept-language,accept-encoding: gzip, deflate, br(thecfnetworkstyle; CFNetwork 1496 on iOS 17.5 sends the same order). CFNetwork macOS 26 and iOS 27 sendaccept: */*,user-agent,priority: u=3,accept-language,accept-encoding: gzip, deflate, br(thecfnetwork-26style). The capturedpriority: u=3appears on iOS 27 and on macOS 26. - Android. Chrome 145.0.7632.218, preinstalled and Google-signed in an
Android 17 (API 37) Google Play emulator image, sends the same ClientHello
and HTTP/2 values as Chrome 145 on desktop, the
Android 10; Kuser agent, andaccept-language: en-US,en;q=0.9(chrome-android-145.0.7632.218-emulator-run<n>.json). Firefox for Android 148.0.2, 149.0.2, 150.0.3, 151.0.4, 152.0.6, 153.0.4, 154.0.1, 155.0.1, and 156.0.1 from archive.mozilla.org (arm64-v8a APKs, signer Mozilla Release Engineering, certificate SHA-256a78b62a5165b4494b2fead9e76a280d22d937fee6251aece599446b2ea319b04; the Fenix releases publish no SHA256SUMS) ran twice each in the same emulator under geckodriver. All sendAndroid 17; Mobile,accept-language: en-US, and the HTTP/2 SETTINGS1:4096;2:0;4:32768;5:16384, which[h2.platforms.android]of Firefox 148 to 156 sets. Firefox 155 and 156 on Android send the desktop ClientHello. Firefox 148 to 154 on Android omitsigned_certificate_timestamp(148 and 149:t13d1716h2_5b57614c22b0_eeeea6562960; 150 to 153:t13d1616h2_86a278354501_eeeea6562960; 154:t13d1516h2_8daaf6152771_eeeea6562960). A profile has no per-platform TLS table, so Leyline sends the desktop ClientHello of each version on Android. - Opera on Windows. Opera 136.0.6008.52 from get.geo.opera.com
(Authenticode signer Opera Norway AS, checksum from Opera’s
.sha256sum) sends the Chrome 152 ClientHello,OPR/136.0.0.0, theOperabrand insec-ch-ua, andaccept-language: en-US,en;q=0.9(opera-136.0.6008.52-windows-run<n>.json). - TCP rows.
platforms.tomlhas Windows, macOS, Linux, and iOS rows. The option order, window, window scale, and TTL of the Windows, macOS, and Linux rows match every browser SYN captured from that host. The browser captures show MSS 1400 because the path clamps it. The hosts’ own SYNs show the default: the Linux host (MTU 1500) sends MSS 1460, window 64240, and optionsmss, sackOK, TS, nop, wscale 10, and the Windows VM (MTU 1500) sends MSS 1460, window 64240, andmss, nop, wscale 8, nop, nop, sackOK. No host SYN capture exists for macOS, so its MSS 1460 is unconfirmed. The iOS row comes from an iPhone 15 Pro Max on iOS 27.0: window 65535, window scale 6, TTL 64, andmss, nop, wscale, nop, nop, TS, sackOK, eol. Its SYN shows MSS 1400 because of the path, and the row’s MSS 1460 is the interface default, which no capture shows. Android has no row: the emulator sends the SYNs of its host.
HTTP/3 captures
Every [h3] table comes from QUIC captures against
https://quic.browserleaks.com/?minify=1. The raw files end in -h3.json or
-h3-run<n>.json.
- Chrome 145 to 154, Brave 1.88.138 and 1.96.59, and Firefox 148 to 156: official Linux builds, headful under Xvfb, two fresh processes each, eight for Chrome 154 and Firefox 156, on 2026-09-25. Firefox 157.0 the same way, two runs, on 2026-10-04. Every build used HTTP/3 on the first navigation with no QUIC flag or preference.
- Chrome 154.0.8037.58 and Firefox 156.0.1 on Windows, two runs each. The
HTTP/3 HEADERS carry
sec-ch-ua-platform: "Windows". - Chrome 154, Brave 1.96.59, Edge 154, Firefox 156, Safari 26.6.2, and Safari 27.0 in a macOS 26.6.2 VM, and Safari 18.6 in a macOS 15.7.7 VM.
- Mobile Safari 18.6 in the iOS 18.6 simulator, two runs with the HTTP/3
frames (
safari-ios-18.6-22G86-simulator-h3-run<n>.json), and Mobile Safari 27.0 in the iOS 27.0 simulator.
Each profile reproduces its capture: the QUIC ClientHello through
[h3.tls], the transport parameter set, values, and order policy, the
connection ID lengths, the SETTINGS list, the control stream frames, and the
pseudo-header order. [h3.tls] lists only the TLS 1.3 ciphers, in the
captured order, and no min_tls_version or padding, because QUIC sends
neither. Values the browser picks at random per connection are random in
Leyline in the same way. See HTTP/3 for the
keys.
- Chromium. The transport parameters come in a random order. All builds
send
version_informationwith a GREASE version at a random position,max_datagram_frame_size65536,initial_max_streams_uni103, and a GREASE parameter of up to 15 bytes. Chrome 145 addsgoogle_version(18258) with QUICv1. Chrome 149 and later addgoogle_connection_optionsORIG. Chrome 152 to 154 add Trust Anchor Identifiers to the QUIC ClientHello; Brave 1.96.59 does not. SETTINGS are1:65536;6:262144;7:100;51:1and a GREASE setting, followed by a GREASE frame of 0 to 3 bytes and a PRIORITY_UPDATE frame. The connection IDs are 8 and 0 bytes. Each Initial packet is 1250 bytes (initial_datagram_size = 1250). - Firefox. The transport parameter order is fixed. Firefox 155 to 157
add parameter 29 and offer QUICv2 in
version_information(GREASE, QUICv2, QUICv1); Firefox 148 to 154 offer GREASE and QUICv1. Every run of every build, 29 in all, sends GREASE-form parameter 4278378010 with the value 1000, so the profiles pin it. The source connection ID has 3 bytes. The destination connection ID length varies per connection;dcid_lengthweights are the 27 lengths observed for Firefox 148 to 156: 8 (12), 10 (4), 11 (1), 13 (5), 15 (2), 16 (1), 19 (1), 20 (1). Both Firefox 157 runs used 8. Firefox 156 adds the ML-DSA schemes to the QUIC signature algorithms and delegated credentials; 148 to 155 and 157 do not. The QUIC ClientHello keepsextended_master_secretandrenegotiation_info. Firefox shuffles the QUIC ClientHello extensions on every connection and keepsquic_transport_parametersand ECH last, so[h3.tls]setspermute_extensionsandextension_tail = [57, 65037]. The Initial packets carry no PADDING frames, so the profiles set noinitial_datagram_size. Every capture shows two Initial packets that differ by 3 or 4 bytes: the first holds two CRYPTO frames, the second holds one. Firefox splits the ClientHello evenly and cuts it at the midpoint of the server name, so the profiles setinitial_crypto_split = "even"andinitial_crypto_reorder = "sni_midpoint". - Safari. The transport parameter order rotates between connections.
Safari 18 and iOS 18 add Apple parameter
0xff080808last. SETTINGS are1:16383;7:100and a GREASE setting, withm,s,a,ppseudo-headers. The source connection ID is empty. Each Initial packet is 1200 bytes (initial_datagram_size = 1200).
The Chrome 154 capture negotiated real ECH, because its browser had the server’s ECH configuration. Leyline sends an ECH GREASE extension with the same extension ID, so the JA4 is the same.
Uncaptured values
These values have no capture of the official build on their own platform. Leyline keeps them so the API stays complete.
- Chrome
[identity.android]146 to 154: only Chrome 145 on Android is captured. The rows follow the reducedAndroid 10; Kuser agent. - The MSS 1460 of the macOS and iOS TCP rows.
- Header presets on Windows and macOS for the Chromium and Gecko styles.
Update cadence
- Chrome, Brave, and Firefox: a profile for each new stable major release. Chrome and Firefox ship a major release every four weeks, and Brave follows Chromium. A daily job captures each new major on Linux, macOS, and Windows and opens a pull request with the profile.
- Opera: a brand row for each new Opera release, from the Chromium version its user agent names. The same daily job adds it.
- BoringSSL: the bundled revision follows the revision that the newest Chrome profile ships. The daily job moves it, and a Chrome profile and its BoringSSL update land together.
- Safari: a profile when Apple ships an OS release, because Safari’s TLS stack changes with macOS and iOS.
- OkHttp and CFNetwork: a recapture when the upstream engine version in
captured_againstchanges.
Every new profile records the exact build in captured_against, and the JA4
and HTTP/2 reference values from that capture.
Profile data stability
A release can change the data behind a pinned variant such as
Browser::Chrome154 to correct a capture; the CHANGELOG names every such
change. Browser::latest moves to newer captures in patch releases.
SessionIdentity::profile_id() returns 16 hex characters: a hash of the
profile data the session uses, with the platform and the brand. The same
data gives the same id in every release, and any change gives a new id. It
is None for a plain session. A profile loaded from TOML gets an id from its
TOML text.
To keep a device stable across upgrades, pin the variant, store
profile_id(), and pass it to SessionBuilder::expect_profile_id on start.
build() then fails with Kind::Config when the data changed, and
err.is_profile_changed() is true.
#![allow(unused)]
fn main() {
use leyline::{Browser, Platform, Session};
fn run(saved_id: &str) -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::Chrome154)
.platform(Platform::Windows)
.expect_profile_id(saved_id)
.build()?;
let _ = session;
Ok(())
}
}
Freeze the profile data
To keep a device across a data change, store the profile itself.
SessionIdentity::export_profile() returns the profile TOML the session
uses. A session built from that TOML sends the same data and has the same
profile_id(). Device::pin_profile stores it in a Device; see
Accounts.
#![allow(unused)]
fn main() {
use leyline::{Browser, BrowserProfile, Platform, Session};
fn run() -> Result<(), Box<dyn std::error::Error>> {
let session = Session::builder()
.browser(Browser::Chrome154)
.platform(Platform::Windows)
.build()?;
let identity = session.identity();
let frozen = identity.export_profile().ok_or("no profile to export")?;
let again = Session::builder()
.profile(BrowserProfile::from_toml(&frozen)?)
.platform(Platform::Windows)
.build()?;
assert_eq!(again.identity().profile_id(), identity.profile_id());
Ok(())
}
}
Load your own profile directory
ProfileRegistry::load(dir) reads a directory laid out like the bundled
set, <family>/<version>.toml, in sorted order, with the same parse and
extension-order validation as the bundled registry. Use it for a browser
release that your crate version does not bundle yet.
BrowserProfile::from_toml parses one profile from a string with the same
validation. SessionBuilder::profile sends either result.
use std::path::Path;
use leyline::{Platform, Session};
use leyline::profile::ProfileRegistry;
fn main() -> leyline::Result<()> {
let registry = ProfileRegistry::load(Path::new("./profiles"))
.expect("profile directory loads");
let profile = registry.get("chrome", 153).expect("chrome 153 profile").clone();
let session = Session::builder()
.profile(profile)
.platform(Platform::MacOS)
.build()?;
drop(session);
Ok(())
}
load returns a ProfileError: Io when a directory or file cannot be
read, Parse when a TOML file is invalid or its extension_permutation
disagrees with the extensions its [tls] block turns on, and Empty when
the directory holds no profile.
A loaded profile carries its own [identity.<platform>] tables, which give
the user agent, sec-ch-ua, and accept_language for the platform. A table
without accept_language takes the value profiles/bare.toml sets for the
platform. build returns Kind::Config when the profile has no table for
the platform; without .platform(), the platform is Windows. A brand overlay
(SessionBuilder::brand) needs chromium_major in [meta].
Header styles
The request headers follow header_style in [meta]. Each top-level table
of profiles/headers.toml is one style, keyed by its header_style value:
chromium, gecko, webkit, webkit-26, webkit-17, okhttp,
cfnetwork, cfnetwork-26, brave, and brave-154. The build generates
the HeaderStyle enum from this file, so a new style needs only a new
table, and the build fails when a profile or a brand names a style the file
does not define. A brand row in profiles/brands.toml can set
header_style to replace the profile’s style.
| Key | Meaning |
|---|---|
variant | The HeaderStyle variant name |
default | true on the one style a profile without header_style uses: chromium |
fallback | The header list for a request without a preset, or a preset the style does not list |
presets | One header list per Preset, with names, order, and values |
extends | Another style that supplies each preset and the fallback this style does not define |
append | Headers every request of the style carries. A session or request header of the same name wins |
order | A header order applied after the merge, including the cookie and caller headers. RequestBuilder::header_order and FingerprintSpec::header_order replace it |
The placeholders {user_agent}, {sec_ch_ua}, {sec_ch_ua_mobile},
{sec_ch_ua_platform}, {accept_language}, {origin}, {referer}, and
{fetch_site} take the values of the session and the request. The brave
style extends chromium: it sends sec-gpc: 1, a navigate accept without
application/signed-exchange, and the header order of the Brave 146 capture.
The brave-154 style carries every preset from the Brave 1.96.59 capture,
with sec-gpc where Brave sends it, and has no order.
TLS keys
key_shares in [tls] lists the groups the key_share extension carries,
in order; each must also be in curves. Without it, BoringSSL sends key
shares for the first one or two groups.
A brand in profiles/brands.toml can set a [<brand>.tls] table whose
request_trust_anchors replaces the base profile’s value. Edge sets it to
false: Edge 152 and 154 do not send the Trust Anchor Identifiers extension
that Chrome 152 and later send.
Build a profile from a JA3 or Akamai string
BrowserProfile::from_fingerprint builds a profile from fingerprint strings
instead of a TOML file. SessionBuilder::profile sends the result.
FingerprintSpec input | Sets |
|---|---|
ja3(raw) | From version,ciphers,extensions,curves,point formats: the cipher list, the curve list, and the exact extension order |
ja4_r(raw) | From a raw JA4_r or JA4_ro string: the cipher list, the extension set, and the signature algorithms. The hashed JA4 form cannot be inverted and is rejected |
akamai(raw) | From SETTINGS|WINDOW_UPDATE|PRIORITY|pseudo-header order: the SETTINGS order and values, the connection window, and the pseudo-header order |
base(profile) | The starting profile. The result keeps every value the strings do not carry: the identity tables, the headers, HTTP/3, the signature algorithms for a JA3 string, and the curves for a JA4_r string. Without a base, the bare profile |
header_order(names) | Replaces the order of the profile’s header style |
name(name) | The profile name in logs and errors |
This example applies the JA3 and Akamai values of the bundled Chrome 152 profile to the Chrome 148 profile:
use leyline::profile::FingerprintSpec;
use leyline::{Browser, BrowserProfile, Session};
fn main() -> leyline::Result<()> {
let ja3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,\
0-23-65281-11-35-16-51-43-45-10-13-5-18-27-17613-51764-65037,4588-29-23-24,0";
let akamai = "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p";
let spec = FingerprintSpec::new()
.base(Browser::Chrome148.profile().clone())
.ja3(ja3)
.akamai(akamai)
.name("Chrome 152 from strings");
let profile = BrowserProfile::from_fingerprint(spec)?;
let chrome152 = Browser::Chrome152.profile();
assert_eq!(profile.tls.ciphers, chrome152.tls.ciphers);
assert_eq!(profile.tls.curves, chrome152.tls.curves);
assert_eq!(chrome152.expected_h2_fingerprint(), Some(akamai));
let session = Session::builder().profile(profile).audit(true).build()?;
drop(session);
Ok(())
}
A session built from these strings reports the same ja3 and
h2_fingerprint in Response::audit() as a Chrome 152 session.
These rules apply to the strings:
- A GREASE value in any list sets
grease = trueand is not stored. A string without GREASE keeps the base value. - Each extension ID turns on the
[tls]setting that sends it. An extension that carries data (application_settings,compress_certificate,delegated_credentials,record_size_limit) takes its value from the base, andfrom_fingerprintfails when the base has none. pre_shared_key(41) setspre_shared_key = true.padding(21) setspadding = trueand must be the last extension.- The JA3 version must be 771 and the point formats
0. The Akamai PRIORITY field must be0. - A cipher, curve, or signature algorithm ID outside Leyline’s built-in table fails, as does an extension or SETTINGS ID that Leyline cannot send.
Every failure returns Kind::Config with a message that names the format
and the field.
Next
Read MSRV for the Rust version Leyline supports.
Minimum supported Rust version
The minimum supported Rust version (MSRV) is 1.96, the rust-version
field in the workspace Cargo.toml. Every crate sets it. The edition is
2024; the forked leyline-bssl* crates keep upstream’s 2021.
What the release gate checks
The release gate runs cargo check for the workspace on 1.96 and runs the
tests on the toolchain that rust-toolchain.toml pins. A toolchain between
them is expected to work but is not part of the gate.
When the MSRV moves
The MSRV stays at least two releases behind current stable. It moves only when Leyline needs a compiler feature, never on a schedule or for style. A bump ships in a minor release with its own line in CHANGELOG.md; a patch release never raises the MSRV.
Next
Read the API map to see how the public API fits together.
Leyline API
This page and the API reference are the
public contract of leyline-http (import name leyline). The API reference
lists every public item and is generated from the compiler; cargo truesight check fails when it no longer matches the code. This page maps each job to
its call and to the guide that explains it, and holds the error kinds and the
rules the API follows.
The bench-internals feature and the leyline_unstable_bssl compiler flag
expose internals for Leyline’s own tests and the BoringSSL context builder.
Neither is part of the contract, and the API reference leaves both out.
Overview
one-off leyline::get(url) one GET from a plain session
session Session::new() plain client, cannot fail
Session::browser(browser) browser session, cannot fail
Session::builder() → SessionBuilder → build() → Session (no browser: plain client)
session.with_proxy(config) clone that shares the pool, other proxy
session.fresh_pool() clone with a new pool and TLS session cache
session.with_cookie_jar(jar) clone that shares the pool, other jar
session.with_redirect(policy) clone that shares the pool, other redirect policy
session.with_identity(id) clone with another identity, own pool partition
session.with_base_url(url) clone that shares the pool, other base URL
session.tab() → Tab current page, initiator for each request
session.shutdown() · is_shut_down() stop every clone and derived session
session.state() → SessionState TLS tickets, Alt-Svc, HSTS
Device::capture / open / save_to / load_from one saved account device
device.autosave(&session, path, interval or DeviceAutosaveOptions) → DeviceAutosave · update / track / device / flush / shutdown
device.tab(&session) → Tab tab on the saved page
crawl SessionBuilder::host_limits(HostLimits) · proxy_pool(ProxyPool) · RequestBuilder::tag
HostLimits: max_in_flight · per_second · host · max_total_in_flight · pause_on · pause_for · max_pause
ProxyPool::identified · rotate_on_block · Session::host_stats · BlockRules::statuses
Session::robots(url, agent) → Robots: allows · allows_use(url, Purpose) · crawl_delay · sitemaps · source
Session::crawl(seed) → Crawl: respect(Respect) · ignore_robots · scope(Scope) · limit · host_limits · next → CrawlPage · stats → CrawlStats (feature `html`)
request session.get / post / put / patch / delete / head / request(Method, url) → RequestBuilder
send RequestBuilder.send() or .await → Response
RequestBuilder.download(path, limit) → bytes written
RequestBuilder.pages() → Pages follows Link rel="next"
session.execute(http::Request<Body>) for prebuilt requests and tower
read Response: status · headers · header · text · bytes · json · read_until
raw: into_stream · copy_to decoded and capped: into_decoded_stream · copy_decoded_to
Response: url · redirect_chain · version · timing · attempts · proxy
Response: link · links · relay_headers(RelayBody) · block · download_to
fail Error · err.kind() → Kind · err.category() → ErrorCategory · gateway_status()
RequestBuilder::error_for_status · err.status / body / body_text / headers / header / retry_after / retries_exhausted / attempts / proxy
Error::find(&dyn std::error::Error) leyline error under other layers
policy TimeoutConfig · RetryPolicy · RedirectPolicy (session default, request override)
RetryPolicy: retry_if · wait_header · retry_unsent · skip_blocks · max_retry_after
TimeoutConfig: body · error_body CompressionConfig: max_body_size · max_error_body
identity Browser · Platform · ChromiumBrand · Identity
observe Trace hooks · Trace::summary · trace::Metrics · trace::Fanout · Response::timing · Response::tls · Response::audit
tower LeylineService (feature `tower`) · relay_headers(&HeaderMap, RelayBody) · redact_url
html html::forms · Form::find · FormEnctype · Tab::submit_form · html::meta · html::links → Anchor · html::text · html::canonical · html::Directives (feature `html`)
test testing::TestServer::http / https / http_on · TestResponse::delay / chunks / close
RecordedRequest: raw · request_line · header_values · header_count · text (feature `test-util`)
One function per job. A setter that takes a config type accepts
impl Into<Config> where a shorthand exists, so the common case stays one
call:
#![allow(unused)]
fn main() {
use std::time::Duration;
use leyline::{Browser, Family, Platform, ProtocolPolicy, Session, TimeoutConfig};
async fn run() -> leyline::Result<()> {
let session = Session::browser(Browser::default());
let body = session.get("https://example.com/").await?.text().await?;
let session = Session::builder()
.browser(Browser::latest(Family::Firefox))
.platform(Platform::MacOS)
.protocol(ProtocolPolicy::Http2)
.timeout(Duration::from_secs(15))
.proxy("http://user:pass@host:8080")
.build()?;
let resp = session
.post("https://example.com/api")
.header("x-trace", "1")
.json(&serde_json::json!({ "q": 1 }))
.timeout(TimeoutConfig::new().total(Duration::from_secs(5)))
.await?;
drop((body, resp));
Ok(())
}
}
Where each job is documented
Sessions and identities
| Job | Call | Guide |
|---|---|---|
| Plain or browser session | Session::new, Session::browser, Session::builder | Sessions |
| Newest or pinned browser | Browser::latest, Browser::get, Browser::Chrome148 | Choose a browser |
| Platform and brand | SessionBuilder::platform, brand | Choose a platform |
| Token and base URL | bearer_auth, base_url, with_base_url | Sessions |
| Languages and user agent | languages, user_agent | Set the languages |
| Derived sessions | with_proxy, with_cookie_jar, with_redirect, with_identity, fresh_pool | Derive a session |
| Stop every clone | Session::shutdown, Error::is_shut_down | Stop a session |
| Save TLS tickets, Alt-Svc, HSTS | Session::state, SessionState::restore_into | Sessions |
| Read what a session sends | Session::identity, Browser::identity, SessionIdentity::profile_id | Sessions |
| Mix and rotate identities | Identity::locked, rotate_tls, rotate_hello, switch_family | Fingerprints |
| Detect a profile change | expect_profile_id, Error::is_profile_changed | Choosing a profile |
| Load or build a profile | ProfileRegistry::load, BrowserProfile::from_toml, from_fingerprint | Profiles |
| Saved account device | Device, DeviceAutosave | Accounts |
| Device save intervals | Device::autosave, DeviceAutosaveOptions | Save the device as it changes |
Requests and responses
| Job | Call | Guide |
|---|---|---|
| Methods, headers, query, bodies | RequestBuilder | Requests |
| Header order and merge | SessionBuilder::headers, RequestBuilder::header | Requests |
| Page initiator and tabs | initiator, Session::tab, Tab | Requests |
Prebuilt http::Request | Session::execute | Requests |
| Status, headers, bodies | Response | Responses |
| Status as an error | error_for_status, error_for_status_ref | Responses |
| Body kept in a status error | CompressionConfig::max_error_body, TimeoutConfig::error_body | Responses |
| Response body cap | CompressionConfig::max_body_size | Responses |
Link pagination | RequestBuilder::pages, Response::links | Responses |
| Stream, download, stop at a marker | .stream(), into_decoded_stream, download, read_until | Streaming |
| Redirect policy | RedirectPolicy | Redirects |
| Cookies | cookie::Jar, Jar::save_to, Jar::autosave | Cookies |
| WebSocket | Session::websocket | WebSocket |
| HTML forms, meta, links | html::forms, Form::set, FormEnctype, Tab::submit_form, html::meta, html::links | Log in with the page’s form |
| Visible text of a page | html::text | Read the text of a page |
| Page robots directives, canonical URL | html::Directives::read, html::canonical | Obey a page’s own rules |
| Cancel a request | drop, Session::shutdown | Cancellation |
Policy, transport, and observation
| Job | Call | Guide |
|---|---|---|
| Timeouts and retries | TimeoutConfig, RetryPolicy | Retries and timeouts |
| Retry on a condition, wait on a header | RetryPolicy::retry_if, wait_header, WaitFormat | Retry on a condition |
| Retry a request the server did not process | RetryPolicy::retry_unsent | The idempotency rule |
| Cap a server-requested wait | RetryPolicy::max_retry_after | Retry-After wins |
| Return a block without a retry | RetryPolicy::skip_blocks | Retry a block through another proxy |
| Bound the body read | TimeoutConfig::body | The timeouts |
| Proxies and proxy pools | ProxyConfig, ProxyUrl, ProxyPool | Proxies |
| Host limits, blocks, crawl metrics | HostLimits, BlockRules, host_stats, trace::Metrics | Crawling |
| Pause a host on a status | HostLimits::pause_on, pause_for, max_pause | Limit each host |
| Read and obey robots.txt | Session::robots, Robots::parse, Robots::allows, Purpose | Obey robots.txt |
| Crawl a site | Session::crawl, Crawl, Respect, Scope, CrawlPage, CrawlStats, SkipReason | Crawl a site |
| DNS, sockets, pool, Happy Eyeballs | DnsConfig, SocketConfig, PoolConfig, HappyEyeballsConfig | Network |
| Trust roots, pins, TLS floor | TlsTrustConfig | TLS trust |
| HTTP/3 | ProtocolPolicy | HTTP/3 |
| Logs and trace hooks | Trace, TracingTrace, trace::Fanout | Logging and tracing |
| One event per request | Trace::summary, trace::Summary | One event per request |
| Fingerprint audit | audit(true), Response::audit, AuditData::compare | Fingerprints |
| Errors | Error, Kind, ErrorCategory | Errors |
| Services, Tower, axum | LeylineService, relay_headers, redact_url | Service integration |
| Tests | testing::TestServer, RecordedRequest::raw, header_values | Testing |
Errors
One error type: leyline::Error, Send + Sync + 'static. Read
err.kind() for the Kind below, or err.category() for the
ErrorCategory that groups it for logs, metrics, and gateway statuses.
Errors covers categories, predicates, attempts, source
errors, and status mapping.
| Kind | Meaning |
|---|---|
Request | HTTP-level failure (bad header, malformed request) |
Redirect | A redirect could not be followed: a custom policy passed 32 hops, a Location scheme other than http or https, or a redirect that must resend a streamed request body. A policy that stops returns the 3xx response and is not an error |
Status | error_for_status rejected the status |
Body | A body cannot be used: a response body over max_body_size or a caller limit, a response body already taken, a request body that cannot be compressed or resent, or a WebSocket protocol violation or oversized message |
Decode | Decompression failed: bad data, a truncated stream, or more than 4 codings |
Timeout | total or response_header expired, or read or body expired in bytes, text, or json |
Connect | DNS resolution or TCP connect failed, or the connect limit expired during DNS, TCP, a proxy handshake, or a TLS or HTTP/3 handshake. An expired limit also makes is_timeout() true |
Tls | TLS handshake or certificate verification failed |
Http2 | HTTP/2 protocol failure |
Http3 | HTTP/3 protocol or handshake failure |
Proxy | Proxy dial, handshake, authentication, or CONNECT failed, the proxy could not reach the origin, or a request of a session that rejected an invalid environment proxy |
Io | Socket I/O, or an HTTP/1.1 response that breaks the wire format; err.io() then has kind InvalidData |
Config | Invalid session, request, TLS profile, trust store, or proxy configuration, including an invalid environment proxy at build() |
Url | URL parse failure |
Json | JSON serialize or deserialize failure |
The profile loaders return profile::ProfileError, and
multipart::Form::file and Part::file return std::io::Result; neither
has a Kind.
Features
Default: charset, compression-gzip, compression-brotli,
compression-deflate, compression-zstd, multipart, websocket,
http3, html. Opt-in: socks, tower, test-util, bench-internals.
full enables every feature except test-util and bench-internals. See
Features and targets.
API shape
- One function per job, and one owner each for timing, retries, redirects, cookies, and body decoding.
- Config types (
TimeoutConfig,RetryPolicy,RedirectPolicy,ProxyConfig,DnsConfig,TlsTrustConfig,PoolConfig,SocketConfig,HappyEyeballsConfig,CompressionConfig,WebSocketConfig) have private fields and consuming setters named after the field, with nowith_prefix. A setter for an optional value takesimpl Into<Option<T>>, andNoneturns the setting off;RetryPolicy::max_retry_aftertakes aDuration:transient()sets it to 60 s, andnone()has no cap. A setter that adds to a list starts withadd_or appends one item (rule,on_status). - Every
SessionBuilderconfig setter replaces the whole value. Start from::new(), which carries the defaults. A request setter merges over the session value field by field. - Public enums, and public structs with public fields, are
#[non_exhaustive],CookieandSameSiteincluded. The exceptions areaudit::Ja3Input,Ja4Input, andJa4hInput, which callers fill with a struct literal. - Builder input errors surface at
build(),send(), orconnect(), never eagerly.Identityis a value type, so its checks run at the call. IntoUrlis sealed.Session::new()andSession::browser(..)never fail: a trust-store problem fails later requests withKind::Tls, and an invalid environment proxy fails them withKind::Proxy.- A patch release never removes a
Browservariant. A retired profile keeps its variant, marked#[deprecated]through its[meta] deprecatednote, until a minor release removes it.Browser::latestandBrowser::for_platformnever return a deprecated profile. Browser,Family,Platform, andChromiumBrandserialize as the stable lowercase id thatid()returns, andFromStrreads it back.
Profile schema
BrowserProfile and the leyline::profile types mirror the bundled
profiles/<family>/<version>.toml files; field names are the TOML keys.
profiles/bare.toml holds the bare profile and profiles/platforms.toml
the per-platform TCP values. The [meta] keys capture, variant,
hello, platform_browser, deprecated, and verified_at are valid and
ignored at runtime; they are not fields of ProfileMeta, and the build reads
the first five to generate Browser. The schema is part of the API: a patch
release can add fields, and removing or renaming one needs a minor release.
See Profiles.
Do not add
- A second constructor for a browser, brand, or platform
(
Session::chrome(),SessionBuilder::macos()). Usebrowser,platform,brand. - A per-field copy of a config type on
SessionBuilderorRequestBuilder(connect_timeout,resolve_host,add_root_certificate_der,max_redirects). Add the field to the config type. - A second setter with other header semantics (
append_header,set_header) or a typed shortcut for one header (referer).SessionBuilder::user_agentis the one session shortcut, because it replaces a profile default. - A second request type.
Session::executetakeshttp::Request<Body>. - A second middleware model inside the send path. Wrap
LeylineServicewith tower layers. - A second body reader (
text_utf8,into_text,as_bytes) or a secondContent-Encodingdecoder. - A second cookie store, such as a per-response cookie map.
- A getter that copies session configuration back out. The exceptions
report the state of a built session:
Session::identity,Session::proxy_url,Session::cookies,Session::pool_stats, andSession::host_stats. - A second timing, retry, or redirect owner.
- A browser, version, or brand table in Rust. Profile data owns it.
leyline-http API
Generated by truesight from the compiler’s view of leyline-http 0.1.5 (features: default, full, test-util; toolchain nightly-2026-09-16; rustdoc format 61). Do not edit this page: change the code or api/leyline-http.toml, then run cargo truesight sync.
1194 public items (148 types, 13 functions, 488 methods, 306 fields, 239 variants) at 1194 paths; 0 are re-export aliases. 711 trait impl lines, 77 inherent impl lines. 9 modules, 0 re-export only. api.txt lists the public items and impls, one per line; a pub use path repeats the item with its fields, variants, or trait items, while impls and their methods appear once, under the path where the item is defined. API changes lists the changes in each release.
Tasks
Modules
| Module | Items | Types | Functions | Methods | Aliases |
|---|---|---|---|---|---|
leyline | 744 | 85 | 3 | 374 | 0 |
leyline::audit | 63 | 8 | 4 | 9 | 0 |
leyline::cookie | 35 | 4 | 0 | 18 | 0 |
leyline::html | 35 | 5 | 5 | 20 | 0 |
leyline::multipart | 14 | 2 | 0 | 12 | 0 |
leyline::profile | 174 | 22 | 0 | 10 | 0 |
leyline::testing | 34 | 4 | 1 | 20 | 0 |
leyline::tls | 8 | 4 | 0 | 4 | 0 |
leyline::trace | 87 | 14 | 0 | 21 | 0 |
Features
| Feature | Adds |
|---|---|
charset | Response::text_with_charset |
multipart | RequestBuilder::multipart, multipart |
html | Crawl, CrawlPage, CrawlStats, Respect, Scope, Session::crawl, SkipReason, Tab::submit_form, html |
websocket | CloseFrame, Session::websocket, WebSocketBuilder, WsConnection, WsMessage, WsSink, WsStream |
http3 | ProtocolPolicy::Http3, ProtocolPolicy::Race |
socks | no public items |
tower | LeylineService |
test-util | testing |
Examples
| Example | Run |
|---|---|
api_client | cargo run -p leyline-http --example api_client |
audit | cargo run -p leyline-http --example audit |
browser_tab | cargo run -p leyline-http --example browser_tab --features html |
concurrent | cargo run -p leyline-http --example concurrent |
cookies | cargo run -p leyline-http --example cookies |
crawler | cargo run -p leyline-http --example crawler |
device | cargo run -p leyline-http --example device --features html |
download | cargo run -p leyline-http --example download |
headers | cargo run -p leyline-http --example headers |
http3 | cargo run -p leyline-http --example http3 --features http3 |
multipart | cargo run -p leyline-http --example multipart --features multipart |
oneliner | cargo run -p leyline-http --example oneliner |
post_json | cargo run -p leyline-http --example post_json |
presets | cargo run -p leyline-http --example presets |
retry | cargo run -p leyline-http --example retry |
stock_monitor | cargo run -p leyline-http --example stock_monitor |
streaming | cargo run -p leyline-http --example streaming |
trace | cargo run -p leyline-http --example trace |
websocket | cargo run -p leyline-http --example websocket --features websocket |
Module leyline
| Item | Kind | Description |
|---|---|---|
Body | struct | |
Error | struct | |
BlockRules | struct | |
BlockSignal | struct | |
BodyStream | struct | |
BrowserProfile | struct | |
CloseFrame | struct | Requires feature websocket. |
CompressionConfig | struct | |
Crawl | struct | Requires feature html. |
CrawlPage | struct | Requires feature html. |
CrawlStats | struct | Requires feature html. |
Device | struct | |
DeviceAutosave | struct | |
DeviceAutosaveOptions | struct | |
DigestAuth | struct | |
DnsConfig | struct | |
HostLimits | struct | |
HostStats | struct | |
Identity | struct | |
LeylineService | struct | Requires feature tower. |
Link | struct | |
NoProxy | struct | |
Pages | struct | |
PoolConfig | struct | |
PoolStats | struct | |
PrefixRead | struct | |
ProxyConfig | struct | |
ProxyHealth | struct | |
ProxyPool | struct | |
ProxyRule | struct | |
ProxyUrl | struct | |
RedirectAttempt | struct | |
RedirectPolicy | struct | |
RequestBuilder | struct | |
Response | struct | |
ResponseTiming | struct | |
RetryPolicy | struct | |
Robots | struct | |
Session | struct | |
SessionBuilder | struct | |
SessionIdentity | struct | |
SessionState | struct | |
SocketConfig | struct | |
Tab | struct | |
TcpProfile | struct | |
TimeoutConfig | struct | |
TlsInfo | struct | |
TlsTrustConfig | struct | |
WebSocketBuilder | struct | Requires feature websocket. |
WebSocketConfig | struct | |
WsConnection | struct | Requires feature websocket. |
WsSink | struct | Requires feature websocket. |
WsStream | struct | Requires feature websocket. |
BlockKind | enum | |
Browser | enum | |
ChromiumBrand | enum | |
ContentEncoding | enum | |
ErrorCategory | enum | |
ErrorCode | enum | |
Family | enum | |
FetchSite | enum | |
H2Error | enum | |
HeaderAnchor | enum | |
HttpVersion | enum | |
Kind | enum | |
Platform | enum | |
Preset | enum | |
ProtocolPolicy | enum | |
ProxyReply | enum | |
Purpose | enum | |
RedirectAction | enum | |
RelayBody | enum | |
Respect | enum | Requires feature html. |
RetryTrigger | enum | |
RobotsSource | enum | |
Scope | enum | Requires feature html. |
SkipReason | enum | Requires feature html. |
StopReason | enum | |
TlsError | enum | |
TlsMinVersion | enum | |
WaitFormat | enum | |
WsMessage | enum | Requires feature websocket. |
IntoParamPair | trait | |
IntoUrl | trait | URL input. |
get | fn | Send one GET. |
redact_url | fn | |
relay_headers | fn | Relay a header map. |
Result | type | |
Url | use | |
http | use | |
alloc::sync::Arc | impl | |
alloc::string::String | impl | |
(K, V) | impl | |
str | impl | |
url::Url | impl |
Structs
Body
Methods
| Method | Description |
|---|---|
len_hint(&self) -> Option<u64> | |
stream<S>(stream: S, length: Option<u64>) -> Self where S: Stream<Item = Result<Bytes>> + Send + ’static |
Trait implementations: From<Form>, From<&’static [u8]>, From<&’static str>, From<()>, From<String>, From<Vec<u8>>, From<Bytes>, Default, Debug, Stream
Error
Methods
| Method | Description |
|---|---|
attempts(&self) -> u32 | |
body(&self) -> Option<&[u8]> | |
body_text(&self) -> Option<Cow<’_, str>> | |
find<’a>(error: &’a (dyn Error + ’static)) -> Option<&’a Error> | Find a leyline error in a chain. |
h2(&self) -> Option<&H2Error> | |
header(&self, name: &str) -> Option<&str> | |
headers(&self) -> Option<&HeaderMap> | |
io(&self) -> Option<&Error> | |
kind(&self) -> Kind | Errors. |
proxy(&self) -> Option<&str> | |
retries_exhausted(&self) -> bool | |
retry_after(&self) -> Option<Duration> | |
status(&self) -> Option<StatusCode> | |
tls(&self) -> Option<&TlsError> | |
url(&self) -> Option<&Url> | |
category(&self) -> ErrorCategory | Error category. |
is_dns(&self) -> bool | DNS failure. |
is_body_limit(&self) -> bool | Body limit. |
is_connect(&self) -> bool | Connect failure. |
is_proxy(&self) -> bool | Proxy failure. |
is_retryable(&self) -> bool | Retryable error. |
is_status(&self) -> bool | Status error. |
is_timeout(&self) -> bool | Timeout. |
is_profile_changed(&self) -> bool | |
is_shut_down(&self) -> bool | Detect a stopped session. |
Trait implementations: From<H2Error>, From<TlsError>, From<Error>, From<InvalidHeaderName>, From<InvalidHeaderValue>, From<InvalidUri>, From<ParseError>, Error, Debug, Display
BlockRules
Methods
| Method | Description |
|---|---|
builtin() -> &’static BlockRules | |
check(&self, response: &Response) -> Option<BlockSignal> | |
extend(&mut self, other: BlockRules) | |
from_toml(source: &str) -> Result<Self> | Load block rules from TOML. |
statuses(statuses: impl IntoIterator<Item = u16>) -> Self | Block rules. |
Trait implementations: Clone, Default, Debug
BlockSignal
Fields
| Field | Description |
|---|---|
kind: BlockKind | |
vendor: String |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
BodyStream
Trait implementations: Debug, Stream
BrowserProfile
Methods
| Method | Description |
|---|---|
expected_h2_fingerprint(&self) -> Option<&str> | |
expected_ja4(&self) -> Option<&str> | |
from_toml(toml_str: &str) -> Result<Self, ProfileError> | Parse a profile from TOML. |
from_fingerprint(spec: FingerprintSpec) -> Result<Self> |
Fields
| Field | Description |
|---|---|
h2: H2Profile | |
h3: Option<H3Profile> | |
identity: HashMap<String, PlatformIdentity> | |
meta: ProfileMeta | |
tls: TlsProfile |
Trait implementations: Clone, Debug, Deserialize<’de>
CloseFrame
Requires feature websocket.
Methods
| Method | Description |
|---|---|
new(code: u16, reason: impl Into<String>) -> Self | Requires feature websocket. |
Fields
| Field | Description |
|---|---|
code: u16 | Requires feature websocket. |
reason: String | Requires feature websocket. |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
CompressionConfig
Methods
| Method | Description |
|---|---|
brotli(self, on: bool) -> Self | |
deflate(self, on: bool) -> Self | |
gzip(self, on: bool) -> Self | |
max_body_size(self, bytes: usize) -> Self | Response body cap. |
max_error_body(self, bytes: usize) -> Self | Status-error body cap. |
new() -> Self | |
none() -> Self | |
zstd(self, on: bool) -> Self |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
Crawl
Requires feature html.
Methods
| Method | Description |
|---|---|
host_limits(self, limits: HostLimits) -> Self | Pace a crawl per host. Requires feature html. |
ignore_robots(self) -> Self | Requires feature html. |
limit(self, pages: usize) -> Self | Limit a crawl’s pages. Requires feature html. |
async next(&mut self) -> Option<Result<CrawlPage>> | Requires feature html. |
respect(self, respect: Respect) -> Self | Obey robots rules in a crawl. Requires feature html. |
scope(self, scope: Scope) -> Self | Requires feature html. |
stats(&self) -> &CrawlStats | Count what a crawl skipped. Requires feature html. |
Trait implementations: Debug, Stream
CrawlPage
Requires feature html.
Methods
| Method | Description |
|---|---|
depth(&self) -> usize | Requires feature html. |
html(&self) -> &str | Requires feature html. |
into_html(self) -> String | Requires feature html. |
meta(&self, name: &str) -> Option<String> | Requires feature html. |
status(&self) -> StatusCode | Requires feature html. |
text(&self) -> &str | Requires feature html. |
url(&self) -> &Url | Requires feature html. |
Trait implementations: Clone, Debug
CrawlStats
Requires feature html.
Methods
| Method | Description |
|---|---|
errors(&self) -> u64 | Requires feature html. |
pages(&self) -> u64 | Requires feature html. |
requests(&self) -> u64 | Requires feature html. |
skipped(&self, reason: SkipReason) -> u64 | Requires feature html. |
Trait implementations: Clone, Default, Debug, Display
Device
Methods
| Method | Description |
|---|---|
autosave(&self, session: &Session, path: impl Into<PathBuf>, options: impl Into<DeviceAutosaveOptions>) -> DeviceAutosave | Save a device as it changes. |
capture(session: &Session, proxy: Option<ProxyUrl>) -> Device | Capture a device. |
load_from(path: impl AsRef<Path>) -> Result<Device> | |
open(&self) -> Result<Session> | Open a saved device. |
pin_profile(&mut self, session: &Session) -> Result<()> | |
save_to(&self, path: impl AsRef<Path>) -> Result<()> | |
session_builder(&self) -> Result<SessionBuilder> | |
tab(&self, session: &Session) -> Tab | |
check(&self, session: &Session) -> Result<()> | Check a session against a device. |
Fields
| Field | Description |
|---|---|
app: BTreeMap<String, Value> | |
brand: Option<ChromiumBrand> | |
env_proxy: bool | |
identity: Option<Identity> | |
jar: Option<Jar> | |
jar_path: Option<PathBuf> | |
languages: Option<Vec<String>> | |
page: Option<Url> | |
platform: Platform | |
profile_id: Option<String> | |
profile_toml: Option<String> | |
proxy: Option<ProxyUrl> | |
proxy_password_env: Option<String> | |
state: SessionState | |
strict: bool | |
user_agent: Option<String> |
Trait implementations: Clone, Debug, Serialize, Deserialize<’de>
DeviceAutosave
Methods
| Method | Description |
|---|---|
device(&self) -> Device | |
async flush(&self) -> Result<()> | |
async shutdown(self) -> Result<()> | |
track(&self, tab: &Tab) | |
update(&self, change: impl FnOnce(&mut Device)) |
Trait implementations: Debug
DeviceAutosaveOptions
Methods
| Method | Description |
|---|---|
const new(interval: Duration) -> Self | Device save intervals. |
const state_interval(self, state_interval: Duration) -> Self |
Fields
| Field | Description |
|---|---|
interval: Duration | |
state_interval: Duration |
Trait implementations: Clone, Eq, PartialEq, From<Duration>, Debug, Copy, StructuralPartialEq
DigestAuth
Methods
| Method | Description |
|---|---|
new(username: impl Into<String>, password: impl Into<String>) -> Self |
Trait implementations: Clone, Debug
DnsConfig
Methods
| Method | Description |
|---|---|
new() -> Self | |
resolve_host(self, host: impl AsRef<str>, addrs: impl IntoIterator<Item = SocketAddr>) -> Self | |
resolver(self, resolver: Arc<dyn Resolver>) -> Self |
Trait implementations: Clone, From<Arc<dyn Resolver>>, Default, Debug
HostLimits
Methods
| Method | Description |
|---|---|
host(self, host: &str, limits: HostLimits) -> Self | Limits for one host. |
max_in_flight(self, n: usize) -> Self | Requests in flight per host. |
max_pause(self, max: Duration) -> Self | Cap a server-requested pause. |
max_total_in_flight(self, n: usize) -> Self | Requests in flight in total. |
new() -> Self | |
pause_for(self, default: Duration) -> Self | Default pause length. |
pause_on(self, statuses: impl IntoIterator<Item = u16>) -> Self | Pause a host on a status. |
per_second(self, rate: f64) -> Self | Requests per second per host. |
Trait implementations: Clone, Default, Debug
HostStats
Methods
| Method | Description |
|---|---|
in_flight(&self) -> usize | |
origin(&self) -> &str | |
waiting(&self) -> usize |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
Identity
Methods
| Method | Description |
|---|---|
brand(self) -> Option<ChromiumBrand> | |
http(self) -> Browser | |
locked(browser: Browser, platform: Platform) -> Self | |
platform(self) -> Platform | |
rotate_hello(self) -> Result<Self> | |
rotate_tls(self, tls: Browser) -> Result<Self> | Rotate the TLS side of an identity. |
switch_family(self, dest: Browser) -> Result<Self> | |
tls(self) -> Browser | |
with_brand(self, brand: ChromiumBrand) -> Self |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq, Serialize, Deserialize<’de>
LeylineService
Requires feature tower.
Methods
| Method | Description |
|---|---|
new(session: Session) -> Self | tower. Requires feature tower. |
Trait implementations: Clone, Debug, Service<Request<Body>>
Link
Methods
| Method | Description |
|---|---|
has_rel(&self, rel: &str) -> bool |
Fields
| Field | Description |
|---|---|
params: Vec<(String, String)> | |
rel: Vec<String> | |
url: Url |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
NoProxy
Methods
| Method | Description |
|---|---|
new<I, S>(patterns: I) -> Self where I: IntoIterator<Item = S>, S: Into<String> |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq
Pages
Methods
| Method | Description |
|---|---|
limit(self, max: usize) -> Self | Limit pagination. |
async next(&mut self) -> Option<Result<Response>> |
Trait implementations: Debug, Stream
PoolConfig
Methods
| Method | Description |
|---|---|
h2_ping_after_idle(self, d: impl Into<Option<Duration>>) -> Self | |
h2_ping_timeout(self, d: Duration) -> Self | |
idle_timeout(self, d: Duration) -> Self | |
keepalive(self, on: bool) -> Self | |
max_connections(self, n: usize) -> Self | |
max_h1_conns_per_host(self, n: usize) -> Self | |
new() -> Self |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
PoolStats
Fields
| Field | Description |
|---|---|
busy: usize | |
entries: usize | |
evictions_dead: u64 | |
evictions_idle: u64 | |
evictions_lru: u64 | |
h1_hits: u64 | |
h1_misses: u64 | |
h2_hits: u64 | |
h2_misses: u64 | |
h2_ping_failures: u64 | |
h3_hits: u64 | |
h3_misses: u64 | |
idle: usize | |
installs: u64 | |
max_connections: usize | |
stale_probed: u64 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
PrefixRead
Fields
| Field | Description |
|---|---|
bytes: Vec<u8> | |
stopped_by: StopReason |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
ProxyConfig
Methods
| Method | Description |
|---|---|
env(self, on: bool) -> Self | |
new() -> Self | |
no_proxy(self, no_proxy: NoProxy) -> Self | |
rule(self, rule: ProxyRule) -> Self |
Trait implementations: Clone, From<&String>, From<&str>, From<String>, From<ProxyUrl>, Default, Debug
ProxyHealth
Fields
| Field | Description |
|---|---|
banned_until: Option<Instant> | |
failures: u32 | |
in_use: u32 | |
proxy: String |
Trait implementations: Clone, Debug
ProxyPool
Methods
| Method | Description |
|---|---|
ban_after(self, failures: u32) -> Self | Ban a failing proxy. |
ban_for(self, duration: Duration) -> Self | |
identified<P: Into<ProxyConfig>>(proxies: impl IntoIterator<Item = (P, Identity)>) -> Self | Pin an identity to each proxy. |
new<P: Into<ProxyConfig>>(proxies: impl IntoIterator<Item = P>) -> Self | Build a proxy pool. |
rotate_on_block(self, rules: BlockRules) -> Self | Rotate the proxy on a block. |
stats(&self) -> Vec<ProxyHealth> | Proxy health. |
sticky_for(self, duration: Duration) -> Self | Keep a proxy for a time. |
Trait implementations: Clone, Debug
ProxyRule
Methods
| Method | Description |
|---|---|
all(proxy_url: impl Into<String>) -> Self | |
http(proxy_url: impl Into<String>) -> Self | |
https(proxy_url: impl Into<String>) -> Self |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
ProxyUrl
Methods
| Method | Description |
|---|---|
parse(raw: impl AsRef<str>) -> Result<Self> |
Trait implementations: Clone, Eq, PartialEq, TryFrom<String>, Debug, Display, Hash, StructuralPartialEq, Serialize, Deserialize<’de>
RedirectAttempt
Fields
| Field | Description |
|---|---|
location: Option<&’a str> | |
previous: &’a [Url] | |
status: u16 | |
url: &’a Url |
Trait implementations: Debug, Clone, Copy
RedirectPolicy
Methods
| Method | Description |
|---|---|
custom<F>(f: F) -> Self where F: Fn(RedirectAttempt<’_>) -> RedirectAction + Send + Sync + ’static | |
limited(max: usize) -> Self | |
none() -> Self |
Trait implementations: Clone, Default, Debug
RequestBuilder
Methods
| Method | Description |
|---|---|
anchored(self, anchor: HeaderAnchor, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) -> Self | |
basic_auth(self, username: &str, password: &str) -> Self | Basic auth. |
bearer_auth(self, token: &str) -> Self | Bearer auth. |
body(self, body: impl Into<Body>) -> Self | Raw or streamed body. |
compress(self, encoding: ContentEncoding) -> Self | Compress a request body. |
digest_auth(self, auth: DigestAuth) -> Self | Digest auth. |
form<I, P>(self, params: I) -> Self where I: IntoIterator<Item = P>, P: IntoParamPair | Form body. |
header(self, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) -> Self | Headers. |
header_order(self, order: &[&str]) -> Self | Header order. |
headers<I, P>(self, headers: I) -> Self where I: IntoIterator<Item = P>, P: IntoParamPair | |
json(self, value: &impl Serialize) -> Self | JSON body. |
multipart(self, form: Form) -> Self | Multipart body. Requires feature multipart. |
preset(self, preset: Preset) -> Self | Set the fetch context. |
proxy(self, config: impl Into<ProxyConfig>) -> Self | Proxy one request. |
query<I, P>(self, params: I) -> Self where I: IntoIterator<Item = P>, P: IntoParamPair | Query. |
redirect(self, policy: RedirectPolicy) -> Self | |
retry(self, policy: RetryPolicy) -> Self | |
stream(self) -> Self | Stream a response. |
timeout(self, config: impl Into<TimeoutConfig>) -> Self | Request timeout. |
cookie_jar(self, jar: Jar) -> Self | |
async download(self, path: impl AsRef<Path>, limit: Option<u64>) -> Result<u64> | Download to a file. |
error_for_status(self) -> Self | Status check after retries, with the body. |
initiator(self, page: impl IntoUrl) -> Self | |
async read_until<F>(self, limit: usize, done: F) -> Result<PrefixRead> where F: FnMut(&[u8], usize) -> bool | |
tag(self, tag: impl Into<String>) -> Self | |
pages(self) -> Pages | Follow Link pagination. |
async send(self) -> Result<Response> | Send. |
Trait implementations: Debug, IntoFuture
Response
Methods
| Method | Description |
|---|---|
attempts(&self) -> u32 | |
audit(&self) -> Option<&AuditData> | |
block(&self) -> Option<BlockSignal> | Detect a block page. |
content_length(&self) -> Option<u64> | |
cookies(&self) -> impl Iterator<Item = Cookie> + ’_ | Response cookies. |
error_for_status(self) -> Result<Self> | Status check. |
error_for_status_ref(&self) -> Result<&Self> | |
header(&self, name: &str) -> Option<&str> | |
headers(&self) -> &HeaderMap | Response headers. |
proxy(&self) -> Option<&str> | |
redirect_chain(&self) -> &[Url] | |
request_headers(&self) -> impl Iterator<Item = (&str, &str)> | |
status(&self) -> StatusCode | |
timing(&self) -> &ResponseTiming | Timing. |
tls(&self) -> Option<&TlsInfo> | TLS details. |
trailers(&self) -> impl Iterator<Item = (&HeaderName, &HeaderValue)> | |
url(&self) -> &Url | |
version(&self) -> HttpVersion | |
async bytes(self) -> Result<Bytes> | Read body. |
async copy_decoded_to<W>(self, writer: &mut W, limit: Option<u64>) -> Result<u64> where W: AsyncWrite + Unpin | Copy the decoded body to a writer. |
async copy_to<W>(self, writer: &mut W) -> Result<u64> where W: AsyncWrite + Unpin | Save a body to a file. |
into_decoded_stream(self, limit: Option<u64>) -> Result<BodyStream> | Stream the decoded body. |
into_stream(self) -> Result<BodyStream> | Stream body. |
async json<T: DeserializeOwned>(self) -> Result<T> | Read the body as JSON. |
async read_until<F>(self, limit: usize, done: F) -> Result<Vec<u8>> where F: FnMut(&[u8], usize) -> bool | Read a prefix of a body. |
async text(self) -> Result<String> | Read the body as text. |
async text_with_charset(self, default_encoding: &str) -> Result<String> | Requires feature charset. |
async text_with_charset(self, _default_encoding: &str) -> Result<String> | Requires feature charset. |
async download_to(self, path: impl AsRef<Path>, limit: Option<u64>) -> Result<u64> | Download a received response. |
link(&self, rel: &str) -> Option<Url> | Read one Link relation. |
links(&self) -> Vec<Link> | Read Link headers. |
relay_headers(&self, body: RelayBody) -> HeaderMap | Relay response headers. |
Trait implementations: Debug
ResponseTiming
Fields
| Field | Description |
|---|---|
connect_ms: Option<u32> | |
reused: bool | |
send_ms: u32 | |
total_ms: u32 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
RetryPolicy
Methods
| Method | Description |
|---|---|
allow_non_idempotent(self, allow: bool) -> Self | |
backoff(&self, attempt: u32) -> Duration | |
backoff_factor(self, factor: f64) -> Self | |
initial_backoff(self, d: Duration) -> Self | |
jitter(self, on: bool) -> Self | |
max_backoff(self, d: Duration) -> Self | |
max_retries(self, n: u32) -> Self | |
max_retry_after(self, d: Duration) -> Self | Cap a server-requested wait. |
none() -> Self | |
on_status(self, code: u16) -> Self | |
retry_if<F>(self, f: F) -> Self where F: Fn(&Response) -> bool + Send + Sync + RefUnwindSafe + ’static | Retry on a condition. |
retry_on(self, triggers: impl IntoIterator<Item = RetryTrigger>) -> Self | Retry triggers. |
retry_unsent(self, enabled: bool) -> Self | Retry a request that was not sent. |
rotate_proxies<P: Into<ProxyConfig>>(self, proxies: impl IntoIterator<Item = P>) -> Self | Rotate proxies on retry. |
skip_blocks(self, rules: BlockRules) -> Self | Return a block without a retry. |
transient() -> Self | |
wait_header(self, name: impl Into<String>, format: WaitFormat) -> Self | Wait on a rate-limit header. |
Trait implementations: Clone, Default, Debug
Robots
Methods
| Method | Description |
|---|---|
allows(&self, url: &Url) -> bool | |
allows_use(&self, url: &Url, purpose: Purpose) -> bool | |
crawl_delay(&self) -> Option<Duration> | |
parse(text: &str, agent: &str) -> Robots | Parse a robots.txt file. |
sitemaps(&self) -> &[String] | |
source(&self) -> RobotsSource |
Trait implementations: Clone, Debug
Session
Methods
| Method | Description |
|---|---|
browser(browser: Browser) -> Self | One-line browser session. |
builder() -> SessionBuilder | |
cookies(&self) -> &Jar | |
delete(&self, url: impl IntoUrl) -> RequestBuilder | |
async execute(&self, req: Request<Body>) -> Result<Response> | Prebuilt request. |
fresh_pool(&self) -> Self | Open new connections through the same proxy. |
get(&self, url: impl IntoUrl) -> RequestBuilder | |
head(&self, url: impl IntoUrl) -> RequestBuilder | |
host_stats(&self) -> Vec<HostStats> | Read per-host queues. |
new() -> Self | Default session. |
patch(&self, url: impl IntoUrl) -> RequestBuilder | |
pool_stats(&self) -> PoolStats | |
post(&self, url: impl IntoUrl) -> RequestBuilder | |
async preconnect(&self, url: impl IntoUrl) -> Result<()> | Warm a connection. |
proxy_url(&self) -> Option<ProxyUrl> | |
put(&self, url: impl IntoUrl) -> RequestBuilder | |
request(&self, method: Method, url: impl IntoUrl) -> RequestBuilder | Build a request. |
tab(&self) -> Tab | Keep the current page. |
with_base_url(&self, url: impl IntoUrl) -> Result<Self> | Derive a session with another base URL. |
with_cookie_jar(&self, jar: Jar) -> Self | Derive a session with another jar. |
with_proxy(&self, config: impl Into<ProxyConfig>) -> Self | Derive a session with another proxy. |
with_redirect(&self, policy: RedirectPolicy) -> Self | |
crawl(&self, seed: impl IntoUrl) -> Crawl | Crawl a site. Requires feature html. |
identity(&self) -> SessionIdentity | What a session sends. |
with_identity(&self, identity: Identity) -> Result<Self> | Derive a session with another identity. |
is_shut_down(&self) -> bool | |
shutdown(&self) | Stop every clone of a session. |
async robots(&self, url: impl IntoUrl, agent: &str) -> Result<Robots> | Fetch and obey robots.txt. |
state(&self) -> SessionState | Save TLS tickets, Alt-Svc, and HSTS. |
websocket(&self, url: impl IntoUrl) -> WebSocketBuilder | WebSocket. Requires feature websocket. |
Trait implementations: Clone, Default, Debug, Display
SessionBuilder
Methods
| Method | Description |
|---|---|
audit(self, enabled: bool) -> Self | Fingerprint audit. |
base_url(self, url: impl IntoUrl) -> Self | Session base URL. |
brand(self, brand: ChromiumBrand) -> Self | Brand overlay. |
browser(self, browser: Browser) -> Self | Pick a browser. |
build(self) -> Result<Session> | |
compression(self, config: CompressionConfig) -> Self | Decompress. |
cookie_jar(self, jar: Jar) -> Self | Cookies. |
dns(self, config: impl Into<DnsConfig>) -> Self | DNS. |
expect_profile_id(self, id: &str) -> Self | Expect a profile id. |
headers<I, P>(self, headers: I) -> Self where I: IntoIterator<Item = P>, P: IntoParamPair | Session default headers. |
host_limits(self, limits: HostLimits) -> Self | Limit each host. |
https_only(self, enabled: bool) -> Self | Refuse plain HTTP. |
identity(self, id: Identity) -> Self | Mix TLS and HTTP identities. |
languages<I, S>(self, langs: I) -> Self where I: IntoIterator<Item = S>, S: AsRef<str> | Session languages. |
platform(self, platform: Platform) -> Self | Pick a platform. |
pool(self, config: PoolConfig) -> Self | Pool. |
profile(self, profile: BrowserProfile) -> Self | Send a loaded profile. |
protocol(self, policy: ProtocolPolicy) -> Self | Protocol. |
proxy(self, config: impl Into<ProxyConfig>) -> Self | Proxy. |
proxy_pool(self, pool: ProxyPool) -> Self | Proxy pool. |
redirect(self, policy: RedirectPolicy) -> Self | Redirect. |
retry(self, policy: RetryPolicy) -> Self | Retry. |
socket(self, config: SocketConfig) -> Self | Socket and connect tuning. |
tcp_profile(self, profile: TcpProfile) -> Self | TCP fingerprint. |
timeout(self, config: impl Into<TimeoutConfig>) -> Self | Session timeout. |
tls_trust(self, trust: TlsTrustConfig) -> Self | TLS trust. |
trace(self, hook: impl Trace) -> Self | Trace. |
user_agent(self, value: &str) -> Self | Session user agent. |
websocket_config(self, config: WebSocketConfig) -> Self | Set WebSocket limits. |
bearer_auth(self, token: &str) -> Self | Session bearer token. |
Trait implementations: Debug
SessionIdentity
Methods
| Method | Description |
|---|---|
brand(&self) -> Option<ChromiumBrand> | |
browser(&self) -> Option<Browser> | |
export_profile(&self) -> Option<String> | |
identity(&self) -> Option<Identity> | |
languages(&self) -> Option<&[String]> | |
platform(&self) -> Platform | |
profile_id(&self) -> Option<&str> | |
to_identity(&self) -> Option<Identity> | |
user_agent(&self) -> &str |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
SessionState
Methods
| Method | Description |
|---|---|
is_empty(&self) -> bool | |
restore_into(&self, session: &Session) | Restore saved session state. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq, Serialize, Deserialize<’de>
SocketConfig
Methods
| Method | Description |
|---|---|
happy_eyeballs(self, config: impl Into<Option<HappyEyeballsConfig>>) -> Self | |
local_address(self, addr: impl Into<Option<IpAddr>>) -> Self | |
local_ipv4(self, addr: impl Into<Option<Ipv4Addr>>) -> Self | |
local_ipv6(self, addr: impl Into<Option<Ipv6Addr>>) -> Self | |
new() -> Self | |
recv_buffer_size(self, n: impl Into<Option<usize>>) -> Self | |
send_buffer_size(self, n: impl Into<Option<usize>>) -> Self | |
strict(self, on: bool) -> Self | |
tcp_keepalive(self, d: impl Into<Option<Duration>>) -> Self | |
tcp_keepalive_interval(self, d: impl Into<Option<Duration>>) -> Self | |
tcp_keepalive_retries(self, n: impl Into<Option<u32>>) -> Self | |
tcp_nodelay(self, on: impl Into<Option<bool>>) -> Self | |
tcp_user_timeout(self, d: impl Into<Option<Duration>>) -> Self |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq
Tab
Methods
| Method | Description |
|---|---|
current(&self) -> Option<Url> | |
fetch(&self, method: Method, url: impl IntoUrl) -> RequestBuilder | Send a script fetch from a tab. |
async follow(&self, url: impl IntoUrl) -> Result<Response> | |
async open(&self, url: impl IntoUrl) -> Result<Response> | Browse with a tab. |
post_json(&self, url: impl IntoUrl, body: &impl Serialize) -> RequestBuilder | Post JSON from a tab. |
session(&self) -> &Session | |
set_current(&self, page: Option<Url>) | |
async submit<I, P>(&self, url: impl IntoUrl, fields: I) -> Result<Response> where I: IntoIterator<Item = P>, P: IntoParamPair | |
async submit_form(&self, form: &Form) -> Result<Response> | Submit a page form. Requires feature html. |
subresource(&self, url: impl IntoUrl, preset: Preset) -> RequestBuilder | Load a subresource from a tab. |
xhr(&self, url: impl IntoUrl) -> RequestBuilder | Send an XHR from a tab. |
Trait implementations: Clone, Debug
TcpProfile
Fields
| Field | Description |
|---|---|
df: bool | |
mss: u32 | |
no_delay: bool | |
options: Vec<u8> | |
ttl: u32 | |
window_scale: u32 | |
window_size: u32 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq, Deserialize<’de>
TimeoutConfig
Methods
| Method | Description |
|---|---|
body(self, d: impl Into<Option<Duration>>) -> Self | Body read timeout. |
connect(self, d: impl Into<Option<Duration>>) -> Self | |
error_body(self, d: impl Into<Option<Duration>>) -> Self | Status-error body read timeout. |
new() -> Self | |
read(self, d: impl Into<Option<Duration>>) -> Self | |
response_header(self, d: impl Into<Option<Duration>>) -> Self | |
total(self, d: impl Into<Option<Duration>>) -> Self |
Trait implementations: Clone, Eq, PartialEq, From<Duration>, Default, Debug, Copy, StructuralPartialEq
TlsInfo
Fields
| Field | Description |
|---|---|
cipher: Option<String> | |
peer_cert_der: Option<Vec<u8>> | |
version: Option<String> |
Trait implementations: Clone, Default, Debug
TlsTrustConfig
Methods
| Method | Description |
|---|---|
add_ca_der(self, der: impl Into<Vec<u8>>) -> Self | |
add_ca_file(self, path: impl Into<PathBuf>) -> Self | |
add_pinned_leaf_sha256(self, sha256: [u8; 32]) -> Self | |
client_identity(self, certificate_chain_file: impl Into<PathBuf>, private_key_file: impl Into<PathBuf>) -> Self | |
danger_accept_invalid_certs(self, accept: bool) -> Self | |
env_roots(self, on: bool) -> Self | |
min_tls_version(self, version: TlsMinVersion) -> Self | Set a TLS version floor. |
new() -> Self | |
system_roots(self, on: bool) -> Self |
Trait implementations: Clone, Default, Debug
WebSocketBuilder
Requires feature websocket.
Methods
| Method | Description |
|---|---|
config(self, config: WebSocketConfig) -> Self | Requires feature websocket. |
async connect(self) -> Result<WsConnection> | Requires feature websocket. |
header(self, name: impl TryInto<HeaderName>, value: impl TryInto<HeaderValue>) -> Self | Requires feature websocket. |
headers<I, P>(self, headers: I) -> Self where I: IntoIterator<Item = P>, P: IntoParamPair | Requires feature websocket. |
proxy(self, config: impl Into<ProxyConfig>) -> Self | Requires feature websocket. |
Trait implementations: Debug, IntoFuture
WebSocketConfig
Methods
| Method | Description |
|---|---|
accept_unmasked_frames(self, on: bool) -> Self | |
max_frame_size(self, n: impl Into<Option<usize>>) -> Self | |
max_message_size(self, n: impl Into<Option<usize>>) -> Self | |
max_write_buffer_size(self, n: impl Into<Option<usize>>) -> Self | |
new() -> Self | |
prefer_http2(self, on: bool) -> Self | |
read_buffer_size(self, n: impl Into<Option<usize>>) -> Self | |
write_buffer_size(self, n: impl Into<Option<usize>>) -> Self |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
WsConnection
Requires feature websocket.
Methods
| Method | Description |
|---|---|
async close(&mut self) -> Result<()> | Requires feature websocket. |
header(&self, name: &str) -> Option<&str> | Requires feature websocket. |
protocol(&self) -> Option<&str> | Requires feature websocket. |
async recv(&mut self) -> Result<Option<WsMessage>> | Requires feature websocket. |
async send(&mut self, msg: WsMessage) -> Result<()> | Requires feature websocket. |
split(self) -> (WsSink, WsStream) | Requires feature websocket. |
Trait implementations: Debug
WsSink
Requires feature websocket.
Methods
| Method | Description |
|---|---|
async close(&mut self) -> Result<()> | Requires feature websocket. |
async send(&mut self, msg: WsMessage) -> Result<()> | Requires feature websocket. |
Trait implementations: Debug
WsStream
Requires feature websocket.
Methods
Trait implementations: Debug
Enums
BlockKind
Variants
| Variant | Description |
|---|---|
Block | |
Captcha | |
Challenge |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq, Deserialize<’de>
Browser
Methods
| Method | Description |
|---|---|
all() -> &’static [Browser] | |
family(&self) -> Family | |
for_platform(self, platform: Platform) -> Self | |
get(family: Family, version: u32) -> Option<Self> | Pin a browser version. |
id(&self) -> &’static str | |
identity(self, platform: Platform, brand: Option<ChromiumBrand>) -> Option<PlatformIdentity> | Identity values. |
latest(family: Family) -> Self | |
version(&self) -> u32 | |
profile(self) -> &’static BrowserProfile |
Variants
| Variant | Description |
|---|---|
Brave146 | |
Brave154 | |
Brave155 | |
CfnetworkIOS18 | |
CfnetworkIOS27 | |
CfnetworkMacOS26 | |
Chrome145 | |
Chrome146 | |
Chrome147 | |
Chrome148 | |
Chrome149 | |
Chrome150 | |
Chrome151 | |
Chrome152 | |
Chrome153 | |
Chrome154 | |
Chrome155 | |
Firefox148 | |
Firefox149 | |
Firefox150 | |
Firefox151 | |
Firefox152 | |
Firefox153 | |
Firefox154 | |
Firefox155 | |
Firefox156 | |
Firefox157 | |
Firefox158 | |
OkHttpAndroid10 | |
Safari18 | |
Safari26 | |
Safari27 | |
SafariIOS17 | |
SafariIOS18 | |
SafariIOS27 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Display, Hash, Copy, StructuralPartialEq, FromStr, Serialize, Deserialize<’de>
ChromiumBrand
Methods
| Method | Description |
|---|---|
all() -> &’static [ChromiumBrand] | |
id(&self) -> &’static str |
Variants
| Variant | Description |
|---|---|
Chrome | |
Edge | |
Opera |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Display, Hash, Copy, StructuralPartialEq, FromStr, Serialize, Deserialize<’de>
ContentEncoding
Variants
| Variant | Description |
|---|---|
Brotli | |
Deflate | |
Gzip | |
Zstd |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
ErrorCategory
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str | Name an error category. |
gateway_status(self) -> Option<StatusCode> |
Variants
| Variant | Description |
|---|---|
Body | |
BodyLimit | |
Config | |
Connect | |
Decode | |
Dns | |
Other | |
Protocol | |
Proxy | |
ProxyTarget | |
Redirect | |
Request | |
Status | |
Timeout | |
Tls | |
Url |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, Hash, Copy, StructuralPartialEq
ErrorCode
Methods
| Method | Description |
|---|---|
from_u32(val: u32) -> Self |
Variants
| Variant | Description |
|---|---|
Cancel = 8 | |
CompressionError = 9 | |
ConnectError = 10 | |
EnhanceYourCalm = 11 | |
FlowControlError = 3 | |
FrameSizeError = 6 | |
Http11Required = 13 | |
InadequateSecurity = 12 | |
InternalError = 2 | |
NoError = 0 | |
ProtocolError = 1 | |
RefusedStream = 7 | |
SettingsTimeout = 4 | |
StreamClosed = 5 |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
Family
Methods
| Method | Description |
|---|---|
all() -> &’static [Family] | |
id(&self) -> &’static str |
Variants
| Variant | Description |
|---|---|
Brave | |
CfNetwork | |
Chrome | |
Firefox | |
OkHttp | |
Safari | |
SafariIos |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, Hash, Copy, StructuralPartialEq, FromStr, Serialize, Deserialize<’de>
FetchSite
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str | |
of(context: &Url, request: &Url) -> Self | sec-fetch-site value. |
Variants
| Variant | Description |
|---|---|
CrossSite | |
SameOrigin | |
SameSite |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, Hash, Copy, StructuralPartialEq
H2Error
Fields
| Field | Description |
|---|---|
Connection::code: ErrorCode | |
Connection::reason: String | |
FrameTooLarge::max: u32 | |
FrameTooLarge::size: u32 | |
Stream::code: ErrorCode | |
Stream::stream_id: u32 |
Variants
| Variant | Description |
|---|---|
Connection | |
FrameTooLarge | |
Hpack(String) | |
Io(Error) | |
RequestBody(Error) | |
Stream |
Trait implementations: From<Error>, Error, Debug, Display
HeaderAnchor
Variants
| Variant | Description |
|---|---|
AfterAccept | |
AfterCchUa | |
AfterCchUaMobile | |
AfterCchUaPlatform | |
AfterContentType | |
AfterFetchDest | |
AfterUserAgent | |
BeforeAcceptEncoding | |
BeforeCchUa |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
HttpVersion
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str |
Variants
| Variant | Description |
|---|---|
Http1_1 | |
Http2 | |
Http3 |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
Kind
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str |
Variants
| Variant | Description |
|---|---|
Body | |
Config | |
Connect | |
Decode | |
Http2 | |
Http3 | |
Io | |
Json | |
Proxy | |
Redirect | |
Request | |
Status | |
Timeout | |
Tls | |
Url |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, Hash, Copy, StructuralPartialEq
Platform
Methods
| Method | Description |
|---|---|
all() -> &’static [Platform] | |
id(&self) -> &’static str | |
tcp_profile(&self) -> TcpProfile |
Variants
| Variant | Description |
|---|---|
Android | |
Host | |
IOS | |
Linux | |
MacOS | |
Windows |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Display, Hash, Copy, StructuralPartialEq, FromStr, Serialize, Deserialize<’de>
Preset
Variants
| Variant | Description |
|---|---|
CrossOrigin | |
Form | |
FormNavigate | |
FrameNavigate | |
Image | |
Native | |
Navigate | |
Reload | |
SameSite | |
Script | |
Xhr |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq, Deserialize<’de>
ProtocolPolicy
Variants
| Variant | Description |
|---|---|
Auto | |
Http1 | |
Http2 | |
Http3 | Requires feature http3. |
Race | Requires feature http3. |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
ProxyReply
Variants
| Variant | Description |
|---|---|
HttpStatus(u16) | |
Socks5(u8) |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
Purpose
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str |
Variants
| Variant | Description |
|---|---|
AiInput | |
AiTrain | |
Search |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
RedirectAction
Variants
| Variant | Description |
|---|---|
Follow | |
Stop |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
RelayBody
Variants
| Variant | Description |
|---|---|
AsReceived | |
Decoded |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
Respect
Requires feature html.
Variants
| Variant | Description |
|---|---|
AiTraining | Requires feature html. |
Robots | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
RetryTrigger
Variants
| Variant | Description |
|---|---|
ConnectionError | |
ServerError | |
Status(u16) | |
Timeout |
Trait implementations: Clone, Debug, Copy
RobotsSource
Variants
| Variant | Description |
|---|---|
Parsed | |
Unavailable | |
Unreachable |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
Scope
Requires feature html.
Variants
| Variant | Description |
|---|---|
Domain | Requires feature html. |
Host | Requires feature html. |
Prefix | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
SkipReason
Requires feature html.
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str | Requires feature html. |
Variants
| Variant | Description |
|---|---|
Blocked | Requires feature html. |
Duplicate | Requires feature html. |
NoAi | Requires feature html. |
NoIndex | Requires feature html. |
NotHtml | Requires feature html. |
Redirects | Requires feature html. |
Robots | Requires feature html. |
Scope | Requires feature html. |
Signal | Requires feature html. |
Status | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
StopReason
Variants
| Variant | Description |
|---|---|
EndOfBody | |
LimitReached | |
PredicateMatched |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
TlsError
Fields
| Field | Description |
|---|---|
Certificate::detail: String | |
Certificate::reason: Option<&’static str> | |
Certificate::verify_code: Option<i32> | |
Proxy::detail: String | |
Proxy::source: Option<Error> | |
Proxy::status: Option<u16> | |
ProxyTargetUnreachable::detail: String | |
ProxyTargetUnreachable::reply: ProxyReply |
Variants
| Variant | Description |
|---|---|
Certificate | |
Dns(Error) | |
Handshake(String) | |
HandshakeIo(Error) | |
Hostname(String) | |
Pinning(String) | |
Profile(String) | |
Proxy | |
ProxyTargetUnreachable | |
Rejected(Error) | |
SslConfig(String) | |
TcpConnect(Error) | |
TrustStore(String) |
Trait implementations: Error, Debug, Display
TlsMinVersion
Variants
| Variant | Description |
|---|---|
Tls10 | |
Tls12 | |
Tls13 |
Trait implementations: Clone, Eq, Ord, PartialEq, PartialOrd, Debug, Copy, StructuralPartialEq
WaitFormat
Variants
| Variant | Description |
|---|---|
HttpDate | |
Seconds | |
UnixSeconds |
Trait implementations: Clone, Eq, PartialEq, Debug, Hash, Copy, StructuralPartialEq
WsMessage
Requires feature websocket.
Variants
| Variant | Description |
|---|---|
Binary(Vec<u8>) | Requires feature websocket. |
Close(Option<CloseFrame>) | Requires feature websocket. |
Ping | Requires feature websocket. |
Pong | Requires feature websocket. |
Text(String) | Requires feature websocket. |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
Traits
IntoParamPair
Methods
| Method | Description |
|---|---|
into_param_pair(self) -> (String, String) |
IntoUrl
URL input.
Methods
| Method | Description |
|---|---|
into_url(self) -> Result<Url> |
Functions
get
pub async fn get(url: impl IntoUrl) -> Result<Response>
Send one GET.
redact_url
pub fn redact_url(raw: &str) -> String
relay_headers
pub fn relay_headers(headers: &HeaderMap, body: RelayBody) -> HeaderMap
Relay a header map.
Type aliases
Result
pub type Result<T> = Result<T, Error>
Re-exports
Url
http
Implementations
alloc::sync::Arc
Trait implementations: Trace
alloc::string::String
Trait implementations: From<ProxyUrl>, IntoUrl
(K, V)
Trait implementations: IntoParamPair
str
Trait implementations: IntoUrl
url::Url
Trait implementations: IntoUrl
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::audit
| Item | Kind | Description |
|---|---|---|
AuditData | struct | |
FingerprintReport | struct | |
HeaderOutcome | struct | |
Ja3Input | struct | |
Ja4Input | struct | |
Ja4hInput | struct | |
Observed | struct | |
FieldOutcome | enum | |
compute_ja3 | fn | |
compute_ja4 | fn | Compute a JA4 offline. |
compute_ja4h | fn | |
compute_ja4t | fn |
Structs
AuditData
Methods
| Method | Description |
|---|---|
compare(&self, observed: &Observed) -> FingerprintReport | Compare with an echo service. |
Fields
| Field | Description |
|---|---|
h2_fingerprint: String | |
ja3: String | |
ja4: String | |
ja4h: String | |
ja4t: String | |
permutes_extensions: bool | |
request_headers: Vec<(String, String)> |
Trait implementations: Clone, Debug
FingerprintReport
Methods
| Method | Description |
|---|---|
is_match(&self) -> bool |
Fields
| Field | Description |
|---|---|
h2_fingerprint: FieldOutcome | |
header_order: FieldOutcome | |
headers: Vec<HeaderOutcome> | |
ja3: FieldOutcome | |
ja4: FieldOutcome |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, StructuralPartialEq
HeaderOutcome
Fields
| Field | Description |
|---|---|
name: String | |
outcome: FieldOutcome |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
Ja3Input
Fields
| Field | Description |
|---|---|
ciphers: &’a [String] | |
curves: &’a [String] | |
extension_ids: &’a [u16] | |
tls_record_version: u16 |
Trait implementations: Debug
Ja4Input
Fields
| Field | Description |
|---|---|
alpn: &’a str | |
ciphers: &’a [String] | |
curves: &’a [String] | |
extension_ids: &’a [u16] | |
has_sni: bool | |
sigalgs: &’a [String] | |
tls_version: &’a str |
Trait implementations: Debug
Ja4hInput
Fields
| Field | Description |
|---|---|
headers: &’a [(String, String)] | |
http_version: &’a str | |
method: &’a str |
Trait implementations: Debug
Observed
Methods
| Method | Description |
|---|---|
from_json(text: &str) -> Result<Observed> | |
h2_fingerprint(self, value: impl Into<String>) -> Self | |
header(self, name: &str, value: impl Into<String>) -> Self | |
ja3(self, value: impl Into<String>) -> Self | |
ja4(self, value: impl Into<String>) -> Self | |
new() -> Self |
Fields
| Field | Description |
|---|---|
h2_fingerprint: Option<String> | |
headers: Vec<(String, String)> | |
ja3: Option<String> | |
ja4: Option<String> |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq
Enums
FieldOutcome
Methods
| Method | Description |
|---|---|
is_mismatch(&self) -> bool |
Fields
| Field | Description |
|---|---|
Absent::expected: String | |
Informational::expected: String | |
Informational::observed: String | |
Mismatch::expected: String | |
Mismatch::observed: String |
Variants
| Variant | Description |
|---|---|
Absent | |
Informational | |
Match | |
Mismatch | |
NotReported |
Trait implementations: Clone, Eq, PartialEq, Debug, Display, StructuralPartialEq
Functions
compute_ja3
pub fn compute_ja3(input: &Ja3Input<'_>) -> String
compute_ja4
pub fn compute_ja4(input: &Ja4Input<'_>) -> String
Compute a JA4 offline.
compute_ja4h
pub fn compute_ja4h(input: &Ja4hInput<'_>) -> String
compute_ja4t
pub fn compute_ja4t(tcp: &TcpProfile) -> String
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::cookie
| Item | Kind | Description |
|---|---|---|
Cookie | struct | |
Jar | struct | |
JarAutosave | struct | |
SameSite | enum |
Structs
Cookie
Methods
| Method | Description |
|---|---|
is_expired(&self) -> bool |
Fields
| Field | Description |
|---|---|
creation_time: SystemTime | |
domain: String | |
expires: Option<SystemTime> | |
host_only: bool | |
http_only: bool | |
name: String | |
path: String | |
same_site: SameSite | |
secure: bool | |
value: String |
Trait implementations: Clone, Debug, Serialize, Deserialize<’de>
Jar
Methods
| Method | Description |
|---|---|
all_cookies(&self) -> Vec<Cookie> | |
clear(&self) | |
cookie_header(&self, url: &Url) -> Option<String> | |
extend_from(&self, other: &Jar) | Merge a jar. |
get_cookie(&self, url: &Url, name: &str) -> Option<String> | Read a cookie. |
load_cookies(&self, cookie_str: &str, url: &Url) | |
new() -> Self | |
remove(&self, url: &Url, name: &str) -> usize | Remove cookies on one host. |
remove_named(&self, name: &str) -> usize | Remove cookies on every host. |
set_cookie(&self, url: &Url, name: &str, value: &str) | Set a cookie. |
snapshot(&self) -> Self | Copy a jar. |
store_set_cookie(&self, set_cookie: &str, url: &Url) | Seed a cookie with attributes. |
autosave(&self, path: impl Into<PathBuf>, interval: Duration) -> JarAutosave | Save a jar as it changes. |
load_from(path: impl AsRef<Path>) -> Result<Jar> | Load a jar from a file. |
save_to(&self, path: impl AsRef<Path>) -> Result<()> | Save a jar to a file. |
Trait implementations: Clone, Default, Debug, Serialize, Deserialize<’de>
JarAutosave
Methods
Trait implementations: Debug
Enums
SameSite
Variants
| Variant | Description |
|---|---|
Lax | |
None | |
Strict |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq, Serialize, Deserialize<’de>
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::html
| Item | Kind | Description |
|---|---|---|
Anchor | struct | Requires feature html. |
Directives | struct | Requires feature html. |
Form | struct | Requires feature html. |
FormEnctype | enum | Form encoding. Requires feature html. |
FormMethod | enum | Requires feature html. |
canonical | fn | Read a page’s canonical URL. Requires feature html. |
forms | fn | Parse forms. Requires feature html. |
links | fn | Read the links of a page. Requires feature html. |
meta | fn | Read the meta tags of a page. Requires feature html. |
text | fn | Read the visible text of a page. Requires feature html. |
Structs
Anchor
Requires feature html.
Methods
| Method | Description |
|---|---|
href(&self) -> &str | Requires feature html. |
rel(&self) -> Option<&str> | Requires feature html. |
text(&self) -> &str | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
Directives
Requires feature html.
Methods
| Method | Description |
|---|---|
ai(&self) -> bool | Requires feature html. |
follow(&self) -> bool | Requires feature html. |
index(&self) -> bool | Requires feature html. |
read(document: &str, headers: &HeaderMap, agent: &str) -> Directives | Read a page’s robots directives. Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq
Form
Requires feature html.
Methods
| Method | Description |
|---|---|
action(&self) -> &str | Requires feature html. |
base(&self) -> Option<&str> | Requires feature html. |
buttons(&self) -> Vec<&str> | Requires feature html. |
enctype(&self) -> FormEnctype | Requires feature html. |
field(&self, name: &str) -> Option<&str> | Requires feature html. |
fields(&self) -> &[(String, String)] | Requires feature html. |
find(document: &str, selector: &str) -> Option<Form> | Find a form by id or name. Requires feature html. |
id(&self) -> Option<&str> | Requires feature html. |
method(&self) -> FormMethod | Requires feature html. |
name(&self) -> Option<&str> | Requires feature html. |
press(&mut self, button: &str) -> bool | Requires feature html. |
set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self | Fill a form. Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq
Enums
FormEnctype
Form encoding. Requires feature html.
Methods
| Method | Description |
|---|---|
as_str(self) -> &’static str | Requires feature html. |
Variants
| Variant | Description |
|---|---|
Multipart | Requires feature html. |
TextPlain | Requires feature html. |
UrlEncoded | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Hash, Copy, StructuralPartialEq
FormMethod
Requires feature html.
Variants
| Variant | Description |
|---|---|
Get | Requires feature html. |
Post | Requires feature html. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Hash, Copy, StructuralPartialEq
Functions
canonical
pub fn canonical(document: &str) -> Option<String>
Read a page’s canonical URL. Requires feature html.
forms
pub fn forms(document: &str) -> Vec<Form>
Parse forms. Requires feature html.
links
pub fn links(document: &str) -> Vec<Anchor>
Read the links of a page. Requires feature html.
meta
pub fn meta(document: &str, name: &str) -> Option<String>
Read the meta tags of a page. Requires feature html.
text
pub fn text(document: &str) -> String
Read the visible text of a page. Requires feature html.
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::multipart
Structs
Form
Requires feature multipart.
Methods
| Method | Description |
|---|---|
boundary(&self) -> &str | Requires feature multipart. |
file(self, name: impl Into<String>, path: impl AsRef<Path>) -> Result<Self> | Requires feature multipart. |
new() -> Self | Requires feature multipart. |
part(self, name: impl Into<String>, part: Part) -> Self | Requires feature multipart. |
text(self, name: impl Into<String>, value: impl Into<String>) -> Self | Requires feature multipart. |
Trait implementations: Default, Debug
Part
Requires feature multipart.
Methods
| Method | Description |
|---|---|
bytes(bytes: impl Into<Bytes>) -> Self | Requires feature multipart. |
file(path: impl AsRef<Path>) -> Result<Self> | Requires feature multipart. |
filename(self, name: impl Into<String>) -> Self | Requires feature multipart. |
header(self, name: impl Into<String>, value: impl Into<String>) -> Self | Requires feature multipart. |
mime(self, mime: impl Into<String>) -> Self | Requires feature multipart. |
stream<S>(stream: S) -> Self where S: Stream<Item = Result<Bytes>> + Send + ’static | Requires feature multipart. |
text(value: impl Into<String>) -> Self | Requires feature multipart. |
Trait implementations: Debug
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::profile
| Item | Kind | Description |
|---|---|---|
FingerprintSpec | struct | |
H2Fingerprint | struct | |
H2PlatformOverride | struct | |
H2PriorityProfile | struct | |
H2Profile | struct | |
H3Grease | struct | |
H3Profile | struct | |
H3Setting | struct | |
H3TransportParam | struct | |
H3VersionInformation | struct | |
PlatformIdentity | struct | |
ProfileMeta | struct | |
ProfileRegistry | struct | |
TlsFingerprint | struct | |
TlsProfile | struct | |
H3ConnectionIdLength | enum | |
H3CryptoReorder | enum | |
H3CryptoSplit | enum | |
H3Order | enum | |
H3VersionGrease | enum | |
HeaderStyle | enum | |
ProfileError | enum |
Structs
FingerprintSpec
Methods
| Method | Description |
|---|---|
akamai(self, raw: impl Into<String>) -> Self | |
base(self, profile: BrowserProfile) -> Self | |
header_order<I, S>(self, order: I) -> Self where I: IntoIterator<Item = S>, S: Into<String> | |
ja3(self, raw: impl Into<String>) -> Self | |
ja4_r(self, raw: impl Into<String>) -> Self | |
name(self, name: impl Into<String>) -> Self | |
new() -> Self |
Trait implementations: Clone, Default, Debug
H2Fingerprint
Fields
| Field | Description |
|---|---|
akamai: Option<String> |
Trait implementations: Clone, Default, Debug, Deserialize<’de>
H2PlatformOverride
Fields
| Field | Description |
|---|---|
enable_push: Option<bool> | |
fingerprint: Option<H2Fingerprint> | |
header_table_size: Option<u32> | |
initial_connection_window_size: Option<u32> | |
initial_stream_window_size: Option<u32> | |
max_concurrent_streams: Option<u32> | |
max_frame_size: Option<u32> | |
max_header_list_size: Option<u32> | |
omit_settings: Vec<String> | |
pseudo_order: Option<Vec<String>> | |
settings_order: Option<Vec<String>> | |
unknown_setting8: Option<u32> | |
unknown_setting9: Option<u32> |
Trait implementations: Clone, Default, Debug, Deserialize<’de>
H2PriorityProfile
Fields
| Field | Description |
|---|---|
exclusive: bool | |
stream_dependency: u32 | |
weight: u8 |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H2Profile
Fields
| Field | Description |
|---|---|
default_priority: Option<H2PriorityProfile> | |
enable_push: Option<bool> | |
fingerprint: Option<H2Fingerprint> | |
header_table_size: Option<u32> | |
initial_connection_window_size: Option<u32> | |
initial_stream_window_size: Option<u32> | |
max_concurrent_streams: Option<u32> | |
max_frame_size: Option<u32> | |
max_header_list_size: Option<u32> | |
platforms: HashMap<String, H2PlatformOverride> | |
pseudo_order: Vec<String> | |
settings_order: Vec<String> | |
unknown_setting8: Option<u32> | |
unknown_setting9: Option<u32> |
Trait implementations: Clone, Debug, Deserialize<’de>
H3Grease
Fields
| Field | Description |
|---|---|
id_bits: u32 | |
max_len: usize | |
value_bits: u32 |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H3Profile
Fields
| Field | Description |
|---|---|
active_connection_id_limit: u64 | |
control_grease_frame: Option<H3Grease> | |
dcid_length: H3ConnectionIdLength | |
initial_crypto_reorder: H3CryptoReorder | |
initial_crypto_split: H3CryptoSplit | |
initial_datagram_size: Option<u16> | |
initial_max_data: u64 | |
initial_max_stream_data_bidi_local: u64 | |
initial_max_stream_data_bidi_remote: u64 | |
initial_max_stream_data_uni: u64 | |
initial_max_streams_bidi: u64 | |
initial_max_streams_uni: u64 | |
max_ack_delay_ms: Option<u64> | |
max_field_section_size: Option<u64> | |
max_idle_timeout_secs: u64 | |
max_udp_payload_size: u16 | |
priority_update: bool | |
pseudo_order: Option<Vec<String>> | |
qpack_blocked_streams: Option<u64> | |
qpack_max_table_capacity: Option<u64> | |
race: bool | |
scid_length: Option<usize> | |
settings: Option<Vec<H3Setting>> | |
tls: Option<TlsProfile> | |
transport_order: H3Order | |
transport_parameters: Option<Vec<H3TransportParam>> |
Trait implementations: Clone, Debug, Deserialize<’de>
H3Setting
Fields
| Field | Description |
|---|---|
grease: Option<H3Grease> | |
id: Option<u64> | |
value: Option<u64> |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H3TransportParam
Fields
| Field | Description |
|---|---|
grease: Option<H3Grease> | |
hex: Option<String> | |
id: Option<u64> | |
pinned: bool | |
varint: Option<u64> | |
versions: Option<H3VersionInformation> |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq, Deserialize<’de>
H3VersionInformation
Fields
| Field | Description |
|---|---|
available: Vec<u32> | |
chosen: u32 | |
grease: Option<H3VersionGrease> |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq, Deserialize<’de>
PlatformIdentity
Fields
| Field | Description |
|---|---|
accept_language: Option<String> | |
sec_ch_ua: String | |
user_agent: String |
Trait implementations: Clone, Debug, Deserialize<’de>
ProfileMeta
Fields
| Field | Description |
|---|---|
browser: String | |
captured_against: Option<String> | |
ch_ua_brand: Option<String> | |
chromium_major: Option<u32> | |
family: String | |
header_style: HeaderStyle | |
name: String | |
verified_against: String | |
version: u32 |
Trait implementations: Clone, Debug, Deserialize<’de>
ProfileRegistry
Methods
| Method | Description |
|---|---|
get(&self, browser: &str, version: u32) -> Option<&BrowserProfile> | |
global() -> &’static Self | |
load(dir: &Path) -> Result<Self, ProfileError> | Load profiles from a directory. |
Trait implementations: Default, Debug
TlsFingerprint
Fields
| Field | Description |
|---|---|
ja4: Option<String> | |
platforms: HashMap<String, TlsFingerprint> | |
resumed_ja4: Option<String> |
Trait implementations: Clone, Default, Debug, Deserialize<’de>
TlsProfile
Fields
| Field | Description |
|---|---|
alps: Option<String> | |
alps_new_codepoint: bool | |
cert_compression: Vec<String> | |
ciphers: Vec<String> | |
curves: Vec<String> | |
delegated_credentials: Option<String> | |
ech_grease: bool | |
extension_permutation: Option<Vec<u16>> | |
extension_tail: Vec<u16> | |
fingerprint: Option<TlsFingerprint> | |
grease: bool | |
key_shares: Option<Vec<String>> | |
min_tls_version: Option<String> | |
ocsp_stapling: bool | |
padding: bool | |
permute_extensions: bool | |
pre_shared_key: bool | |
record_size_limit: Option<u16> | |
request_trust_anchors: bool | |
session_tickets: bool | |
sigalg_grease: bool | |
sigalgs: Vec<String> | |
signed_cert_timestamps: bool | |
tls12_extensions: bool |
Trait implementations: Clone, Debug, Deserialize<’de>
Enums
H3ConnectionIdLength
Fields
| Field | Description |
|---|---|
Weighted::weights: Vec<(usize, u32)> |
Variants
| Variant | Description |
|---|---|
Fixed(usize) | |
Weighted |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq, Deserialize<’de>
H3CryptoReorder
Variants
| Variant | Description |
|---|---|
None | |
SniMidpoint |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H3CryptoSplit
Variants
| Variant | Description |
|---|---|
Even | |
Fill |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H3Order
Variants
| Variant | Description |
|---|---|
Fixed | |
Rotate | |
Shuffle |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq, Deserialize<’de>
H3VersionGrease
Variants
| Variant | Description |
|---|---|
First | |
Random |
Trait implementations: Clone, Eq, PartialEq, Debug, Copy, StructuralPartialEq, Deserialize<’de>
HeaderStyle
Variants
| Variant | Description |
|---|---|
Bare = 10 | |
Brave = 0 | |
Brave154 = 1 | |
CfNetwork = 2 | |
CfNetwork26 = 3 | |
Chromium = 4 | |
Gecko = 5 | |
OkHttp = 6 | |
WebKit = 7 | |
WebKit17 = 8 | |
WebKit26 = 9 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Hash, Copy, StructuralPartialEq, Deserialize<’de>
ProfileError
Fields
| Field | Description |
|---|---|
Empty::path: PathBuf | |
Io::path: PathBuf | |
Io::source: Error | |
Parse::path: Option<PathBuf> | |
Parse::source: Box<(dyn Error + Send + Sync)> |
Variants
| Variant | Description |
|---|---|
Empty | |
Io | |
Parse |
Trait implementations: Error, Debug, Display
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::testing
| Item | Kind | Description |
|---|---|---|
RecordedRequest | struct | Requires feature test-util. |
TestResponse | struct | Requires feature test-util. |
TestServer | struct | Requires feature test-util. |
queue | fn | Requires feature test-util. |
Handler | type | Requires feature test-util. |
Structs
RecordedRequest
Requires feature test-util.
Methods
| Method | Description |
|---|---|
header(&self, name: &str) -> Option<&str> | Requires feature test-util. |
header_count(&self, name: &str) -> usize | Count a header. Requires feature test-util. |
header_values(&self, name: &str) -> Vec<&str> | Read every value of a header. Requires feature test-util. |
text(&self) -> Cow<’_, str> | Requires feature test-util. |
Fields
| Field | Description |
|---|---|
body: Vec<u8> | Requires feature test-util. |
headers: Vec<(String, String)> | Requires feature test-util. |
method: String | Requires feature test-util. |
raw: Vec<u8> | Read the request head as received. Requires feature test-util. |
request_line: String | Read the request line. Requires feature test-util. |
target: String | Requires feature test-util. |
Trait implementations: Clone, Eq, PartialEq, Debug, StructuralPartialEq
TestResponse
Requires feature test-util.
Methods
| Method | Description |
|---|---|
body(self, body: impl Into<Vec<u8>>) -> Self | Requires feature test-util. |
chunks<I, B>(self, parts: I, pause: Duration) -> Self where I: IntoIterator<Item = B>, B: Into<Vec<u8>> | Requires feature test-util. |
close(self) -> Self | Requires feature test-util. |
delay(self, delay: Duration) -> Self | Requires feature test-util. |
header(self, name: impl Into<String>, value: impl Into<String>) -> Self | Requires feature test-util. |
new(status: u16) -> Self | Requires feature test-util. |
Fields
| Field | Description |
|---|---|
body: Vec<u8> | Requires feature test-util. |
headers: Vec<(String, String)> | Requires feature test-util. |
status: u16 | Requires feature test-util. |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, StructuralPartialEq
TestServer
Requires feature test-util.
Methods
| Method | Description |
|---|---|
addr(&self) -> SocketAddr | Requires feature test-util. |
ca_der(&self) -> Option<&[u8]> | Requires feature test-util. |
async http<F>(handler: F) -> Result<Self> where F: Fn(&RecordedRequest) -> TestResponse + Send + Sync + ’static | Run a local test server. Requires feature test-util. |
http_on<F>(listener: TcpListener, handler: F) -> Result<Self> where F: Fn(&RecordedRequest) -> TestResponse + Send + Sync + ’static | Run a test server on your listener. Requires feature test-util. |
async https<F>(handler: F) -> Result<Self> where F: Fn(&RecordedRequest) -> TestResponse + Send + Sync + ’static | Run a local HTTPS test server. Requires feature test-util. |
async next_request(&self) -> Option<RecordedRequest> | Requires feature test-util. |
async requests(&self) -> Vec<RecordedRequest> | Requires feature test-util. |
async shutdown(self) | Requires feature test-util. |
trust(&self) -> TlsTrustConfig | Requires feature test-util. |
url(&self, path: &str) -> String | Requires feature test-util. |
Trait implementations: Debug, Drop
Functions
queue
pub fn queue(responses: impl IntoIterator<Item = TestResponse>) -> impl Fn(&RecordedRequest) -> TestResponse + Send + Sync + 'static
Requires feature test-util.
Type aliases
Handler
pub type Handler = Arc<(dyn Fn(&RecordedRequest) -> TestResponse + Send + Sync)>
Requires feature test-util.
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::tls
| Item | Kind | Description |
|---|---|---|
HappyEyeballsConfig | struct | |
SystemResolver | struct | |
Resolver | trait | |
ResolveFuture | type |
Structs
HappyEyeballsConfig
Methods
| Method | Description |
|---|---|
attempt_limit(self, n: usize) -> Self | |
new() -> Self | |
resolve_delay(self, d: Duration) -> Self |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Copy, StructuralPartialEq
SystemResolver
Trait implementations: Clone, Default, Debug, Copy, Resolver
Traits
Resolver
Methods
| Method | Description |
|---|---|
resolve<’a>(&’a self, host: &’a str, port: u16) -> ResolveFuture<’a> |
Type aliases
ResolveFuture
pub type ResolveFuture<'a> = Pin<Box<(dyn Future<Output = Result<Vec<SocketAddr>, Error>> + Send + 'a)>>
Generated by truesight from leyline-http 0.1.5. See the API overview.
Module leyline::trace
| Item | Kind | Description |
|---|---|---|
BodyEnd | struct | |
Connect | struct | |
Dns | struct | |
Done | struct | |
Fanout | struct | |
Head | struct | |
Metrics | struct | |
MetricsSnapshot | struct | |
Sent | struct | |
Summary | struct | |
Tls | struct | |
TracingTrace | struct | |
BodyOutcome | enum | |
Trace | trait |
Structs
BodyEnd
Fields
| Field | Description |
|---|---|
bytes: u64 | |
elapsed: Duration | |
id: u64 | |
outcome: BodyOutcome<’a> |
Trait implementations: Debug
Connect
Fields
| Field | Description |
|---|---|
elapsed: Duration | |
host: &’a str | |
id: u64 | |
port: u16 | |
reused: bool |
Trait implementations: Debug
Dns
Fields
| Field | Description |
|---|---|
addrs: usize | |
elapsed: Duration | |
host: &’a str | |
id: u64 | |
port: u16 |
Trait implementations: Debug
Done
Fields
| Field | Description |
|---|---|
elapsed: Duration | |
id: u64 | |
outcome: Result<(), &’a Error> |
Trait implementations: Debug
Fanout
Methods
| Method | Description |
|---|---|
new() -> Self | |
with(self, hook: impl Trace) -> Self | Send events to several traces. |
Trait implementations: Clone, Default, Debug, Trace
Head
Fields
| Field | Description |
|---|---|
elapsed: Duration | |
headers: &’a HeaderMap | |
host: &’a str | |
id: u64 | |
protocol: HttpVersion | |
status: u16 |
Trait implementations: Debug
Metrics
Methods
| Method | Description |
|---|---|
new() -> Arc<Self> | Count requests. |
snapshot(&self) -> MetricsSnapshot |
Trait implementations: Default, Debug, Trace
MetricsSnapshot
Methods
| Method | Description |
|---|---|
attempts(&self) -> u64 | |
bodies_complete(&self) -> u64 | |
bodies_dropped(&self) -> u64 | |
bodies_failed(&self) -> u64 | |
errors(&self, category: ErrorCategory) -> u64 | |
errors_total(&self) -> u64 | |
latency(&self) -> Vec<(Option<Duration>, u64)> | |
requests(&self) -> u64 | |
status_class(&self, class: u8) -> u64 |
Trait implementations: Clone, Eq, PartialEq, Default, Debug, Display, StructuralPartialEq
Sent
Fields
| Field | Description |
|---|---|
elapsed: Duration | |
host: &’a str | |
id: u64 | |
method: &’a str | |
path: &’a str | |
protocol: HttpVersion |
Trait implementations: Debug
Summary
Fields
| Field | Description |
|---|---|
attempts: u32 | |
browser: Option<Browser> | |
elapsed: Duration | |
id: u64 | |
method: &’a str | |
original_url: &’a str | |
outcome: Result<(), &’a Error> | |
proxy: Option<String> | |
redirects: usize | |
status: Option<StatusCode> | |
streamed: bool | |
tag: Option<&’a str> | |
url: Option<&’a Url> | |
version: Option<HttpVersion> |
Trait implementations: Debug
Tls
Fields
| Field | Description |
|---|---|
alpn: Option<&’a str> | |
cipher: Option<&’a str> | |
elapsed: Duration | |
host: &’a str | |
id: u64 | |
version: Option<&’a str> |
Trait implementations: Debug
TracingTrace
Trait implementations: Clone, Default, Debug, Copy, Trace
Enums
BodyOutcome
Variants
| Variant | Description |
|---|---|
Complete | |
Dropped | |
Failed(&’a Error) |
Trait implementations: Debug
Traits
Trace
Methods
| Method | Description |
|---|---|
body(&self, ev: &BodyEnd<’_>) | |
connect(&self, ev: &Connect<’_>) | |
dns(&self, ev: &Dns<’_>) | |
done(&self, ev: &Done<’_>) | |
head(&self, ev: &Head<’_>) | |
sent(&self, ev: &Sent<’_>) | |
summary(&self, ev: &Summary<’_>) | One event per request. |
tls(&self, ev: &Tls<’_>) |
Generated by truesight from leyline-http 0.1.5. See the API overview.
leyline-http API changes
Generated by truesight from the item list committed at each release tag (v{version}). Do not edit this page.
0.1.5
- Added
Browser::Firefox158. - Added
Crawl. - Added
Crawl::Item. - Added
Crawl::host_limits. - Added
Crawl::ignore_robots. - Added
Crawl::limit. - Added
Crawl::next. - Added
Crawl::respect. - Added
Crawl::scope. - Added
Crawl::stats. - Added
CrawlPage. - Added
CrawlPage::depth. - Added
CrawlPage::html. - Added
CrawlPage::into_html. - Added
CrawlPage::meta. - Added
CrawlPage::status. - Added
CrawlPage::text. - Added
CrawlPage::url. - Added
CrawlStats. - Added
CrawlStats::errors. - Added
CrawlStats::pages. - Added
CrawlStats::requests. - Added
CrawlStats::skipped. - Added
Purpose. - Added
Purpose::AiInput. - Added
Purpose::AiTrain. - Added
Purpose::Search. - Added
Purpose::as_str. - Added
Respect. - Added
Respect::AiTraining. - Added
Respect::Robots. - Added
Robots. - Added
Robots::allows. - Added
Robots::allows_use. - Added
Robots::crawl_delay. - Added
Robots::parse. - Added
Robots::sitemaps. - Added
Robots::source. - Added
RobotsSource. - Added
RobotsSource::Parsed. - Added
RobotsSource::Unavailable. - Added
RobotsSource::Unreachable. - Added
Scope. - Added
Scope::Domain. - Added
Scope::Host. - Added
Scope::Prefix. - Added
Session::crawl. - Added
Session::robots. - Added
SkipReason. - Added
SkipReason::Blocked. - Added
SkipReason::Duplicate. - Added
SkipReason::NoAi. - Added
SkipReason::NoIndex. - Added
SkipReason::NotHtml. - Added
SkipReason::Redirects. - Added
SkipReason::Robots. - Added
SkipReason::Scope. - Added
SkipReason::Signal. - Added
SkipReason::Status. - Added
SkipReason::as_str. - Added
html::Directives. - Added
html::Directives::ai. - Added
html::Directives::follow. - Added
html::Directives::index. - Added
html::Directives::read. - Added
html::canonical. - Added
html::text. - Added
impl core::clone::Clone for CrawlPage. - Added
impl core::clone::Clone for CrawlStats. - Added
impl core::clone::Clone for Purpose. - Added
impl core::clone::Clone for Respect. - Added
impl core::clone::Clone for Robots. - Added
impl core::clone::Clone for RobotsSource. - Added
impl core::clone::Clone for Scope. - Added
impl core::clone::Clone for SkipReason. - Added
impl core::clone::Clone for html::Directives. - Added
impl core::cmp::Eq for Purpose. - Added
impl core::cmp::Eq for Respect. - Added
impl core::cmp::Eq for RobotsSource. - Added
impl core::cmp::Eq for Scope. - Added
impl core::cmp::Eq for SkipReason. - Added
impl core::cmp::Eq for html::Directives. - Added
impl core::cmp::PartialEq for Purpose. - Added
impl core::cmp::PartialEq for Respect. - Added
impl core::cmp::PartialEq for RobotsSource. - Added
impl core::cmp::PartialEq for Scope. - Added
impl core::cmp::PartialEq for SkipReason. - Added
impl core::cmp::PartialEq for html::Directives. - Added
impl core::default::Default for CrawlStats. - Added
impl core::default::Default for Scope. - Added
impl core::fmt::Debug for Crawl. - Added
impl core::fmt::Debug for CrawlPage. - Added
impl core::fmt::Debug for CrawlStats. - Added
impl core::fmt::Debug for Purpose. - Added
impl core::fmt::Debug for Respect. - Added
impl core::fmt::Debug for Robots. - Added
impl core::fmt::Debug for RobotsSource. - Added
impl core::fmt::Debug for Scope. - Added
impl core::fmt::Debug for SkipReason. - Added
impl core::fmt::Debug for html::Directives. - Added
impl core::fmt::Display for CrawlStats. - Added
impl core::hash::Hash for Purpose. - Added
impl core::hash::Hash for RobotsSource. - Added
impl core::hash::Hash for SkipReason. - Added
impl core::marker::Copy for Purpose. - Added
impl core::marker::Copy for Respect. - Added
impl core::marker::Copy for RobotsSource. - Added
impl core::marker::Copy for Scope. - Added
impl core::marker::Copy for SkipReason. - Added
impl core::marker::Copy for html::Directives. - Added
impl core::marker::StructuralPartialEq for Purpose. - Added
impl core::marker::StructuralPartialEq for Respect. - Added
impl core::marker::StructuralPartialEq for RobotsSource. - Added
impl core::marker::StructuralPartialEq for Scope. - Added
impl core::marker::StructuralPartialEq for SkipReason. - Added
impl core::marker::StructuralPartialEq for html::Directives. - Added
impl futures_core::stream::Stream for Crawl. - Added
impl Crawl. - Added
impl CrawlPage. - Added
impl CrawlStats. - Added
impl Purpose. - Added
impl Robots. - Added
impl SkipReason. - Added
impl html::Directives.
0.1.4
- Added
Browser::Brave155. - Added
Browser::Chrome155.
0.1.3
- Added
PrefixRead. - Added
PrefixRead::bytes. - Added
PrefixRead::stopped_by. - Added
RequestBuilder::read_until. - Added
StopReason. - Added
StopReason::EndOfBody. - Added
StopReason::LimitReached. - Added
StopReason::PredicateMatched. - Added
impl core::clone::Clone for PrefixRead. - Added
impl core::clone::Clone for StopReason. - Added
impl core::cmp::Eq for PrefixRead. - Added
impl core::cmp::Eq for StopReason. - Added
impl core::cmp::PartialEq for PrefixRead. - Added
impl core::cmp::PartialEq for StopReason. - Added
impl core::fmt::Debug for PrefixRead. - Added
impl core::fmt::Debug for StopReason. - Added
impl core::hash::Hash for StopReason. - Added
impl core::marker::Copy for StopReason. - Added
impl core::marker::StructuralPartialEq for PrefixRead. - Added
impl core::marker::StructuralPartialEq for StopReason.
0.1.2
- Added
Browser::Firefox157.
0.1.1
- Added
BlockKind. - Added
BlockKind::Block. - Added
BlockKind::Captcha. - Added
BlockKind::Challenge. - Added
BlockRules. - Added
BlockRules::builtin. - Added
BlockRules::check. - Added
BlockRules::extend. - Added
BlockRules::from_toml. - Added
BlockRules::statuses. - Added
BlockSignal. - Added
BlockSignal::kind. - Added
BlockSignal::vendor. - Added
Browser::Err. - Added
Browser::id. - Added
ChromiumBrand::id. - Added
CompressionConfig::max_error_body. - Added
Device. - Added
Device::app. - Added
Device::autosave. - Added
Device::brand. - Added
Device::capture. - Added
Device::check. - Added
Device::env_proxy. - Added
Device::identity. - Added
Device::jar. - Added
Device::jar_path. - Added
Device::languages. - Added
Device::load_from. - Added
Device::open. - Added
Device::page. - Added
Device::pin_profile. - Added
Device::platform. - Added
Device::profile_id. - Added
Device::profile_toml. - Added
Device::proxy. - Added
Device::proxy_password_env. - Added
Device::save_to. - Added
Device::session_builder. - Added
Device::state. - Added
Device::strict. - Added
Device::tab. - Added
Device::user_agent. - Added
DeviceAutosave. - Added
DeviceAutosave::device. - Added
DeviceAutosave::flush. - Added
DeviceAutosave::shutdown. - Added
DeviceAutosave::track. - Added
DeviceAutosave::update. - Added
DeviceAutosaveOptions. - Added
DeviceAutosaveOptions::interval. - Added
DeviceAutosaveOptions::new. - Added
DeviceAutosaveOptions::state_interval. - Added
Error::attempts. - Added
Error::body. - Added
Error::body_text. - Added
Error::category. - Added
Error::find. - Added
Error::header. - Added
Error::headers. - Added
Error::is_body_limit. - Added
Error::is_dns. - Added
Error::is_profile_changed. - Added
Error::is_proxy. - Added
Error::is_shut_down. - Added
Error::proxy. - Added
Error::retries_exhausted. - Added
Error::retry_after. - Added
ErrorCategory. - Added
ErrorCategory::Body. - Added
ErrorCategory::BodyLimit. - Added
ErrorCategory::Config. - Added
ErrorCategory::Connect. - Added
ErrorCategory::Decode. - Added
ErrorCategory::Dns. - Added
ErrorCategory::Other. - Added
ErrorCategory::Protocol. - Added
ErrorCategory::Proxy. - Added
ErrorCategory::ProxyTarget. - Added
ErrorCategory::Redirect. - Added
ErrorCategory::Request. - Added
ErrorCategory::Status. - Added
ErrorCategory::Timeout. - Added
ErrorCategory::Tls. - Added
ErrorCategory::Url. - Added
ErrorCategory::as_str. - Added
ErrorCategory::gateway_status. - Added
Family::Err. - Added
Family::all. - Added
Family::id. - Added
HeaderAnchor::BeforeCchUa. - Added
HostLimits. - Added
HostLimits::host. - Added
HostLimits::max_in_flight. - Added
HostLimits::max_pause. - Added
HostLimits::max_total_in_flight. - Added
HostLimits::new. - Added
HostLimits::pause_for. - Added
HostLimits::pause_on. - Added
HostLimits::per_second. - Added
HostStats. - Added
HostStats::in_flight. - Added
HostStats::origin. - Added
HostStats::waiting. - Added
Identity::brand. - Added
Identity::with_brand. - Added
Link. - Added
Link::has_rel. - Added
Link::params. - Added
Link::rel. - Added
Link::url. - Added
Pages. - Added
Pages::Item. - Added
Pages::limit. - Added
Pages::next. - Added
Platform::Err. - Added
Platform::all. - Added
Platform::id. - Added
PoolStats::busy. - Added
PoolStats::idle. - Added
ProxyHealth. - Added
ProxyHealth::banned_until. - Added
ProxyHealth::failures. - Added
ProxyHealth::in_use. - Added
ProxyHealth::proxy. - Added
ProxyPool. - Added
ProxyPool::ban_after. - Added
ProxyPool::ban_for. - Added
ProxyPool::identified. - Added
ProxyPool::new. - Added
ProxyPool::rotate_on_block. - Added
ProxyPool::stats. - Added
ProxyPool::sticky_for. - Added
ProxyReply. - Added
ProxyReply::HttpStatus. - Added
ProxyReply::Socks5. - Added
ProxyUrl::Error. - Added
RelayBody. - Added
RelayBody::AsReceived. - Added
RelayBody::Decoded. - Added
RequestBuilder::cookie_jar. - Added
RequestBuilder::download. - Added
RequestBuilder::error_for_status. - Added
RequestBuilder::initiator. - Added
RequestBuilder::pages. - Added
RequestBuilder::tag. - Added
Response::attempts. - Added
Response::block. - Added
Response::copy_decoded_to. - Added
Response::download_to. - Added
Response::into_decoded_stream. - Added
Response::link. - Added
Response::links. - Added
Response::proxy. - Added
Response::relay_headers. - Added
RetryPolicy::backoff. - Added
RetryPolicy::retry_if. - Added
RetryPolicy::retry_unsent. - Added
RetryPolicy::rotate_proxies. - Added
RetryPolicy::skip_blocks. - Added
RetryPolicy::wait_header. - Added
Session::browser. - Added
Session::host_stats. - Added
Session::is_shut_down. - Added
Session::shutdown. - Added
Session::state. - Added
Session::tab. - Added
Session::with_base_url. - Added
SessionBuilder::base_url. - Added
SessionBuilder::bearer_auth. - Added
SessionBuilder::expect_profile_id. - Added
SessionBuilder::host_limits. - Added
SessionBuilder::languages. - Added
SessionBuilder::proxy_pool. - Added
SessionBuilder::user_agent. - Added
SessionIdentity::export_profile. - Added
SessionIdentity::languages. - Added
SessionIdentity::profile_id. - Added
SessionIdentity::to_identity. - Added
SessionState. - Added
SessionState::is_empty. - Added
SessionState::restore_into. - Added
Tab. - Added
Tab::current. - Added
Tab::fetch. - Added
Tab::follow. - Added
Tab::open. - Added
Tab::post_json. - Added
Tab::session. - Added
Tab::set_current. - Added
Tab::submit. - Added
Tab::submit_form. - Added
Tab::subresource. - Added
Tab::xhr. - Added
TimeoutConfig::body. - Added
TimeoutConfig::error_body. - Added
TlsError::ProxyTargetUnreachable. - Added
TlsError::ProxyTargetUnreachable::detail. - Added
TlsError::ProxyTargetUnreachable::reply. - Added
WaitFormat. - Added
WaitFormat::HttpDate. - Added
WaitFormat::Seconds. - Added
WaitFormat::UnixSeconds. - Added
audit::AuditData::compare. - Added
audit::AuditData::permutes_extensions. - Added
audit::AuditData::request_headers. - Added
audit::FieldOutcome. - Added
audit::FieldOutcome::Absent. - Added
audit::FieldOutcome::Absent::expected. - Added
audit::FieldOutcome::Informational. - Added
audit::FieldOutcome::Informational::expected. - Added
audit::FieldOutcome::Informational::observed. - Added
audit::FieldOutcome::Match. - Added
audit::FieldOutcome::Mismatch. - Added
audit::FieldOutcome::Mismatch::expected. - Added
audit::FieldOutcome::Mismatch::observed. - Added
audit::FieldOutcome::NotReported. - Added
audit::FieldOutcome::is_mismatch. - Added
audit::FingerprintReport. - Added
audit::FingerprintReport::h2_fingerprint. - Added
audit::FingerprintReport::header_order. - Added
audit::FingerprintReport::headers. - Added
audit::FingerprintReport::is_match. - Added
audit::FingerprintReport::ja3. - Added
audit::FingerprintReport::ja4. - Added
audit::HeaderOutcome. - Added
audit::HeaderOutcome::name. - Added
audit::HeaderOutcome::outcome. - Added
audit::Observed. - Added
audit::Observed::from_json. - Added
audit::Observed::h2_fingerprint. - Added
audit::Observed::header. - Added
audit::Observed::headers. - Added
audit::Observed::ja3. - Added
audit::Observed::ja4. - Added
audit::Observed::new. - Added
cookie::Jar::autosave. - Added
cookie::Jar::load_from. - Added
cookie::Jar::save_to. - Added
cookie::JarAutosave. - Added
cookie::JarAutosave::flush. - Added
cookie::JarAutosave::shutdown. - Added
get. - Added
html. - Added
html::Anchor. - Added
html::Anchor::href. - Added
html::Anchor::rel. - Added
html::Anchor::text. - Added
html::Form. - Added
html::Form::action. - Added
html::Form::base. - Added
html::Form::buttons. - Added
html::Form::enctype. - Added
html::Form::field. - Added
html::Form::fields. - Added
html::Form::find. - Added
html::Form::id. - Added
html::Form::method. - Added
html::Form::name. - Added
html::Form::press. - Added
html::Form::set. - Added
html::FormEnctype. - Added
html::FormEnctype::Multipart. - Added
html::FormEnctype::TextPlain. - Added
html::FormEnctype::UrlEncoded. - Added
html::FormEnctype::as_str. - Added
html::FormMethod. - Added
html::FormMethod::Get. - Added
html::FormMethod::Post. - Added
html::forms. - Added
html::links. - Added
html::meta. - Added
multipart::Part::file. - Added
profile::HeaderStyle::Bare. - Added
redact_url. - Added
relay_headers. - Added
testing. - Added
testing::Handler. - Added
testing::RecordedRequest. - Added
testing::RecordedRequest::body. - Added
testing::RecordedRequest::header. - Added
testing::RecordedRequest::header_count. - Added
testing::RecordedRequest::header_values. - Added
testing::RecordedRequest::headers. - Added
testing::RecordedRequest::method. - Added
testing::RecordedRequest::raw. - Added
testing::RecordedRequest::request_line. - Added
testing::RecordedRequest::target. - Added
testing::RecordedRequest::text. - Added
testing::TestResponse. - Added
testing::TestResponse::body. - Added
testing::TestResponse::chunks. - Added
testing::TestResponse::close. - Added
testing::TestResponse::delay. - Added
testing::TestResponse::header. - Added
testing::TestResponse::headers. - Added
testing::TestResponse::new. - Added
testing::TestResponse::status. - Added
testing::TestServer. - Added
testing::TestServer::addr. - Added
testing::TestServer::ca_der. - Added
testing::TestServer::http. - Added
testing::TestServer::http_on. - Added
testing::TestServer::https. - Added
testing::TestServer::next_request. - Added
testing::TestServer::requests. - Added
testing::TestServer::shutdown. - Added
testing::TestServer::trust. - Added
testing::TestServer::url. - Added
testing::queue. - Added
trace::BodyEnd. - Added
trace::BodyEnd::bytes. - Added
trace::BodyEnd::elapsed. - Added
trace::BodyEnd::id. - Added
trace::BodyEnd::outcome. - Added
trace::BodyOutcome. - Added
trace::BodyOutcome::Complete. - Added
trace::BodyOutcome::Dropped. - Added
trace::BodyOutcome::Failed. - Added
trace::Fanout. - Added
trace::Fanout::new. - Added
trace::Fanout::with. - Added
trace::Metrics. - Added
trace::Metrics::new. - Added
trace::Metrics::snapshot. - Added
trace::MetricsSnapshot. - Added
trace::MetricsSnapshot::attempts. - Added
trace::MetricsSnapshot::bodies_complete. - Added
trace::MetricsSnapshot::bodies_dropped. - Added
trace::MetricsSnapshot::bodies_failed. - Added
trace::MetricsSnapshot::errors. - Added
trace::MetricsSnapshot::errors_total. - Added
trace::MetricsSnapshot::latency. - Added
trace::MetricsSnapshot::requests. - Added
trace::MetricsSnapshot::status_class. - Added
trace::Summary. - Added
trace::Summary::attempts. - Added
trace::Summary::browser. - Added
trace::Summary::elapsed. - Added
trace::Summary::id. - Added
trace::Summary::method. - Added
trace::Summary::original_url. - Added
trace::Summary::outcome. - Added
trace::Summary::proxy. - Added
trace::Summary::redirects. - Added
trace::Summary::status. - Added
trace::Summary::streamed. - Added
trace::Summary::tag. - Added
trace::Summary::url. - Added
trace::Summary::version. - Added
trace::Trace::body. - Added
trace::Trace::summary. - Added
impl core::clone::Clone for BlockKind. - Added
impl core::clone::Clone for BlockRules. - Added
impl core::clone::Clone for BlockSignal. - Added
impl core::clone::Clone for Device. - Added
impl core::clone::Clone for DeviceAutosaveOptions. - Added
impl core::clone::Clone for ErrorCategory. - Added
impl core::clone::Clone for HostLimits. - Added
impl core::clone::Clone for HostStats. - Added
impl core::clone::Clone for Link. - Added
impl core::clone::Clone for ProxyHealth. - Added
impl core::clone::Clone for ProxyPool. - Added
impl core::clone::Clone for ProxyReply. - Added
impl core::clone::Clone for RelayBody. - Added
impl core::clone::Clone for SessionState. - Added
impl core::clone::Clone for Tab. - Added
impl core::clone::Clone for WaitFormat. - Added
impl core::clone::Clone for audit::FieldOutcome. - Added
impl core::clone::Clone for audit::FingerprintReport. - Added
impl core::clone::Clone for audit::HeaderOutcome. - Added
impl core::clone::Clone for audit::Observed. - Added
impl core::clone::Clone for html::Anchor. - Added
impl core::clone::Clone for html::Form. - Added
impl core::clone::Clone for html::FormEnctype. - Added
impl core::clone::Clone for html::FormMethod. - Added
impl core::clone::Clone for testing::RecordedRequest. - Added
impl core::clone::Clone for testing::TestResponse. - Added
impl core::clone::Clone for trace::Fanout. - Added
impl core::clone::Clone for trace::MetricsSnapshot. - Added
impl core::cmp::Eq for BlockKind. - Added
impl core::cmp::Eq for BlockSignal. - Added
impl core::cmp::Eq for DeviceAutosaveOptions. - Added
impl core::cmp::Eq for ErrorCategory. - Added
impl core::cmp::Eq for HostStats. - Added
impl core::cmp::Eq for Link. - Added
impl core::cmp::Eq for ProxyReply. - Added
impl core::cmp::Eq for RelayBody. - Added
impl core::cmp::Eq for SessionState. - Added
impl core::cmp::Eq for WaitFormat. - Added
impl core::cmp::Eq for audit::FieldOutcome. - Added
impl core::cmp::Eq for audit::FingerprintReport. - Added
impl core::cmp::Eq for audit::HeaderOutcome. - Added
impl core::cmp::Eq for audit::Observed. - Added
impl core::cmp::Eq for html::Anchor. - Added
impl core::cmp::Eq for html::Form. - Added
impl core::cmp::Eq for html::FormEnctype. - Added
impl core::cmp::Eq for html::FormMethod. - Added
impl core::cmp::Eq for testing::RecordedRequest. - Added
impl core::cmp::Eq for testing::TestResponse. - Added
impl core::cmp::Eq for trace::MetricsSnapshot. - Added
impl core::cmp::PartialEq for BlockKind. - Added
impl core::cmp::PartialEq for BlockSignal. - Added
impl core::cmp::PartialEq for DeviceAutosaveOptions. - Added
impl core::cmp::PartialEq for ErrorCategory. - Added
impl core::cmp::PartialEq for HostStats. - Added
impl core::cmp::PartialEq for Link. - Added
impl core::cmp::PartialEq for ProxyReply. - Added
impl core::cmp::PartialEq for RelayBody. - Added
impl core::cmp::PartialEq for SessionState. - Added
impl core::cmp::PartialEq for WaitFormat. - Added
impl core::cmp::PartialEq for audit::FieldOutcome. - Added
impl core::cmp::PartialEq for audit::FingerprintReport. - Added
impl core::cmp::PartialEq for audit::HeaderOutcome. - Added
impl core::cmp::PartialEq for audit::Observed. - Added
impl core::cmp::PartialEq for html::Anchor. - Added
impl core::cmp::PartialEq for html::Form. - Added
impl core::cmp::PartialEq for html::FormEnctype. - Added
impl core::cmp::PartialEq for html::FormMethod. - Added
impl core::cmp::PartialEq for testing::RecordedRequest. - Added
impl core::cmp::PartialEq for testing::TestResponse. - Added
impl core::cmp::PartialEq for trace::MetricsSnapshot. - Added
impl core::convert::From<core::time::Duration> for DeviceAutosaveOptions. - Added
impl core::convert::From<url::parser::ParseError> for Error. - Added
impl core::convert::TryFrom<alloc::string::String> for ProxyUrl. - Added
impl core::default::Default for BlockRules. - Added
impl core::default::Default for HostLimits. - Added
impl core::default::Default for SessionState. - Added
impl core::default::Default for audit::Observed. - Added
impl core::default::Default for html::Form. - Added
impl core::default::Default for html::FormEnctype. - Added
impl core::default::Default for html::FormMethod. - Added
impl core::default::Default for testing::TestResponse. - Added
impl core::default::Default for trace::Fanout. - Added
impl core::default::Default for trace::Metrics. - Added
impl core::default::Default for trace::MetricsSnapshot. - Added
impl core::fmt::Debug for BlockKind. - Added
impl core::fmt::Debug for BlockRules. - Added
impl core::fmt::Debug for BlockSignal. - Added
impl core::fmt::Debug for Device. - Added
impl core::fmt::Debug for DeviceAutosave. - Added
impl core::fmt::Debug for DeviceAutosaveOptions. - Added
impl core::fmt::Debug for ErrorCategory. - Added
impl core::fmt::Debug for HostLimits. - Added
impl core::fmt::Debug for HostStats. - Added
impl core::fmt::Debug for Link. - Added
impl core::fmt::Debug for Pages. - Added
impl core::fmt::Debug for ProxyHealth. - Added
impl core::fmt::Debug for ProxyPool. - Added
impl core::fmt::Debug for ProxyReply. - Added
impl core::fmt::Debug for RelayBody. - Added
impl core::fmt::Debug for SessionState. - Added
impl core::fmt::Debug for Tab. - Added
impl core::fmt::Debug for WaitFormat. - Added
impl core::fmt::Debug for audit::FieldOutcome. - Added
impl core::fmt::Debug for audit::FingerprintReport. - Added
impl core::fmt::Debug for audit::HeaderOutcome. - Added
impl core::fmt::Debug for audit::Observed. - Added
impl core::fmt::Debug for cookie::JarAutosave. - Added
impl core::fmt::Debug for html::Anchor. - Added
impl core::fmt::Debug for html::Form. - Added
impl core::fmt::Debug for html::FormEnctype. - Added
impl core::fmt::Debug for html::FormMethod. - Added
impl core::fmt::Debug for testing::RecordedRequest. - Added
impl core::fmt::Debug for testing::TestResponse. - Added
impl core::fmt::Debug for testing::TestServer. - Added
impl core::fmt::Debug for trace::Fanout. - Added
impl core::fmt::Debug for trace::Metrics. - Added
impl core::fmt::Debug for trace::MetricsSnapshot. - Added
impl core::fmt::Debug for trace::Summary<'_>. - Added
impl core::fmt::Display for ErrorCategory. - Added
impl core::fmt::Display for audit::FieldOutcome. - Added
impl core::fmt::Display for audit::FingerprintReport. - Added
impl core::fmt::Display for trace::MetricsSnapshot. - Added
impl core::hash::Hash for BlockKind. - Added
impl core::hash::Hash for ErrorCategory. - Added
impl core::hash::Hash for WaitFormat. - Added
impl core::hash::Hash for html::FormEnctype. - Added
impl core::hash::Hash for html::FormMethod. - Added
impl core::marker::Copy for BlockKind. - Added
impl core::marker::Copy for DeviceAutosaveOptions. - Added
impl core::marker::Copy for ErrorCategory. - Added
impl core::marker::Copy for ProxyReply. - Added
impl core::marker::Copy for RelayBody. - Added
impl core::marker::Copy for WaitFormat. - Added
impl core::marker::Copy for html::FormEnctype. - Added
impl core::marker::Copy for html::FormMethod. - Added
impl core::marker::StructuralPartialEq for BlockKind. - Added
impl core::marker::StructuralPartialEq for BlockSignal. - Added
impl core::marker::StructuralPartialEq for DeviceAutosaveOptions. - Added
impl core::marker::StructuralPartialEq for ErrorCategory. - Added
impl core::marker::StructuralPartialEq for HostStats. - Added
impl core::marker::StructuralPartialEq for Link. - Added
impl core::marker::StructuralPartialEq for ProxyReply. - Added
impl core::marker::StructuralPartialEq for RelayBody. - Added
impl core::marker::StructuralPartialEq for SessionState. - Added
impl core::marker::StructuralPartialEq for WaitFormat. - Added
impl core::marker::StructuralPartialEq for audit::FieldOutcome. - Added
impl core::marker::StructuralPartialEq for audit::FingerprintReport. - Added
impl core::marker::StructuralPartialEq for audit::HeaderOutcome. - Added
impl core::marker::StructuralPartialEq for audit::Observed. - Added
impl core::marker::StructuralPartialEq for html::Anchor. - Added
impl core::marker::StructuralPartialEq for html::Form. - Added
impl core::marker::StructuralPartialEq for html::FormEnctype. - Added
impl core::marker::StructuralPartialEq for html::FormMethod. - Added
impl core::marker::StructuralPartialEq for testing::RecordedRequest. - Added
impl core::marker::StructuralPartialEq for testing::TestResponse. - Added
impl core::marker::StructuralPartialEq for trace::MetricsSnapshot. - Added
impl core::ops::drop::Drop for testing::TestServer. - Added
impl core::str::traits::FromStr for Browser. - Added
impl core::str::traits::FromStr for Family. - Added
impl core::str::traits::FromStr for Platform. - Added
impl futures_core::stream::Stream for Pages. - Added
impl BlockRules. - Added
impl Device. - Added
impl DeviceAutosave. - Added
impl DeviceAutosaveOptions. - Added
impl ErrorCategory. - Added
impl Family. - Added
impl HostLimits. - Added
impl HostStats. - Added
impl Link. - Added
impl Pages. - Added
impl ProxyPool. - Added
impl SessionState. - Added
impl Tab. - Added
impl audit::AuditData. - Added
impl audit::FieldOutcome. - Added
impl audit::FingerprintReport. - Added
impl audit::Observed. - Added
impl cookie::JarAutosave. - Added
impl html::Anchor. - Added
impl html::Form. - Added
impl html::FormEnctype. - Added
impl testing::RecordedRequest. - Added
impl testing::TestResponse. - Added
impl testing::TestServer. - Added
impl trace::Fanout. - Added
impl trace::Metrics. - Added
impl trace::MetricsSnapshot. - Added
impl trace::Trace for trace::Fanout. - Added
impl trace::Trace for trace::Metrics. - Added
impl serde_core::ser::Serialize for Browser. - Added
impl serde_core::ser::Serialize for ChromiumBrand. - Added
impl serde_core::ser::Serialize for Device. - Added
impl serde_core::ser::Serialize for Family. - Added
impl serde_core::ser::Serialize for Identity. - Added
impl serde_core::ser::Serialize for Platform. - Added
impl serde_core::ser::Serialize for ProxyUrl. - Added
impl serde_core::ser::Serialize for SessionState. - Added
impl<'a> core::fmt::Debug for trace::BodyEnd<'a>. - Added
impl<'a> core::fmt::Debug for trace::BodyOutcome<'a>. - Added
impl<'de> serde_core::de::Deserialize<'de> for BlockKind. - Added
impl<'de> serde_core::de::Deserialize<'de> for Browser. - Added
impl<'de> serde_core::de::Deserialize<'de> for ChromiumBrand. - Added
impl<'de> serde_core::de::Deserialize<'de> for Device. - Added
impl<'de> serde_core::de::Deserialize<'de> for Family. - Added
impl<'de> serde_core::de::Deserialize<'de> for Identity. - Added
impl<'de> serde_core::de::Deserialize<'de> for Platform. - Added
impl<'de> serde_core::de::Deserialize<'de> for ProxyUrl. - Added
impl<'de> serde_core::de::Deserialize<'de> for SessionState. - Changed
profile::HeaderStyle::Brave. - Changed
profile::HeaderStyle::Brave154. - Changed
profile::HeaderStyle::CfNetwork. - Changed
profile::HeaderStyle::CfNetwork26. - Changed
profile::HeaderStyle::Chromium. - Changed
profile::HeaderStyle::Gecko. - Changed
profile::HeaderStyle::OkHttp. - Changed
profile::HeaderStyle::WebKit. - Changed
profile::HeaderStyle::WebKit17. - Changed
profile::HeaderStyle::WebKit26.
0.1.0
First recorded API: 1281 lines in the item list.
leyline-http examples
Generated by truesight from the crate’s example targets. Each page shows the source that cargo builds.
| Example | Run |
|---|---|
api_client | cargo run -p leyline-http --example api_client |
audit | cargo run -p leyline-http --example audit |
browser_tab | cargo run -p leyline-http --example browser_tab --features html |
concurrent | cargo run -p leyline-http --example concurrent |
cookies | cargo run -p leyline-http --example cookies |
crawler | cargo run -p leyline-http --example crawler |
device | cargo run -p leyline-http --example device --features html |
download | cargo run -p leyline-http --example download |
headers | cargo run -p leyline-http --example headers |
http3 | cargo run -p leyline-http --example http3 --features http3 |
multipart | cargo run -p leyline-http --example multipart --features multipart |
oneliner | cargo run -p leyline-http --example oneliner |
post_json | cargo run -p leyline-http --example post_json |
presets | cargo run -p leyline-http --example presets |
retry | cargo run -p leyline-http --example retry |
stock_monitor | cargo run -p leyline-http --example stock_monitor |
streaming | cargo run -p leyline-http --example streaming |
trace | cargo run -p leyline-http --example trace |
websocket | cargo run -p leyline-http --example websocket --features websocket |
Example api_client
Run it:
cargo run -p leyline-http --example api_client
Source: crates/leyline/examples/api_client.rs
use std::time::Duration;
use leyline::http::StatusCode;
use leyline::{Error, ErrorCategory, RetryPolicy, Session, WaitFormat};
use serde::{Deserialize, Serialize};
const BASE_URL: &str = "https://api.example/v1/";
const TOKEN_VAR: &str = "API_TOKEN";
const RATE_REMAINING: &str = "x-ratelimit-remaining";
const RATE_RESET: &str = "x-ratelimit-reset";
const MAX_RATE_WAIT: Duration = Duration::from_secs(120);
#[derive(Debug, Deserialize)]
struct Repo {
name: String,
}
#[derive(Serialize)]
struct NewIssue<'a> {
title: &'a str,
}
struct Client {
session: Session,
}
impl Client {
fn new(token: &str) -> leyline::Result<Self> {
let retry = RetryPolicy::transient()
.max_retries(3)
.retry_if(|resp| {
resp.status() == StatusCode::FORBIDDEN && resp.header(RATE_REMAINING) == Some("0")
})
.wait_header(RATE_RESET, WaitFormat::UnixSeconds)
.max_retry_after(MAX_RATE_WAIT);
let session = Session::builder()
.base_url(BASE_URL)
.bearer_auth(token)
.retry(retry)
.build()?;
Ok(Self { session })
}
async fn repos(&self) -> leyline::Result<Vec<Repo>> {
let mut all = Vec::new();
let mut pages = self.session.get("repos").error_for_status().pages();
while let Some(page) = pages.next().await {
all.extend(page?.json::<Vec<Repo>>().await?);
}
Ok(all)
}
async fn open_issue(&self, repo: &str, title: &str) -> leyline::Result<()> {
self.session
.post(format!("repos/{repo}/issues"))
.json(&NewIssue { title })
.error_for_status()
.send()
.await
.map(drop)
}
fn explain(e: &Error) -> String {
match e.category() {
ErrorCategory::Status => match e.retry_after() {
Some(wait) if e.retries_exhausted() => {
format!("rate limited for {wait:?} after {} attempts", e.attempts())
}
Some(wait) => format!("busy, retry in {wait:?}"),
None => {
let body = e.body_text().unwrap_or_default();
format!("api error {:?}: {body}", e.status())
}
},
ErrorCategory::Timeout => "the api timed out".to_string(),
ErrorCategory::Dns | ErrorCategory::Connect | ErrorCategory::Tls => {
format!("network error: {e}")
}
other => format!("{other:?}: {e}"),
}
}
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let token = std::env::var(TOKEN_VAR).unwrap_or_else(|_| "REPLACE_WITH_YOUR_TOKEN".into());
let client = Client::new(&token)?;
match client.repos().await {
Ok(repos) => repos.iter().for_each(|r| println!("{}", r.name)),
Err(e) => eprintln!("{}", Client::explain(&e)),
}
if let Err(e) = client.open_issue("demo", "hello").await {
eprintln!("{}", Client::explain(&e));
}
Ok(())
}
Example audit
Run it:
cargo run -p leyline-http --example audit
Source: crates/leyline/examples/audit.rs
use leyline::audit::{FieldOutcome, Observed};
use leyline::{Browser, Platform, Session};
const ECHO_URL: &str = "https://tls.peet.ws/api/all";
fn show(name: &str, outcome: &FieldOutcome) {
match outcome {
FieldOutcome::Match => println!("{name:<15} match"),
FieldOutcome::Mismatch { expected, observed } => {
println!("{name:<15} MISMATCH sent {expected}, echo saw {observed}")
}
FieldOutcome::Informational { expected, observed } => {
println!(
"{name:<15} differs, expected for this profile: sent {expected}, echo saw {observed}"
)
}
FieldOutcome::NotReported => println!("{name:<15} not reported"),
_ => println!("{name:<15} {outcome:?}"),
}
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.platform(Platform::Linux)
.audit(true)
.build()?;
let resp = session.get(ECHO_URL).send().await?;
let Some(audit) = resp.audit().cloned() else {
eprintln!("no audit data on this response");
return Ok(());
};
println!("JA4T: {}", audit.ja4t);
println!("JA4H: {}", audit.ja4h);
let observed = Observed::from_json(&resp.text().await?)?;
let report = audit.compare(&observed);
show("JA4", &report.ja4);
show("JA3", &report.ja3);
show("H2 fingerprint", &report.h2_fingerprint);
println!("fingerprint matches: {}", report.is_match());
Ok(())
}
Example browser_tab
Run it:
cargo run -p leyline-http --example browser_tab --features html
Source: crates/leyline/examples/browser_tab.rs
use leyline::html::Form;
use leyline::{Browser, Session};
use serde_json::Value;
const HOME: &str = "https://shop.example/";
const CART_API: &str = "/api/cart";
const LOGIN: &str = "/login";
const LOGIN_FORM: &str = "login";
const FIELDS: [(&str, &str); 2] = [("email", "user@example.com"), ("password", "secret")];
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::default())
.languages(["de-DE", "de", "en"])
.build()?;
let tab = session.tab();
let home = tab.open(HOME).await?;
println!("home: {} {}", home.status(), home.url());
let cart: Value = tab.xhr(CART_API).send().await?.json().await?;
println!("cart: {cart}");
let page = tab.follow(LOGIN).await?.text().await?;
let Some(mut form) = Form::find(&page, LOGIN_FORM) else {
println!("no form with id or name {LOGIN_FORM:?}");
return Ok(());
};
for (name, value) in FIELDS {
form.set(name, value);
}
let done = tab.submit_form(&form).await?;
println!("after submit: {} at {:?}", done.status(), tab.current());
Ok(())
}
Example concurrent
Run it:
cargo run -p leyline-http --example concurrent
Source: crates/leyline/examples/concurrent.rs
use std::time::Instant;
use http::StatusCode;
use leyline::Session;
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://httpbin.org/get".to_string());
let session = Session::builder().build()?;
let n = 50usize;
let started = Instant::now();
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let session = session.clone();
let url = url.clone();
handles.push(tokio::spawn(async move {
let resp = session.get(&url).await?;
Ok::<(usize, StatusCode), leyline::Error>((i, resp.status()))
}));
}
let mut ok = 0usize;
for h in handles {
if let Ok(Ok((_, status))) = h.await
&& !status.is_client_error()
&& !status.is_server_error()
{
ok += 1;
}
}
let elapsed = started.elapsed();
println!(
"{n} concurrent requests: {ok} succeeded in {:.2?} ({:.0} req/s)",
elapsed,
n as f64 / elapsed.as_secs_f64()
);
Ok(())
}
Example cookies
Run it:
cargo run -p leyline-http --example cookies
Source: crates/leyline/examples/cookies.rs
use leyline::cookie::Jar;
use leyline::{Browser, Platform, Session};
const URL_SET: &str = "https://example.com/login";
const URL_READ: &str = "https://example.com/me";
#[tokio::main]
async fn main() -> leyline::Result<()> {
let jar = Jar::new();
let session = Session::builder()
.browser(Browser::Chrome147)
.platform(Platform::Linux)
.cookie_jar(jar)
.build()?;
drop(session.get(URL_SET).await?);
let resp = session.get(URL_READ).await?;
println!("status: {}", resp.status());
Ok(())
}
Example crawler
Run it:
cargo run -p leyline-http --example crawler
Source: crates/leyline/examples/crawler.rs
use std::sync::LazyLock;
use std::time::Duration;
use leyline::trace::{BodyEnd, BodyOutcome, Fanout, Metrics, Summary, Trace};
use leyline::{
BlockRules, Browser, ErrorCategory, HostLimits, ProxyPool, RetryPolicy, Session, TimeoutConfig,
};
const BODY_LIMIT: u64 = 5 * 1024 * 1024;
const MAX_IN_FLIGHT: usize = 16;
const PROXIES: [&str; 2] = [
"http://user:pass@proxy1.example:8080",
"http://user:pass@proxy2.example:8080",
];
const PAGES: usize = 40;
const BLOCK_STATUSES: [u16; 2] = [403, 429];
const BODY_TIMEOUT: Duration = Duration::from_secs(30);
static BLOCKS: LazyLock<BlockRules> = LazyLock::new(|| {
let mut rules = BlockRules::builtin().clone();
rules.extend(BlockRules::statuses(BLOCK_STATUSES));
rules
});
struct Log;
impl Trace for Log {
fn summary(&self, ev: &Summary<'_>) {
let status = ev.status.map(|s| s.as_u16()).unwrap_or(0);
println!(
"#{} [{}] {} {} via {} -> {status} in {:?} ({} attempts)",
ev.id,
ev.tag.unwrap_or("-"),
ev.method,
leyline::redact_url(ev.original_url),
ev.proxy.as_deref().unwrap_or("direct"),
ev.elapsed,
ev.attempts,
);
}
fn body(&self, ev: &BodyEnd<'_>) {
let outcome = match ev.outcome {
BodyOutcome::Complete => "complete".to_owned(),
BodyOutcome::Failed(err) => format!("failed: {err}"),
BodyOutcome::Dropped => "dropped".to_owned(),
_ => "ended".to_owned(),
};
println!(
"#{} body {} bytes in {:?}: {outcome}",
ev.id, ev.bytes, ev.elapsed
);
}
}
async fn fetch(session: Session, index: usize, url: String) {
let resp = match session
.get(&url)
.tag(format!("page-{index}"))
.stream()
.send()
.await
{
Ok(resp) => resp,
Err(e) => return report(&url, &e),
};
let via = resp.proxy().unwrap_or("direct").to_owned();
if let Some(signal) = BLOCKS.check(&resp) {
println!("{url} via {via}: {:?} by {}", signal.kind, signal.vendor);
return;
}
let mut sink = tokio::io::sink();
match resp.copy_decoded_to(&mut sink, Some(BODY_LIMIT)).await {
Ok(n) => println!("{url} via {via}: {n} bytes"),
Err(e) => report(&url, &e),
}
}
fn report(url: &str, e: &leyline::Error) {
let via = e.proxy().unwrap_or("direct");
let attempts = e.attempts();
match e.category() {
ErrorCategory::Proxy => {
eprintln!("{url}: proxy {via} failed after {attempts} attempts, the pool will rotate")
}
ErrorCategory::BodyLimit => eprintln!("{url}: page larger than {BODY_LIMIT} bytes"),
other => eprintln!("{url}: {other} via {via} after {attempts} attempts: {e}"),
}
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let proxies = ProxyPool::new(PROXIES)
.sticky_for(Duration::from_secs(300))
.ban_after(3)
.ban_for(Duration::from_secs(120))
.rotate_on_block(BLOCKS.clone());
let metrics = Metrics::new();
let session = Session::builder()
.browser(Browser::default())
.host_limits(
HostLimits::new()
.max_in_flight(4)
.per_second(2.0)
.max_total_in_flight(MAX_IN_FLIGHT),
)
.proxy_pool(proxies.clone())
.retry(RetryPolicy::transient().skip_blocks(BLOCKS.clone()))
.timeout(TimeoutConfig::new().body(BODY_TIMEOUT))
.trace(Fanout::new().with(Log).with(metrics.clone()))
.build()?;
let tasks: Vec<_> = (1..=PAGES)
.map(|page| format!("https://shop.example/products?page={page}"))
.enumerate()
.map(|(index, url)| tokio::spawn(fetch(session.clone(), index, url)))
.collect();
for task in tasks {
if let Err(e) = task.await {
eprintln!("fetch task failed: {e}");
}
}
println!("{}", metrics.snapshot());
for health in proxies.stats() {
println!(
"{}: {} failures, banned until {:?}",
health.proxy, health.failures, health.banned_until
);
}
Ok(())
}
Example device
Run it:
cargo run -p leyline-http --example device --features html
Source: crates/leyline/examples/device.rs
use std::path::Path;
use std::time::Duration;
use leyline::html::Form;
use leyline::{Browser, Device, ProxyConfig, ProxyUrl, Session};
use serde_json::Value;
const DEVICE: &str = "device.json";
const JAR: &str = "cookies.json";
const PROXY: &str = "http://user:pass@proxy.example:8080";
const LANGUAGES: [&str; 2] = ["en-GB", "fr"];
const HOME: &str = "https://account.example/";
const LOGIN: &str = "https://account.example/login";
const LOGIN_FORM: &str = "login";
const EMAIL_FIELD: &str = "email";
const PASSWORD_FIELD: &str = "password";
const EMAIL_ENV: &str = "ACCOUNT_EMAIL";
const PASSWORD_ENV: &str = "ACCOUNT_PASSWORD";
const EMAIL_KEY: &str = "email";
const SAVE_INTERVAL: Duration = Duration::from_secs(2);
fn create() -> leyline::Result<(Session, Device)> {
let proxy = ProxyUrl::parse(PROXY)?;
let session = Session::builder()
.browser(Browser::Chrome154)
.languages(LANGUAGES)
.proxy(ProxyConfig::from(proxy.clone()).env(false))
.build()?;
let mut device = Device::capture(&session, Some(proxy));
device.pin_profile(&session)?;
device.strict = true;
device.jar_path = Some(JAR.into());
if let Ok(email) = std::env::var(EMAIL_ENV) {
device.app.insert(EMAIL_KEY.into(), Value::String(email));
}
Ok((session, device))
}
fn restore() -> leyline::Result<(Session, Device)> {
let device = Device::load_from(DEVICE)?;
let session = device.open()?;
Ok((session, device))
}
async fn log_in(tab: &leyline::Tab, device: &Device) -> leyline::Result<()> {
let page = tab.open(LOGIN).await?.text().await?;
let Some(mut form) = Form::find(&page, LOGIN_FORM) else {
println!("no login form: the session is already logged in");
return Ok(());
};
let email = device
.app
.get(EMAIL_KEY)
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let password = std::env::var(PASSWORD_ENV).unwrap_or_default();
form.set(EMAIL_FIELD, email).set(PASSWORD_FIELD, password);
let done = tab.submit_form(&form).await?;
println!("login: {} at {}", done.status(), done.url());
Ok(())
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let (session, device) = if Path::new(DEVICE).exists() {
restore()?
} else {
create()?
};
let tab = device.tab(&session);
let autosave = device.autosave(&session, DEVICE, SAVE_INTERVAL);
autosave.track(&tab);
if device.page.is_none() {
log_in(&tab, &device).await?;
}
let resp = tab.follow(HOME).await?;
println!("status {} at {}", resp.status(), resp.url());
autosave.shutdown().await?;
Ok(())
}
Example download
Run it:
cargo run -p leyline-http --example download
Source: crates/leyline/examples/download.rs
use leyline::{ErrorCategory, Session};
const LIMIT: u64 = 50 * 1024 * 1024;
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://httpbin.org/bytes/4096".to_string());
let path = std::env::args()
.nth(2)
.unwrap_or_else(|| "download.bin".to_string());
let session = Session::builder().build()?;
match session.get(&url).download(&path, Some(LIMIT)).await {
Ok(written) => println!("saved {written} bytes to {path}"),
Err(e) => match e.category() {
ErrorCategory::Status => eprintln!("server answered {:?}", e.status()),
ErrorCategory::BodyLimit => eprintln!("file is larger than {LIMIT} bytes"),
ErrorCategory::Timeout => eprintln!("timed out: {e}"),
ErrorCategory::Dns | ErrorCategory::Connect => eprintln!("network: {e}"),
_ => return Err(e),
},
}
Ok(())
}
Example headers
Run it:
cargo run -p leyline-http --example headers
Source: crates/leyline/examples/headers.rs
use leyline::Session;
const URL: &str = "https://example.com/protected";
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder().build()?;
let resp = session
.get(URL)
.headers([("x-request-id", "ex-001")])
.header("accept-language", "en-US,en;q=0.9")
.header("referer", "https://example.com/")
.bearer_auth("REPLACE_WITH_YOUR_TOKEN")
.send()
.await?;
println!("status: {}", resp.status());
Ok(())
}
Example http3
Run it:
cargo run -p leyline-http --example http3 --features http3
Source: crates/leyline/examples/http3.rs
use leyline::{Browser, Session};
const URL: &str = "https://www.cloudflare.com/";
const SECOND_URL: &str = "https://www.cloudflare.com/cdn-cgi/trace";
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::browser(Browser::default());
let first = session.get(URL).await?;
println!(
"first: {} over {:?}, alt-svc {:?}",
first.status(),
first.version(),
first.header("alt-svc")
);
let second = session.get(SECOND_URL).await?;
println!("second: {} over {:?}", second.status(), second.version());
Ok(())
}
Example multipart
Run it:
cargo run -p leyline-http --example multipart --features multipart
Source: crates/leyline/examples/multipart.rs
use leyline::Session;
use leyline::multipart::Form;
const URL: &str = "https://httpbin.org/post";
const NOTE_FIELD: &str = "note";
const NOTE: &str = "uploaded with leyline";
const FILE_FIELD: &str = "file";
#[tokio::main]
async fn main() -> leyline::Result<()> {
let path = std::env::args()
.nth(1)
.unwrap_or_else(|| "Cargo.toml".to_string());
let session = Session::new();
let form = Form::new().text(NOTE_FIELD, NOTE).file(FILE_FIELD, &path)?;
let resp = session
.post(URL)
.multipart(form)
.error_for_status()
.send()
.await?;
println!("status: {}", resp.status());
println!("body: {}", resp.text().await?);
Ok(())
}
Example oneliner
Run it:
cargo run -p leyline-http --example oneliner
Source: crates/leyline/examples/oneliner.rs
#[tokio::main]
async fn main() -> leyline::Result<()> {
let resp = leyline::get("https://httpbin.org/get").await?;
println!("status: {}", resp.status());
println!("{}", resp.text().await?);
Ok(())
}
Example post_json
Run it:
cargo run -p leyline-http --example post_json
Source: crates/leyline/examples/post_json.rs
use leyline::Session;
use serde::Serialize;
const URL: &str = "https://example.com/users";
#[derive(Serialize)]
struct CreateUser {
name: &'static str,
age: u32,
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder().build()?;
let payload = CreateUser {
name: "ada",
age: 36,
};
let resp = session.post(URL).json(&payload).send().await?;
println!("status: {}", resp.status());
println!("body: {}", resp.text().await?);
Ok(())
}
Example presets
Run it:
cargo run -p leyline-http --example presets
Source: crates/leyline/examples/presets.rs
use leyline::{Browser, Platform, Session};
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::Chrome147)
.platform(Platform::Windows)
.build()?;
let page = session.get("https://example.com/").await?;
println!("navigate: {}", page.status());
let js = session
.get("https://example.com/app.js")
.preset(leyline::Preset::Script)
.await?;
println!(
"script: {} ({} bytes)",
js.status(),
js.bytes().await?.len()
);
let api = session
.get("https://example.com/api/state")
.preset(leyline::Preset::Xhr)
.await?;
println!("xhr get: {}", api.status());
let posted = session
.post("https://example.com/api/items")
.preset(leyline::Preset::Xhr)
.body("payload=p%3D1")
.await?;
println!("xhr post: {}", posted.status());
let created = session
.post("https://example.com/users")
.json(&serde_json::json!({ "name": "ada" }))
.await?;
println!("json: {}", created.status());
match api.error_for_status() {
Ok(resp) => println!("ok: {}", resp.status()),
Err(e) if e.is_status() => println!("status: {:?}", e.status()),
Err(e) if e.is_timeout() => println!("timed out"),
Err(e) if e.is_connect() => println!("connect failure: {e}"),
Err(e) => return Err(e),
}
Ok(())
}
Example retry
Run it:
cargo run -p leyline-http --example retry
Source: crates/leyline/examples/retry.rs
use std::time::Duration;
use leyline::{RetryPolicy, Session};
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://httpbin.org/status/503,503,200".to_string());
let session = Session::builder().build()?;
let policy = RetryPolicy::transient()
.max_retries(4)
.initial_backoff(Duration::from_millis(100))
.max_backoff(Duration::from_secs(2));
let resp = session
.request(http::Method::GET, url)
.retry(policy)
.send()
.await?;
println!("final status: {}", resp.status());
Ok(())
}
Example stock_monitor
Run it:
cargo run -p leyline-http --example stock_monitor
Source: crates/leyline/examples/stock_monitor.rs
use leyline::{Browser, Session, StopReason};
const IN_STOCK: &[u8] = b"\"availability\":\"InStock\"";
const OUT_OF_STOCK: &[u8] = b"\"availability\":\"OutOfStock\"";
const PAGE_LIMIT: usize = 512 * 1024;
enum Availability {
InStock,
OutOfStock,
Unknown(StopReason),
}
fn contains(body: &[u8], from: usize, marker: &[u8]) -> bool {
let start = from.saturating_sub(marker.len() - 1);
body[start..].windows(marker.len()).any(|w| w == marker)
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://example.com/".to_string());
let session = Session::browser(Browser::default());
let page = session
.get(&url)
.error_for_status()
.read_until(PAGE_LIMIT, |body, from| {
contains(body, from, IN_STOCK) || contains(body, from, OUT_OF_STOCK)
})
.await?;
let availability = match page.stopped_by {
StopReason::PredicateMatched if contains(&page.bytes, 0, IN_STOCK) => Availability::InStock,
StopReason::PredicateMatched => Availability::OutOfStock,
other => Availability::Unknown(other),
};
match availability {
Availability::InStock => println!("{url}: in stock"),
Availability::OutOfStock => println!("{url}: out of stock"),
Availability::Unknown(reason) => println!(
"{url}: unknown, no marker in {} decoded bytes ({reason:?})",
page.bytes.len()
),
}
Ok(())
}
Example streaming
Run it:
cargo run -p leyline-http --example streaming
Source: crates/leyline/examples/streaming.rs
use bytes::Bytes;
use futures_util::StreamExt;
use leyline::{Body, Session};
use tokio::io::AsyncWriteExt;
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "https://httpbin.org/post".to_string());
let file_path = std::env::args().nth(2);
let session = Session::builder().build()?;
let body = if let Some(path) = file_path {
let meta = tokio::fs::metadata(&path).await.expect("stat input file");
let file = tokio::fs::File::open(&path).await.expect("open input file");
let reader = tokio_util::io::ReaderStream::new(file).map(|r| r);
Body::stream(reader, Some(meta.len()))
} else {
let chunks = (0..16).map(|_| Ok::<Bytes, std::io::Error>(Bytes::from(vec![b'A'; 65536])));
let stream = futures_util::stream::iter(chunks);
Body::stream(stream, Some(16 * 65536))
};
let resp = session
.post(&url)
.body(body)
.header("content-type", "application/octet-stream")
.stream()
.send()
.await?;
eprintln!("status {}", resp.status());
let mut stream = resp.into_stream()?;
let mut stdout = tokio::io::stdout();
let mut total = 0u64;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(leyline::Error::from)?;
total += chunk.len() as u64;
stdout.write_all(&chunk).await.expect("stdout");
}
eprintln!("\nreceived {total} bytes");
Ok(())
}
Example trace
Run it:
cargo run -p leyline-http --example trace
Source: crates/leyline/examples/trace.rs
use leyline::trace::{Connect, Dns, Done, Head, Sent, Tls, Trace};
use leyline::{Browser, Session};
const URL: &str = "https://example.com";
struct Printer;
impl Trace for Printer {
fn dns(&self, ev: &Dns<'_>) {
println!(
"[{}] dns {}:{} -> {} addrs in {:?}",
ev.id, ev.host, ev.port, ev.addrs, ev.elapsed
);
}
fn connect(&self, ev: &Connect<'_>) {
println!(
"[{}] connect {}:{} reused={} in {:?}",
ev.id, ev.host, ev.port, ev.reused, ev.elapsed
);
}
fn tls(&self, ev: &Tls<'_>) {
println!(
"[{}] tls {} {} alpn={} in {:?}",
ev.id,
ev.version.unwrap_or("?"),
ev.cipher.unwrap_or("?"),
ev.alpn.unwrap_or("?"),
ev.elapsed
);
}
fn sent(&self, ev: &Sent<'_>) {
println!(
"[{}] sent {} in {:?}",
ev.id,
ev.protocol.as_str(),
ev.elapsed
);
}
fn head(&self, ev: &Head<'_>) {
println!(
"[{}] head {} {} in {:?}",
ev.id,
ev.status,
ev.protocol.as_str(),
ev.elapsed
);
}
fn done(&self, ev: &Done<'_>) {
match ev.outcome {
Ok(()) => println!("[{}] done ok in {:?}", ev.id, ev.elapsed),
Err(e) => println!("[{}] done {:?}: {e}", ev.id, e.kind()),
}
}
}
#[tokio::main]
async fn main() -> leyline::Result<()> {
let session = Session::builder()
.browser(Browser::Chrome147)
.trace(Printer)
.build()?;
drop(session.get(URL).await?);
println!("--- second request, pooled ---");
drop(session.get(URL).await?);
Ok(())
}
Example websocket
Run it:
cargo run -p leyline-http --example websocket --features websocket
Source: crates/leyline/examples/websocket.rs
use leyline::Session;
#[tokio::main]
async fn main() -> leyline::Result<()> {
let url = std::env::args()
.nth(1)
.unwrap_or_else(|| "wss://echo.websocket.org".to_string());
let session = Session::browser(leyline::Browser::default());
let mut ws = session.websocket(&url).connect().await?;
ws.send(leyline::WsMessage::Text("hello from leyline".to_owned()))
.await?;
if let Some(msg) = ws.recv().await? {
println!("{msg:?}");
}
ws.close().await
}