Skip to main content

Builder

Struct Builder 

Source
pub struct Builder<G: Gate, E: Render = NoRender> { /* private fields */ }
Expand description

Declares an application. Every mistake is reported by build at once, before anything listens.

use web_net::{App, get};

let app = App::builder()
    .host("example.com")
    .development()
    .health("/healthz")
    .route(get("/", |cx| cx.text(200, "home")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "ok");

Implementations§

Source§

impl<G: Gate, E: Render> Builder<G, E>

Source

pub fn compress(self, compression: Compression) -> Self

Compress replies with gzip when the client accepts it: HTML, CSS, JavaScript, JSON, XML, SVG, and other text, over a minimum size. Off by default: a proxy in front usually compresses.

A compressed page that carries a secret beside text the visitor chose (a reflected query value) can leak the secret through its compressed size (BREACH). csrf-sec masks its tokens per reply; keep compression off for pages with other secrets.

use web_net::{App, Compression, get};

let app = App::builder()
    .compress(Compression::default())
    .route(get("/", |cx| cx.text(200, &"hello ".repeat(400))))
    .build()?;
let r = serve_net::testing::respond(
    &app,
    b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept-Encoding: gzip\r\n\r\n",
);
assert_eq!(r.header("content-encoding"), Some(&b"gzip"[..]));
Source

pub fn csrf(self, csrf: Csrf) -> Self

CSRF protection with csrf-sec (re-exported as web_net::csrf_sec), built from a persisted 32-byte key. Required as soon as one route takes an unsafe method and is not exempt.

web-net always applies the origin policy and a token, whatever the Csrf’s Mode; its keys, lifetimes, and allowed origins apply as set.

use web_net::{App, csrf_sec::Csrf, get, post};

let app = App::builder()
    .csrf(Csrf::new(*b"a persisted 32-byte secret key!!").unwrap())
    .route(get("/", |cx| cx.text(200, "form")))
    .route(post("/", |cx| cx.redirect(303, "/")))
    .build()?;
// A POST without a token is refused before its handler runs.
let r = serve_net::testing::respond(
    &app,
    b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n",
);
assert_eq!(r.status, 403);
Source

pub fn redirect_hosts(self, hosts: &[&str]) -> Self

Hosts that Context::redirect_external may send a visitor to (accounts.example.com). Everything else is refused.

use web_net::{App, get};

let app = App::builder()
    .redirect_hosts(&["accounts.example.com"])
    .route(get("/login", |cx| cx.redirect_external(302, "https://accounts.example.com/login")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /login HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("location"), Some(&b"https://accounts.example.com/login"[..]));
Source

pub fn assets(self, assets: Assets) -> Self

Serves a folder of files under a mount path, before routing and without the gate (files are public).

use web_net::{App, Assets};

let dir = std::env::temp_dir().join("web-net-doc-builder-assets");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("hello.txt"), "hello").unwrap();

let app = App::builder().assets(Assets::new("/static", &dir)).build()?;
let r = serve_net::testing::respond(&app, b"GET /static/hello.txt HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "hello"));
Source

pub fn health(self, path: &str) -> Self

A liveness path (/healthz): 200 ok whenever the process answers. Answered before routing and without the gate.

let app = web_net::App::builder().health("/healthz").build()?;
let r = serve_net::testing::respond(&app, b"GET /healthz HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "ok"));
Source

pub fn ready( self, path: &str, check: impl Fn() -> bool + Send + Sync + 'static, ) -> Self

A readiness path (/readyz): 200 ready when check says so, 503 not ready otherwise. Keep check to a flag read: it runs on the request path.

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serve_net::testing::respond;

let up = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&up);
let app = web_net::App::builder()
    .ready("/readyz", move || flag.load(Ordering::Relaxed))
    .build()?;
let req = b"GET /readyz HTTP/1.1\r\nHost: localhost\r\n\r\n";
assert_eq!(respond(&app, req).status, 503);
up.store(true, Ordering::Relaxed);
assert_eq!(respond(&app, req).text(), "ready");
Source

pub fn security_headers(self, policy: SecurityHeaders) -> Self

The security headers every reply carries (default SecurityHeaders::default).

use web_net::{App, SecurityHeaders, get};

// A proxy in front sets the security headers, so web-net sends none.
let app = App::builder()
    .security_headers(SecurityHeaders::none())
    .route(get("/", |cx| cx.html(200, "<p>hi</p>")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("content-security-policy"), None);
Source

pub fn route(self, route: Route<G::Identity, G::Rule, E>) -> Self

Adds a route.

use web_net::{App, get};

let app = App::builder().route(get("/about", |cx| cx.text(200, "about"))).build()?;
let r = serve_net::testing::respond(&app, b"GET /about HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "about");
Source

pub fn group(self, group: Group<G::Identity, G::Rule, E>) -> Self

Adds a group’s routes.

use web_net::{App, Group, get};

let app = App::builder()
    .group(Group::at("/admin").route(get("/users", |cx| cx.text(200, "users"))))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /admin/users HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "users");
Source

pub fn hook<F>(self, hook: Hook, routes: Routes, f: F) -> Self
where F: Fn(&mut Context<'_, G::Identity, E>) -> Result + Send + Sync + 'static,

Runs f at hook for the routes routes selects.

use web_net::{App, Hook, Routes, get};

let app = App::builder()
    .hook(Hook::Finish, Routes::all(), |cx| {
        cx.set_header("x-served-by", "web-net");
        Ok(())
    })
    .route(get("/", |cx| cx.text(200, "home")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.header("x-served-by"), Some(&b"web-net"[..]));
Source

pub fn host(self, host: &str) -> Self

A host the site answers (example.com, or localhost:8080 with its port). The first one is canonical; any other host gets 421.

With no host declared, the site answers the development hosts only (localhost, 127.0.0.1, [::1], *.localhost), so a site works on a laptop as it is and a name someone else points at the server (DNS rebinding) is never the site. Declare the production host before deploying, or any_host for a site that must answer every name.

use web_net::{App, get};
use serve_net::testing::respond;

let app = App::builder().route(get("/", |cx| cx.text(200, "hi"))).build()?;
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n").status, 200);
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 421);

let app = App::builder().host("example.com").route(get("/", |cx| cx.text(200, "hi"))).build()?;
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n").status, 200);
assert_eq!(respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 421);
Source

pub fn alias(self, host: &str) -> Self

A host that is redirected (301, or 308 for unsafe methods) to the canonical host (www.example.com to example.com).

use web_net::{App, get};

let app = App::builder()
    .host("example.com")
    .alias("www.example.com")
    .route(get("/", |cx| cx.text(200, "home")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"http://example.com/"[..])));
Source

pub fn development(self) -> Self

A development instance of a site with declared hosts: localhost, 127.0.0.1, [::1], and *.localhost are answered as the site too (with any port), and HTTPS is not required for them. Never set this in production. (A site with no declared host answers only these already.)

use web_net::{App, get};

let app = App::builder()
    .host("example.com")
    .development()
    .route(get("/", |cx| cx.text(200, "home")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost:8080\r\n\r\n");
assert_eq!(r.status, 200);
Source

pub fn any_host(self) -> Self

Answers every Host, for a site behind a front that checked the host already, or one that is reached by names it cannot list. Not with host.

use web_net::{App, get};

let app = App::builder().any_host().route(get("/", |cx| cx.text(200, "hi"))).build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: anything.example\r\n\r\n");
assert_eq!(r.status, 200);
Source

pub fn base(self, base: &str) -> Self

The path the site is installed under (/blog). It is removed before routing and added back by Context::url, so routes and links are written as if the site were at /. Requests outside it get 404.

use serve_net::testing::respond;
use web_net::{App, get};

let app = App::builder()
    .base("/blog")
    .route(get("/posts", |cx| cx.text(200, "posts")))
    .build()?;
assert_eq!(respond(&app, b"GET /blog/posts HTTP/1.1\r\nHost: localhost\r\n\r\n").text(), "posts");
assert_eq!(respond(&app, b"GET /posts HTTP/1.1\r\nHost: localhost\r\n\r\n").status, 404);
Source

pub fn trailing_slash(self, policy: TrailingSlash) -> Self

The trailing-slash policy (default TrailingSlash::Strip).

use web_net::{App, TrailingSlash, get};

// Both spellings reach the route and neither is redirected.
let app = App::builder()
    .trailing_slash(TrailingSlash::Ignore)
    .route(get("/docs", |cx| cx.text(200, "docs")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /docs/ HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.text()), (200, "docs"));
Source

pub fn lowercase_paths(self) -> Self

Paths are lower case: one with an upper-case letter is redirected.

use web_net::{App, get};

let app = App::builder()
    .lowercase_paths()
    .route(get("/about", |cx| cx.text(200, "about")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /About HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"/about"[..])));
Source

pub fn https_only(self) -> Self

Plain HTTP is redirected to HTTPS on the canonical host (behind a TLS proxy, serve-net must trust the proxy’s scheme). Needs a host.

use web_net::{App, get};

let app = App::builder()
    .host("example.com")
    .https_only()
    .route(get("/", |cx| cx.text(200, "home")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
assert_eq!((r.status, r.header("location")), (301, Some(&b"https://example.com/"[..])));
Source

pub fn sign_in(self, path: &str) -> Self

The site-relative sign-in page. A Denial::sign_in on a GET or HEAD is then answered with a 303 to it, the page asked for passed as next; without it, or for other methods, the answer is 401.

use web_net::{App, get};
// `Members` answers `Rule::Member` with `Denial::sign_in` for everyone.
let app = App::with_gate(Members)
    .sign_in("/login")
    .route(get("/login", |cx| cx.text(200, "sign in")).public())
    .route(get("/account", |cx| cx.text(200, "account")).allow(Rule::Member))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET /account HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!((r.status, r.header("location")), (303, Some(&b"/login?next=/account"[..])));
Source

pub fn www_authenticate(self, challenge: &str) -> Self

The WWW-Authenticate value sent with a 401 (default Session).

let app = web_net::App::builder().www_authenticate("Bearer").build()?;
Source

pub fn events(self, events: impl Events) -> Self

Where refusals, denials, and failures are reported.

use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use web_net::{App, Error, Event, EventKind, Events, get};

#[derive(Default)]
struct Failures(AtomicU32);
impl Events for Failures {
    fn event(&self, e: &Event<'_>) {
        if e.kind == EventKind::Failed {
            self.0.fetch_add(1, Ordering::Relaxed);
        }
    }
}

let failures = Arc::new(Failures::default());
let app = App::builder()
    .events(Arc::clone(&failures))
    .route(get("/", |_cx| Err(Error::unavailable("db.down"))))
    .build()?;
serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(failures.0.load(Ordering::Relaxed), 1);
Source

pub fn checkpoint(self, checkpoint: impl Checkpoint) -> Self

The in-flight revocation check at web-net’s stages.

use serve_net::{Envelope, Verdict};
use web_net::{App, Checkpoint, Stage, get};

struct Maintenance;
impl Checkpoint for Maintenance {
    fn check(&self, _: &Envelope, _: Stage) -> Verdict {
        Verdict::Deny { status: 503 }
    }
}

let app = App::builder()
    .checkpoint(Maintenance)
    .route(get("/", |cx| cx.text(200, "hi")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.status, 503);
Source

pub fn page_limit(self, bytes: usize) -> Self

The budget for one rendered page (default 8 MiB).

use web_net::{App, Context, Guest, get};
// `Pages` is an engine whose one template, "page", writes the view (a `str`).
let app = App::builder()
    .render(Pages)
    .page_limit(4)
    .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("more than four bytes")).template("page"))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.status, 500);
Source

pub fn wipe_buffers(self) -> Self

Zero the reply buffers web-net reuses after every request, so one visitor’s page never lingers in memory another request reuses. Costs one pass over the bytes written.

use web_net::{App, get};

let app = App::builder()
    .wipe_buffers()
    .route(get("/", |cx| cx.text(200, "a private page")))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "a private page");
Source

pub fn build(self) -> Result<App<G, E>, BuildError>

Compiles the application.

§Errors

A BuildError listing every problem: a route without declared access, a malformed pattern, two handlers for one method of one path, an unknown template, a malformed host or base path.

use web_net::{App, get};

assert!(App::builder().route(get("/", |cx| cx.text(200, "hi"))).build().is_ok());

// Every problem is listed, not only the first.
let err = App::builder()
    .base("blog")
    .route(get("/a", |cx| cx.text(200, "a")))
    .route(get("/a", |cx| cx.text(200, "again")))
    .build()
    .unwrap_err();
assert_eq!(err.problems().len(), 2);
Source§

impl<G: Gate> Builder<G, NoRender>

Source

pub fn render<E: Render>(self, engine: E) -> Builder<G, E>

The template engine. Set it before adding routes and hooks: their handlers are typed by it, so any added before are dropped and the build says so.

use web_net::{App, Context, Guest, Output, Render, RenderError, TemplateId, get};

struct Pages;
impl Render for Pages {
    type View<'a> = str;
    fn template(&self, name: &str) -> Option<TemplateId> {
        (name == "page").then_some(TemplateId(0))
    }
    fn render(&self, _: TemplateId, title: &str, out: &mut Output<'_>) -> Result<(), RenderError> {
        out.write_str("<h1>")?;
        out.write_str(title)?; // a real engine escapes
        out.write_str("</h1>")
    }
}

let app = App::builder()
    .render(Pages)
    .route(get("/", |cx: &mut Context<'_, Guest, Pages>| cx.render("Home")).template("page"))
    .build()?;
let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
assert_eq!(r.text(), "<h1>Home</h1>");

Trait Implementations§

Source§

impl<G: Gate, E: Render> Debug for Builder<G, E>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<G, E = NoRender> !RefUnwindSafe for Builder<G, E>

§

impl<G, E = NoRender> !UnwindSafe for Builder<G, E>

§

impl<G, E> Forget for Builder<G, E>

§

impl<G, E> Freeze for Builder<G, E>
where G: Freeze, Vec<Route<<G as Gate>::Identity, <G as Gate>::Rule, E>>: Freeze, Vec<(Hook, Routes, Arc<dyn Fn(&mut Context<'_, <G as Gate>::Identity, E>) -> Result<(), Error> + Sync + Send>)>: Freeze, Option<E>: Freeze,

§

impl<G, E> Move for Builder<G, E>

§

impl<G, E> Send for Builder<G, E>
where Vec<Route<<G as Gate>::Identity, <G as Gate>::Rule, E>>: Send, Vec<(Hook, Routes, Arc<dyn Fn(&mut Context<'_, <G as Gate>::Identity, E>) -> Result<(), Error> + Sync + Send>)>: Send, Option<E>: Send,

§

impl<G, E> Sync for Builder<G, E>
where Vec<Route<<G as Gate>::Identity, <G as Gate>::Rule, E>>: Sync, Vec<(Hook, Routes, Arc<dyn Fn(&mut Context<'_, <G as Gate>::Identity, E>) -> Result<(), Error> + Sync + Send>)>: Sync, Option<E>: Sync,

§

impl<G, E> Unpin for Builder<G, E>
where G: Unpin, Vec<Route<<G as Gate>::Identity, <G as Gate>::Rule, E>>: Unpin, Vec<(Hook, Routes, Arc<dyn Fn(&mut Context<'_, <G as Gate>::Identity, E>) -> Result<(), Error> + Sync + Send>)>: Unpin, Option<E>: Unpin,

§

impl<G, E> UnsafeUnpin for Builder<G, E>
where G: UnsafeUnpin, Vec<Route<<G as Gate>::Identity, <G as Gate>::Rule, E>>: UnsafeUnpin, Vec<(Hook, Routes, Arc<dyn Fn(&mut Context<'_, <G as Gate>::Identity, E>) -> Result<(), Error> + Sync + Send>)>: UnsafeUnpin, Option<E>: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.