mini-serve 0.13.12

An HTTP server: trie router, middleware, CORS, optional TLS. Built on hyper + tokio.
Documentation
//! How hyper frames the bodies this crate produces.
//!
//! `ResponseBody` is an enum that implements `Body` by delegating to its variants.
//! `size_hint` is the delegation that matters most: hyper reads it to choose
//! between `Content-Length` and chunked transfer encoding. A defaulted
//! implementation reports "length unknown", which is legal but silently turns
//! every in-memory response chunked — bigger on the wire, and a visible protocol
//! change for anything downstream. These tests fail if that delegation is lost.

use hyper::StatusCode;
use mini_serve::{body, handler, json, RouteBuilder, ServeError};
use hyper::body::Bytes;
use hyper::Response;

#[tokio::test]
async fn an_in_memory_body_is_framed_by_content_length_not_chunked() {
	let payload = "hello, framing";
	let app = RouteBuilder::stateless()
		.get(
			"/bytes",
			handler(move |_req, _state| async move {
				Ok::<_, ServeError>(Response::new(body(Bytes::from(payload))))
			}),
		)
		.seal();

	let port = app.bind_ephemeral().await.unwrap();
	let response = reqwest::get(format!("http://127.0.0.1:{port}/bytes")).await.unwrap();

	assert_eq!(
		response.headers().get("content-length").map(|v| v.to_str().unwrap()),
		Some(payload.len().to_string().as_str()),
		"an in-memory body must carry its exact length"
	);
	assert!(
		!response.headers().contains_key("transfer-encoding"),
		"a known-length body must not be chunked: {:?}",
		response.headers()
	);
	assert_eq!(response.text().await.unwrap(), payload);
}

/// The `json` helper takes the same path and sets its own `Content-Length`; this
/// guards against the enum reframing it underneath.
#[tokio::test]
async fn a_json_body_keeps_its_content_length() {
	let app = RouteBuilder::stateless()
		.get(
			"/json",
			handler(|_req, _state| async {
				json(StatusCode::OK, &serde_json::json!({"ok": true}))
			}),
		)
		.seal();

	let port = app.bind_ephemeral().await.unwrap();
	let response = reqwest::get(format!("http://127.0.0.1:{port}/json")).await.unwrap();

	let len: usize = response
		.headers()
		.get("content-length")
		.expect("json responses must be length-framed")
		.to_str()
		.unwrap()
		.parse()
		.unwrap();
	let text = response.text().await.unwrap();
	assert_eq!(len, text.len(), "declared length must match the body sent");
}
/// RFC 9110 §9.3.2: a HEAD response carries the `Content-Length` its GET would.
/// HEAD swaps the body for the `Empty` variant, whose `size_hint` is exact-zero —
/// so this also catches `Empty` reporting an unknown length and dragging a
/// `Transfer-Encoding` onto a bodiless response.
#[tokio::test]
async fn head_reports_the_length_of_the_get_it_mirrors() {
	let payload = "a body of known size";
	let app = RouteBuilder::stateless()
		.get(
			"/thing",
			handler(move |_req, _state| async move {
				Ok::<_, ServeError>(Response::new(body(Bytes::from(payload))))
			}),
		)
		.seal();

	let port = app.bind_ephemeral().await.unwrap();
	let client = reqwest::Client::new();
	let url = format!("http://127.0.0.1:{port}/thing");

	let get = client.get(&url).send().await.unwrap();
	let get_len = get.headers().get("content-length").cloned();

	let head = client.head(&url).send().await.unwrap();
	assert_eq!(head.headers().get("content-length"), get_len.as_ref());
	assert!(!head.headers().contains_key("transfer-encoding"));
	assert!(head.text().await.unwrap().is_empty(), "HEAD must send no body");
}