From 3e313261f288867de73992e8d1581cb451a9af25 Mon Sep 17 00:00:00 2001 From: Bruno Cesar Rocha Date: Tue, 29 Sep 2026 15:35:56 +0100 Subject: [PATCH] fix: keep development server bound to requested interface --- .../marmite/references/cli-reference.md | 4 +-- example/content/docs/getting-started.md | 6 ++-- .../docs/marmite-command-line-interface.md | 16 +++++---- .../marmite-0-4-3-release-notes.md | 19 ++++++++++ src/cli.rs | 4 +-- src/server.rs | 34 +++++++++++------- src/tests/server.rs | 35 +++++++++++++++++++ 7 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 example/content/release-notes/marmite-0-4-3-release-notes.md diff --git a/.agents/skills/marmite/references/cli-reference.md b/.agents/skills/marmite/references/cli-reference.md index 6ec5b18..9433a59 100644 --- a/.agents/skills/marmite/references/cli-reference.md +++ b/.agents/skills/marmite/references/cli-reference.md @@ -201,8 +201,8 @@ marmite [site_folder] atproto publish --dry-run |------|-------|---------|-------------| | `--verbose` | `-v` | 0 (warn) | Verbosity: `-v` info, `-vv` debug, `-vvv` trace, `-vvvv` trace all | | `--watch` | `-w` | false | Auto-rebuild on file changes | -| `--serve` | | false | Start built-in HTTP server | -| `--bind ` | | `0.0.0.0:8000` | Server bind address (requires `--serve`) | +| `--serve` | | false | Start development-only HTTP server (not for production) | +| `--bind ` | | `127.0.0.1:8000` | Server bind address (requires `--serve`) | | `--config ` | `-c` | `marmite.yaml` | Path to config file | | `--force` | | false | Force full rebuild | diff --git a/example/content/docs/getting-started.md b/example/content/docs/getting-started.md index 3c2b673..44da6c8 100644 --- a/example/content/docs/getting-started.md +++ b/example/content/docs/getting-started.md @@ -193,13 +193,15 @@ webserver such as **Apache** or **Nginx**, or most probably use a free static hosting service such as **Github pages**, **Netlify** or **CLoudflare**. However, during the content writing you want to check how the website looks, -so the built-in server comes handy, just add `--serve` +so the built-in development-only server comes handy, just add `--serve`. +It defaults to `127.0.0.1:8000` and is not for production deployments. ``` marmite myblog site --watch --serve ... Watching for changes in folder: myblog Starting built-in HTTP server +Development-only server. Not for production deployments. Server started at http://localhost:8000/ - Type ^C to stop. ``` @@ -210,7 +212,7 @@ When the server is running, a [[marmite-toolbar]] icon appears at the top-left c The server also exposes a [[content-management-api]] under `/__marmite__/` for programmatic content and config management. If you want to share your site with others in the same network, just -pass `--bind "0.0.0.0:8000` and then share your local IP address. +pass `--bind 0.0.0.0:8000` and then share your local IP address. ## Media diff --git a/example/content/docs/marmite-command-line-interface.md b/example/content/docs/marmite-command-line-interface.md index a60fe1a..952eafa 100644 --- a/example/content/docs/marmite-command-line-interface.md +++ b/example/content/docs/marmite-command-line-interface.md @@ -174,19 +174,23 @@ Watching for changed on: myblog/ Marmite generates a flat site, which means you can open it directly on your browser (with some limitations) or use any web server to serve it. -Marmite comes with a built-in server to use only locally. +Marmite comes with a development-only server, not for production deployments. +It binds to `127.0.0.1:8000` by default. Use `--serve` to start the server. ```console -$ /marmite myblog /var/www/myblog -w +$ /marmite myblog /var/www/myblog -w --serve Site generated at: /var/www/myblog Watching for changed on: myblog/ Starting built-in HTTP server... -Server started at http://0.0.0.0:8000/ - Type ^C to stop. +Development-only server. Not for production deployments. +Server started at http://127.0.0.1:8000/ - Type ^C to stop. ``` -If you want to change the address use `--bind ip:port` +If you want to change the address use `--bind ip:port`. For access from other +machines on your network, use `--bind 0.0.0.0:8000`. If the requested port is +unavailable, the server uses an OS-assigned port on the requested interface. > [!IMPORTANT] > The built-in server is not suitable for production, when deploying use a webserver such as [Nginx] or read the [[hosting]] guide to learn how to deploy to Github pages, Gitlab pages, Codeberg Pages, Netlify and more. @@ -591,9 +595,9 @@ Options: -w, --watch Detect changes and rebuild the site automatically --serve - Serve the site with a built-in HTTP server + Serve the site with a development-only HTTP server (not for production) --bind - Address to bind the server [default: 0.0.0.0:8000] + Address to bind the server [default: 127.0.0.1:8000] -c, --config Path to custom configuration file [default: marmite.yaml] --init-templates diff --git a/example/content/release-notes/marmite-0-4-3-release-notes.md b/example/content/release-notes/marmite-0-4-3-release-notes.md new file mode 100644 index 0000000..4e8b4a9 --- /dev/null +++ b/example/content/release-notes/marmite-0-4-3-release-notes.md @@ -0,0 +1,19 @@ +--- +date: 2026-09-29 +title: Marmite 0.4.3 Release Notes +slug: marmite-0-4-3-release-notes +stream: draft +tags: [release-notes, marmite] +--- + +## Bug Fixes + +### Development server binding + +`--serve` now defaults to `127.0.0.1:8000`. If the requested port is unavailable, +the fallback uses an OS-assigned port while preserving the requested interface, +instead of binding to all interfaces. Use `--bind 0.0.0.0:8000` explicitly for +access from other machines on your network. + +The CLI help and server startup message now explicitly identify the built-in +server as development-only, not for production deployments. diff --git a/src/cli.rs b/src/cli.rs index 1445ec7..81ac42e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -52,12 +52,12 @@ pub struct Cli { #[arg(long, short)] pub watch: bool, - /// Serve the site with a built-in HTTP server + /// Serve the site with a development-only HTTP server (not for production) #[arg(long)] pub serve: bool, /// Address to bind the server - #[arg(long, default_value = "0.0.0.0:8000", requires = "serve")] + #[arg(long, default_value = "127.0.0.1:8000", requires = "serve")] pub bind: String, /// Path to custom configuration file diff --git a/src/server.rs b/src/server.rs index 802feaa..8726a26 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,11 @@ +//! Development-only preview server and editor API, not for production deployments. + use chrono::Utc; use log::{error, info, warn}; use serde_json::json; use std::fmt::Write as _; use std::io::{Cursor, ErrorKind}; +use std::net::ToSocketAddrs; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::mpsc; @@ -23,7 +26,6 @@ pub struct ServerContext { pub watch_enabled: bool, } -const FALLBACK_BIND_ADDRESS: &str = "0.0.0.0:0"; const LIVE_RELOAD_SCRIPT_PATH: &str = "__marmite__/livereload.js"; const TOOLBAR_JS_PATH: &str = "__marmite__/toolbar.js"; const TOOLBAR_CSS_PATH: &str = "__marmite__/toolbar.css"; @@ -94,23 +96,31 @@ static EDITOR_HTML: LazyLock = LazyLock::new(|| { .expect("embedded editor.html missing - this is a build-time error") }); +fn bind_server(bind_address: &str) -> Result> { + let mut addresses: Vec<_> = bind_address.to_socket_addrs()?.collect(); + Server::http(addresses.as_slice()).or_else(|err| { + warn!( + "Failed to start server on address {bind_address}: {err:?}. Falling back to OS-assigned port on the same interface." + ); + // Only change the port; retain the requested IPv4/IPv6 interfaces. + for address in &mut addresses { + address.set_port(0); + } + Server::http(addresses.as_slice()) + }) +} + pub fn start(bind_address: &str, ctx: &ServerContext, live_reload: Option<&LiveReload>) { - let server = match Server::http(bind_address) { + let server = match bind_server(bind_address) { Ok(server) => server, Err(e) => { - warn!( - "Failed to start server on address {bind_address}: {e:?}. Falling back to OS-assigned port." - ); - match Server::http(FALLBACK_BIND_ADDRESS) { - Ok(server) => server, - Err(e) => { - error!("Failed to start server on fallback address: {e:?}"); - return; - } - } + error!("Failed to start development server: {e:?}"); + return; } }; + info!("Development-only server. Not for production deployments."); + let Some(server_addr) = server.server_addr().to_ip() else { warn!("Failed to get server IP address, using fallback display"); // Use a fallback approach for display purposes diff --git a/src/tests/server.rs b/src/tests/server.rs index 6936e82..e770cc3 100644 --- a/src/tests/server.rs +++ b/src/tests/server.rs @@ -3,6 +3,41 @@ use std::fs; use std::io::Read; use tempfile::TempDir; +#[test] +fn test_default_bind_is_loopback_and_custom_bind_is_preserved() { + use clap::Parser; + + let args = crate::cli::Cli::try_parse_from(["marmite", ".", "--serve"]).unwrap(); + assert_eq!(args.bind, "127.0.0.1:8000"); + let args = + crate::cli::Cli::try_parse_from(["marmite", ".", "--serve", "--bind", "0.0.0.0:9000"]) + .unwrap(); + assert_eq!(args.bind, "0.0.0.0:9000"); +} + +#[test] +fn test_bind_fallback_preserves_interface() { + for ip in ["127.0.0.1", "0.0.0.0", "::1"] { + let ip: std::net::IpAddr = ip.parse().unwrap(); + let occupied = match std::net::TcpListener::bind((ip, 0)) { + Ok(listener) => listener, + Err(err) if ip.is_ipv6() && err.kind() == ErrorKind::AddrNotAvailable => continue, + Err(err) => panic!("Failed to bind test listener: {err}"), + }; + let requested = occupied.local_addr().unwrap(); + let server = bind_server(&requested.to_string()).unwrap(); + let actual = server.server_addr().to_ip().unwrap(); + assert_eq!(actual.ip(), requested.ip()); + assert_ne!(actual.port(), 0); + assert_ne!(actual.port(), requested.port()); + } +} + +#[test] +fn test_invalid_bind_does_not_fall_back_to_all_interfaces() { + assert!(bind_server("invalid-address").is_err()); +} + #[test] fn test_render_not_found_with_file() { let temp_dir = TempDir::new().unwrap();