atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 7288fc269bb6

tarball

atagen <boss@atagen.co>2026-10-10 15:15 UTC

5 files changed+56-6
jelqd: never leave the page waiting on a reply that will not come

A failed /ui/send fetch or an unreadable reply threw before anything was logged, so requests piled up in the log with no answers and no explanation. Handler panics now become a 500 problem instead of a dropped connection, and the page logs any request it could not get an answer for.
Modified file: Cargo.lock+25 additions
@@ -67,6 +67,12 @@ dependencies = [
6767 ]
6868
6969 [[package]]
70+name = "bitflags"
71+version = "2.13.2"
72+source = "registry+https://github.com/rust-lang/crates.io-index"
73+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
74+
75+[[package]]
7076 name = "block-buffer"
7177 version = "0.10.4"
7278 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -315,6 +321,7 @@ dependencies = [
315321 "sha2",
316322 "tokio",
317323 "tower",
324+ "tower-http",
318325 ]
319326
320327 [[package]]
@@ -636,6 +643,24 @@ dependencies = [
636643 ]
637644
638645 [[package]]
646+name = "tower-http"
647+version = "0.6.11"
648+source = "registry+https://github.com/rust-lang/crates.io-index"
649+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
650+dependencies = [
651+ "bitflags",
652+ "bytes",
653+ "futures-util",
654+ "http",
655+ "http-body",
656+ "http-body-util",
657+ "pin-project-lite",
658+ "tower-layer",
659+ "tower-service",
660+ "tracing",
661+]
662+
663+[[package]]
639664 name = "tower-layer"
640665 version = "0.3.3"
641666 source = "registry+https://github.com/rust-lang/crates.io-index"
Modified file: Cargo.toml+1 additions
@@ -19,4 +19,5 @@ serde_json = "1"
1919 sha2 = "0.10"
2020 tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread"] }
2121 tower = { version = "0.5", features = ["util"] }
22+tower-http = { version = "0.6", features = ["catch-panic"] }
2223
Modified file: src/http/middleware.rs+10 additions
@@ -156,3 +156,13 @@ pub async fn chrome(request: Request, next: Next) -> Response {
156156 .or_insert(HeaderValue::from_static("no-store, private"));
157157 response
158158 }
159+
160+/// A handler panic would otherwise drop the connection with no reply at all.
161+pub fn panicked(_: Box<dyn std::any::Any + Send + 'static>) -> Response {
162+ Problem::new(
163+ StatusCode::INTERNAL_SERVER_ERROR,
164+ "server-fault",
165+ "The server has had an internal event of its own. It is not discussing it.",
166+ )
167+ .into_response()
168+}
Modified file: src/http/mod.rs+4 additions
@@ -19,6 +19,8 @@ use axum::{
1919 routing::{MethodRouter, any},
2020 };
2121
22+use tower_http::catch_panic::CatchPanicLayer;
23+
2224 use crate::{config::Config, game::Run};
2325 use ctx::Ctx;
2426 use resource::Resource;
@@ -68,6 +70,7 @@ pub fn router(state: Shared) -> Router {
6870 .route("/ui/send", any(ui::send))
6971 .with_state(api.clone());
7072 api.merge(ui)
73+ .layer(CatchPanicLayer::custom(middleware::panicked))
7174 }
7275
7376 fn global(resource: &'static Resource<Shared>) -> MethodRouter<Shared> {
@@ -109,5 +112,6 @@ fn api(state: Shared) -> Router {
109112 .route("/leaderboard", global(&meta::LEADERBOARD))
110113 .fallback(meta::not_found)
111114 .layer(axum::middleware::from_fn(middleware::chrome))
115+ .layer(CatchPanicLayer::custom(middleware::panicked))
112116 .with_state(state)
113117 }
Modified file: src/ui.html+16 additions−6 deletions
@@ -402,12 +402,22 @@ async function go(event) {
402402 log("> " + method + " " + short(path));
403403 for (const [name, v] of headers) log("> " + name + ": " + v);
404404 headers.push(["Accept-Language", $("locale").value]);
405- const response = await fetch("/ui/send", {
406- method: "POST",
407- headers: { "Content-Type": "application/json" },
408- body: JSON.stringify({ method, path, headers, body }),
409- });
410- const r = await response.json();
405+ let r;
406+ try {
407+ const response = await fetch("/ui/send", {
408+ method: "POST",
409+ headers: { "Content-Type": "application/json" },
410+ body: JSON.stringify({ method, path, headers, body }),
411+ });
412+ r = await response.json();
413+ } catch (error) {
414+ log("! The server did not answer. Is jelqd still running? (" + error.message + ")\n");
415+ return;
416+ }
417+ if (!Array.isArray(r.headers)) {
418+ log("! The page could not read that reply:\n " + JSON.stringify(r) + "\n");
419+ return;
420+ }
411421 log(describe(r) + "\n");
412422 absorb(r, method, path);
413423 }