ehttpd 0.14.0

A HTTP server nano-framework, which can be used to create custom small-scale HTTP server applications
Documentation
#![cfg(feature = "server")]

use ehttpd::bytes::{Data, Sink, Source};
use ehttpd::http::Response;
use ehttpd::server::Server;
use std::io::{BufWriter, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};

#[test]
fn request_with_unread_body_is_not_rescheduled() {
    /// The request timeout
    const TIMEOUT: Duration = Duration::from_secs(1);

    // Register a request handler that erroneously ignores the body
    let (request_tx, requests) = mpsc::channel();
    let server: Server<65_536> = Server::with_request_response(1, move |request| {
        request_tx.send(request.target).expect("failed to report request");
        Response::new_200_ok()
    });

    // A transmission consisting of two request bodies
    let source = Source::from(concat! {
        "POST /first HTTP/1.1\r\n",
        "Content-Length: 6\r\n",
        "Connection: keep-alive\r\n",
        "\r\n",
        "<BODY>",
        "GET /second HTTP/1.1\r\n",
        "Content-Length: 6\r\n",
        "Connection: keep-alive\r\n",
        "\r\n",
        "<BODY>",
    });

    // Ensure that the first request gets processed
    server.dispatch(source, Sink::default()).expect("failed to dispatch connection");
    let first = requests.recv_timeout(TIMEOUT);
    assert_eq!(first, Ok(Data::new_static(b"/first")));

    // Ensure that the second request does not get processed
    let second = requests.recv_timeout(TIMEOUT);
    assert!(second.is_err());
}

#[test]
fn request_with_consumed_body_is_rescheduled() {
    /// The request timeout
    const TIMEOUT: Duration = Duration::from_secs(1);

    // Register a request handler that consumes the body appropriately
    let (request_tx, requests) = mpsc::channel();
    let server: Server<65_536> = Server::with_request_response(1, move |request| {
        let mut body_buf = *b"<BODY>";
        request.stream.read_exact(&mut body_buf).expect("failed to read body");
        request_tx.send(request.target).expect("failed to report request");
        Response::new_200_ok()
    });

    // A transmission consisting of two request bodies
    let source = Source::from(concat! {
        "POST /first HTTP/1.1\r\n",
        "Content-Length: 6\r\n",
        "Connection: keep-alive\r\n",
        "\r\n",
        "<BODY>",
        "GET /second HTTP/1.1\r\n",
        "Content-Length: 6\r\n",
        "Connection: keep-alive\r\n",
        "\r\n",
        "<BODY>",
    });

    // Ensure that the first request gets processed
    server.dispatch(source, Sink::default()).expect("failed to dispatch connection");
    let first = requests.recv_timeout(TIMEOUT);
    assert_eq!(first, Ok(Data::new_static(b"/first")));

    // Ensure that the second request gets processed too
    let second = requests.recv_timeout(TIMEOUT);
    assert_eq!(second, Ok(Data::new_static(b"/second")));
}

#[test]
fn bridge_request_response_writes_response() {
    /// The response timeout
    const TIMEOUT: Duration = Duration::from_secs(1);

    // Register a request handler that returns a body
    let server: Server<65_536> = Server::with_request_response(1, |_| {
        let mut response = Response::new_200_ok();
        response.set_body_data("hello");
        response
    });

    // Create a loopback response connection
    let listener = TcpListener::bind(("127.0.0.1", 0)).expect("failed to bind response socket");
    let mut output = TcpStream::connect(listener.local_addr().expect("failed to get response address"))
        .expect("failed to connect response socket");
    output.set_read_timeout(Some(TIMEOUT)).expect("failed to set response timeout");
    let (sink, _) = listener.accept().expect("failed to accept response socket");

    // A normal HTTP request
    let source = Source::from(concat! {
        "GET / HTTP/1.1\r\n",
        "Connection: close\r\n",
        "\r\n",
    });

    // Dispatch a GET request and read the response
    server.dispatch(source, Sink::from(BufWriter::new(sink))).expect("failed to dispatch connection");
    let mut response = Vec::new();
    output.read_to_end(&mut response).expect("failed to read response");

    // Ensure that the response contains the expected body
    let expected = concat! {
        "HTTP/1.1 200 OK\r\n",
        "Content-Length: 5\r\n",
        "\r\n",
        "hello",
    };
    assert_eq!(response, expected.as_bytes());
}

