Wisp

Type to search every docs page and heading.

Testing and Mixing with Rust Code

The app is a function from request to reply.

In Process

wisp::prepare::<App>().await?; // runs `init`, once
let mut req = wisp::Request::new("GET", "/posts?page=2");
req.header("accept", "text/html");
let reply = wisp::handle::<App>(req).await; // reply.status, .headers (Vec<(name, value)>), .body (wisp::Body)

Same parser, limits, hooks and CSRF check as the server.

Testing an App

wisp::test::client needs no port and keeps cookies like a browser:

// tests/app.rs
wisp::app!();

#[test]
fn counter() {
    let mut app = wisp::test::client::<App>();
    assert!(app.get("/").text().contains("Clicked 0 times"));
    app.post_form("/?/increment", &[]);
    assert!(app.get("/").text().contains("Clicked 1 times"));
    assert!(app.cookie("user").is_none());
}
MethodWhat it does
get(path)GET
post_form(target, &[(name, value)])form POST
send(req)any request
next_chunk(&mut reply)next chunk of a stream
cookie(name)a stored cookie

More in AGENTS.md and api.md. Nothing upgrades in process, so Response::websocket is 501: test WebSockets against the running server (tests/app/tests/http.rs uses a TcpStream).

WebSockets

A +server.rs upgrades with Response::websocket; the connection closes when the handler returns.

// src/routes/ws/+server.rs
fn get() -> Response {
    Response::websocket(|ws| async move {
        while let Some(msg) = ws.recv().await {
            ws.send(msg).await?; // a String is text, a Vec<u8> binary
        }
        Ok(())
    })
}
  • ws.recv() is the next wisp::Message (Text/Binary; msg.text(), msg.bytes()), or None once the client closed or went, or the server stops. Pings are answered, fragments joined.
  • ws.send(msg) fails once closed: stop. recv and send can overlap (tokio::select!, or wisp::spawn a sender with ws.clone()).
  • While recv waits, a client quiet for 30 s is pinged and for 60 s closed (1001). WISP_WS_IDLE sets the 60 (seconds; 0 never). A send-only handler isn't timed (a client that stops reading fails send).
  • A message is at most the route's BODY_LIMIT (1 MB default), else close 1009.
  • before runs first (cookies, hooks).
  • A page on another site is 403 as for a cross-site form (Origin must name the host, or ORIGIN when set). A non-upgrade request gets 426.
  • Only the built-in server upgrades; tower, edge targets and the test client answer 501.

Browser side: new WebSocket(${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws) with onmessage, onopen, onclose.

The tower Feature

Wisp becomes a tower::Service; the default build keeps its two deps.

wisp = { git = "https://wisp.ar0.eu", features = ["tower"] }
let wisp = wisp::tower::service::<App>().await?; // Service<http::Request<B>>

axum answers its own routes, Wisp the rest (examples/axum):

wisp::app!();

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = tokio::net::TcpListener::bind(wisp::address()).await?;
    let wisp = wisp::tower::service::<App>().await?;
    let app = axum::Router::new()
        .route(
            "/api/hello",
            axum::routing::get(|| async { "hello from axum" }),
        )
        .fallback_service(wisp);
    axum::serve(listener, app).await
}

Middleware: tower::ServiceBuilder::new().layer(tower_http::compression::CompressionLayer::new()).service(wisp::tower::service::<App>().await?).

hyper:

let svc = hyper_util::service::TowerToHyperService::new(wisp::tower::service::<App>().await?);
hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
    .serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
    .await?;

Lambda with a main of your own (--target lambda needs no code; put WISP_SECRET in the function's environment):

wisp::app!();

#[tokio::main]
async fn main() -> Result<(), lambda_http::Error> {
    lambda_http::run(wisp::tower::service::<App>().await?).await
}

Axum Inside Wisp

Send paths to an axum Router from before (or a catch-all +server.rs). before may return Result<Option<Response>> (Some answers instead of the route). async fn before takes every route off the no-wait fast path.

// src/hooks.rs
async fn before(cx: &mut Cx) -> Result<Option<Response>> {
    if cx.path().starts_with("/api") {
        let mut api = api_router(); // an axum::Router
        return Ok(Some(wisp::tower::call(&mut api, cx).await?));
    }
    Ok(None)
}

Is This Page Useful?

Edit This Page