Graceful errors instead of crashing (#1267)

This commit is contained in:
boatbomber
2026-06-01 20:02:39 -07:00
committed by GitHub
parent 85655ca84f
commit 1abf675949
17 changed files with 167 additions and 99 deletions

View File

@@ -12,6 +12,7 @@ use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Context;
use hyper::{
server::Server,
service::{make_service_fn, service_fn},
@@ -30,7 +31,12 @@ impl LiveServer {
LiveServer { serve_session }
}
pub fn start(self, address: SocketAddr) {
/// Starts the server on the given address, blocking until it stops.
///
/// `on_listening` is invoked once the server has successfully bound to the
/// address, so callers can defer printing any "listening" message until
/// after binding can no longer fail (e.g. due to the port being in use).
pub fn start(self, address: SocketAddr, on_listening: impl FnOnce()) -> anyhow::Result<()> {
let serve_session = Arc::clone(&self.serve_session);
let make_service = make_service_fn(move |_conn| {
@@ -53,9 +59,25 @@ impl LiveServer {
}
});
let rt = Runtime::new().unwrap();
let rt = Runtime::new().context("Failed to start the async runtime for the web server")?;
let _guard = rt.enter();
let server = Server::bind(&address).serve(make_service);
rt.block_on(server).unwrap();
let server = Server::try_bind(&address)
.with_context(|| {
format!(
"Could not start the Rojo server on {address}.\n\
The address may already be in use or reserved. Another Rojo server might already \
be running, or another program may be using that port.\n\
You can pick a different port with the --port option."
)
})?
.serve(make_service);
// Binding succeeded, so it's now safe to tell the user we're listening.
on_listening();
rt.block_on(server)
.context("The Rojo web server encountered a fatal error")?;
Ok(())
}
}

View File

@@ -6,7 +6,7 @@
use std::{borrow::Cow, sync::Arc, time::Duration};
use hyper::{header, Body, Method, Request, Response, StatusCode};
use hyper::{Body, Method, Request, Response, StatusCode};
use rbx_dom_weak::types::{Ref, Variant};
use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag};
@@ -16,7 +16,7 @@ use crate::{
web::{
assets,
interface::{ErrorResponse, SERVER_VERSION},
util::json,
util::{json, response},
},
};
@@ -45,17 +45,11 @@ impl UiService {
}
fn handle_logo(&self) -> Response<Body> {
Response::builder()
.header(header::CONTENT_TYPE, "image/png")
.body(Body::from(assets::logo()))
.unwrap()
response(StatusCode::OK, "image/png", assets::logo())
}
fn handle_icon(&self) -> Response<Body> {
Response::builder()
.header(header::CONTENT_TYPE, "image/png")
.body(Body::from(assets::icon()))
.unwrap()
response(StatusCode::OK, "image/png", assets::icon())
}
fn handle_home(&self) -> Response<Body> {
@@ -66,10 +60,11 @@ impl UiService {
</div>
});
Response::builder()
.header(header::CONTENT_TYPE, "text/html")
.body(Body::from(format!("<!DOCTYPE html>{}", page)))
.unwrap()
response(
StatusCode::OK,
"text/html",
format!("<!DOCTYPE html>{}", page),
)
}
fn handle_show_instances(&self) -> Response<Body> {
@@ -80,10 +75,11 @@ impl UiService {
{ Self::instance(&tree, root_id) }
});
Response::builder()
.header(header::CONTENT_TYPE, "text/html")
.body(Body::from(format!("<!DOCTYPE html>{}", page)))
.unwrap()
response(
StatusCode::OK,
"text/html",
format!("<!DOCTYPE html>{}", page),
)
}
fn instance(tree: &RojoTree, id: Ref) -> HtmlContent<'_> {

View File

@@ -1,6 +1,27 @@
use hyper::{header::CONTENT_TYPE, Body, Response, StatusCode};
use serde::{Deserialize, Serialize};
/// Builds an HTTP response, falling back to an empty `500` response (rather than
/// panicking) if the response could not be constructed. With constant headers
/// and a valid status code this never actually fails, but routing every
/// response through here means a malformed response can never crash the server.
pub fn response(
code: StatusCode,
content_type: &'static str,
body: impl Into<Body>,
) -> Response<Body> {
Response::builder()
.status(code)
.header(CONTENT_TYPE, content_type)
.body(body.into())
.unwrap_or_else(|err| {
log::error!("Failed to build HTTP response: {}", err);
let mut fallback = Response::new(Body::empty());
*fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
fallback
})
}
pub fn msgpack_ok<T: Serialize>(value: T) -> Response<Body> {
msgpack(value, StatusCode::OK)
}
@@ -12,18 +33,14 @@ pub fn msgpack<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
.with_struct_map();
if let Err(err) = value.serialize(&mut serializer) {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, "text/plain")
.body(Body::from(err.to_string()))
.unwrap();
return response(
StatusCode::INTERNAL_SERVER_ERROR,
"text/plain",
err.to_string(),
);
};
Response::builder()
.status(code)
.header(CONTENT_TYPE, "application/msgpack")
.body(Body::from(serialized))
.unwrap()
response(code, "application/msgpack", serialized)
}
pub fn serialize_msgpack<T: Serialize>(value: T) -> anyhow::Result<Vec<u8>> {
@@ -49,17 +66,13 @@ pub fn json<T: Serialize>(value: T, code: StatusCode) -> Response<Body> {
let serialized = match serde_json::to_string(&value) {
Ok(v) => v,
Err(err) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(CONTENT_TYPE, "text/plain")
.body(Body::from(err.to_string()))
.unwrap();
return response(
StatusCode::INTERNAL_SERVER_ERROR,
"text/plain",
err.to_string(),
);
}
};
Response::builder()
.status(code)
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serialized))
.unwrap()
response(code, "application/json", serialized)
}