#[test]
fn bridge_request_response_suppresses_head_body() {
    /// The response timeout
    const TIMEOUT: Duration = Duration::from_secs(1);

    // Register a request handler that returns a body
    let server: Server<65_536> = Server::with_request_response(1, |_| {
        let mut response = Response::new_200_ok();
        response.set_body_data("hello");
        response
    });

    // Create a loopback response connection
    let listener = TcpListener::bind(("127.0.0.1", 0)).expect("failed to bind response socket");
    let mut output = TcpStream::connect(listener.local_addr().expect("failed to get response address"))
        .expect("failed to connect response socket");
    output.set_read_timeout(Some(TIMEOUT)).expect("failed to set response timeout");
    let (sink, _) = listener.accept().expect("failed to accept response socket");

    // A normal HTTP request
    let source = Source::from(concat! {
        "HEAD / HTTP/1.1\r\n",
        "Connection: close\r\n",
        "\r\n",
    });

    // Dispatch a HEAD request and read the response
    server.dispatch(source, Sink::from(BufWriter::new(sink))).expect("failed to dispatch connection");
    let mut response = Vec::new();
    output.read_to_end(&mut response).expect("failed to read response");

    // Ensure that the response body is suppressed
    let expected = concat! {
        "HTTP/1.1 200 OK\r\n",
        "Content-Length: 5\r\n",
        "\r\n",
    };
    assert_eq!(response, expected.as_bytes());
}

#[test]
fn bridge_request_response_honors_response_connection_close() {
    /// The request timeout
    const TIMEOUT: Duration = Duration::from_secs(1);

    // Register a request handler that closes the connection
    let (request_tx, requests) = mpsc::channel();
    let server: Server<65_536> = Server::with_request_response(1, move |request| {
        request_tx.send(request.target).expect("failed to report request");
        let mut response = Response::new_200_ok();
        response.set_connection_close();
        response
    });

    // A transmission consisting of two requests
    let source = Source::from(concat! {
        "GET /first HTTP/1.1\r\n",
        "\r\n",
        "GET /second HTTP/1.1\r\n",
        "\r\n",
    });

    // Ensure that the first request gets processed
    server.dispatch(source, Sink::default()).expect("failed to dispatch connection");
    assert_eq!(requests.recv_timeout(TIMEOUT), Ok(Data::new_static(b"/first")));

    // Ensure that the second request does not get processed
    assert!(requests.recv_timeout(TIMEOUT).is_err());
}

#[test]
fn incomplete_connection_times_out_after_twenty_seconds() {
    /// The server thread startup timeout
    const STARTUP_TIMEOUT: Duration = Duration::from_secs(3);
    /// The expected timeout lower bound
    const TIMEOUT_LOWER_BOUND: Duration = Duration::from_secs(19);
    /// The expected timeout upper bound
    const TIMEOUT_UPPER_BOUND: Duration = Duration::from_secs(25);
    /// The client-side safety timeout
    const CLIENT_TIMEOUT: Duration = Duration::from_secs(30);

    // Reserve an unused local address
    let probe = TcpListener::bind(("127.0.0.1", 0)).expect("failed to reserve test address");
    let address = probe.local_addr().expect("failed to get test address");
    drop(probe);

    // Start a server that rejects incomplete requests
    thread::spawn(move || {
        let server: Server<65_536> =
            Server::with_request_response(1, |_| panic!("incomplete request must not reach the handler"));
        let result = server.accept(address);
        panic!("test server stopped accepting connections: {result:?}");
    });

    // Connect once the server is ready and send an incomplete request
    thread::sleep(STARTUP_TIMEOUT);
    let mut connection = TcpStream::connect(address).expect("failed to connect to server");
    connection.set_read_timeout(Some(CLIENT_TIMEOUT)).expect("failed to set client timeout");
    connection.write_all(b"GET / HTTP/1.1\r\n").expect("failed to write incomplete request");

    // Wait for the server to close the connection
    let started = Instant::now();
    let mut response = Vec::new();
    connection.read_to_end(&mut response).expect("server did not close the timed out connection");

    // Ensure that the connection closed after the expected timeout
    let elapsed = started.elapsed();
    assert!(response.is_empty());
    assert!(elapsed >= TIMEOUT_LOWER_BOUND, "connection closed after {elapsed:?}");
    assert!(elapsed <= TIMEOUT_UPPER_BOUND, "connection closed after {elapsed:?}");
}