Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 a HeaderAnchor slot: BeforeCchUa, AfterCchUa, AfterCchUaMobile, AfterCchUaPlatform, AfterUserAgent, AfterAccept, AfterContentType, AfterFetchDest, or BeforeAcceptEncoding.
#![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.

Presetsec-fetch-modesec-fetch-destsec-fetch-userOther headers
FormcorsemptynoneA script accept, priority: u=1, i
FormNavigatenavigatedocument?1An 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 TReferer
Same originThe full page URL, without fragment and user info
Same site or cross siteThe page origin and /
P is https, T is httpNone
PresetNo initiatorSame originSame site or cross siteP is https, T is http
Xhr GETNoneNoneP originP origin
Xhr POST, FormT originT originP originP origin
FormNavigateT originT originP originnull
Image, ScriptNoneNoneNoneNone

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.

CallSendsChanges the page
open(url)A typed URL: no initiatorYes
follow(url)A link click from the current pageYes
submit(url, fields)A clicked form, Preset::FormNavigateYes
submit_form(&form)A parsed html::Form, with its own method and actionYes
xhr(url), fetch(method, url), post_json(url, &body)A script request, Preset::XhrNo
subresource(url, preset)An image or script, for example Preset::ImageNo

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

CallEffect 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.