I'm developing a simple Actix Web backend in Rust and running it on WSL (Windows Subsystem for Linux).
The server runs fine the first time, but when I use cargo watch -x run to auto-restart it on code changes, it fails to start again on the same port, and it's not a problem with cargo watch, it's the same if I run the code manually.
If I change the port (e.g. from 3000 to 4000), it runs again. But if I keep the same port, I see nothing — the process just hangs.
Here’s the minimal reproducible code:
use actix_web::{App, HttpResponse, HttpServer, Responder, post, web};
#[post("/upload")]
async fn upload(data: web::Bytes) -> impl Responder {
println!("Received data: {:?}", data);
HttpResponse::Ok().json("all right!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Starting the server...");
HttpServer::new(|| {
App::new()
.wrap(
actix_cors::Cors::default()
.allowed_origin("http://localhost:5173")
.allowed_methods(vec!["GET", "POST"])
.allow_any_header(),
)
.service(upload)
})
.bind(("127.0.0.1", 3000))?
.run()
.await
}
This is the first backend I’m building with Rust, so I might be missing something obvious.
the minimal reproducible code:
Thank you for that, and for the cargo watch -x run context.
But the underlying situation is even simpler than that.
Suppose that no edits are made.
You could run the webserver, hit CTRL/C, immediately re-run the webserver daemon,
and observe the same symptom.
Suppose you used & to ask bash to put the daemon into the background,
and then while it's still running you tried to re-run the daemon in the foreground.
That would fail, and with good reason.
When an incoming connection arrives, the kernel needs to know where to send it.
The first daemon owns port 3000, so the second daemon
won't be able to "pick up the phone".
You can diagnose this with netstat -an.
Or focus on a single port with netstat -an | grep 3000.
But nevermind background processes. Your situation is we'll run the daemon in the foreground, interrupt it, perhaps with CTRL/C, and then want to re-run it. Yet we cannot immediately re-bind to that port. Why not? Examine the "netstat" output, and notice the TIME_WAIT status. The first process is gone, yet a zombie socket lives on! Which spells trouble when the second process tries to bind.
Please read the SO_REUSEADDR section of the man page. During development / debugging you will want to specify that option. It sets the TIME_WAIT interval to zero, so the second process will immediately succeed in binding to your favorite port.
use std::net::TcpListener;
...
println!("Starting the server...");
let addr = "127.0.0.1:3000";
let listener = TcpListener::bind(addr)?;
listener.set_reuse_addr(true)?; // this specifies SO_REUSEADDR
HttpServer::new(|| {
App::new()
.wrap(
actix_cors::Cors::default()
.allowed_origin("http://localhost:5173")
.allowed_methods(vec!["GET", "POST"])
.allow_any_header(),
)
.service(upload)
})
.listen(listener)?
.run()
.await
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With