diff --git a/Cargo.lock b/Cargo.lock index 02a4720afb..0bcc593a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,54 +187,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "actix-web-lab" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b7c50a90657ef1868db9dd85f74e82c4d9858ce583d4639ae3583f0bb97775" -dependencies = [ - "actix-http", - "actix-router", - "actix-service", - "actix-utils", - "actix-web", - "actix-web-lab-derive", - "ahash 0.8.6", - "arc-swap", - "async-trait", - "bytes", - "bytestring", - "csv", - "derive_more", - "futures-core", - "futures-util", - "http", - "impl-more", - "itertools 0.11.0", - "local-channel", - "mediatype", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_html_form", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "actix-web-lab-derive" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16294584c7794939b1e5711f28e7cae84ef30e62a520db3f9af425f85269bcd2" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -417,12 +369,6 @@ version = "1.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1f8f5a6f3d50d89e3797d7593a50f96bb2aaa20ca0cc7be1fb673232c91d72" -[[package]] -name = "arc-swap" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" - [[package]] name = "arrayvec" version = "0.5.2" @@ -1095,27 +1041,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "csv" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" -dependencies = [ - "memchr", -] - [[package]] name = "darling" version = "0.14.4" @@ -1670,7 +1595,6 @@ name = "fastn-core" version = "0.1.0" dependencies = [ "actix-web", - "actix-web-lab", "antidote", "async-lock", "async-recursion", @@ -1689,6 +1613,8 @@ dependencies = [ "fluent", "ftd 0.3.0", "futures", + "futures-core", + "futures-util", "hyper", "ignore", "indoc 2.0.4", @@ -2375,12 +2301,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "impl-more" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" - [[package]] name = "include_dir" version = "0.7.3" @@ -2764,12 +2684,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "mediatype" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c408dc227d302f1496c84d9dc68c00fec6f56f9228a18f3023f976f3ca7c945" - [[package]] name = "memchr" version = "2.6.4" @@ -4053,19 +3967,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "serde_html_form" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde65b75f2603066b78d6fa239b2c07b43e06ead09435f60554d3912962b4a3c" -dependencies = [ - "form_urlencoded", - "indexmap 2.1.0", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_json" version = "1.0.108" diff --git a/Cargo.toml b/Cargo.toml index ff2b38b589..77f9159d31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,8 @@ format_num = "0.1" ftd = { path = "ftd" } fastn-js = { path = "fastn-js" } futures = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } +futures-core = "0.3" home = "0.5" ignore = "0.4" include_dir = "0.7" diff --git a/fastn-core/Cargo.toml b/fastn-core/Cargo.toml index 509bf5e86b..c60572d680 100644 --- a/fastn-core/Cargo.toml +++ b/fastn-core/Cargo.toml @@ -31,7 +31,6 @@ github-auth = ["dep:oauth2"] [dependencies] actix-web.workspace = true -actix-web-lab.workspace = true antidote.workspace = true async-lock.workspace = true dirs.workspace = true @@ -41,6 +40,8 @@ clap.workspace = true colored.workspace = true native-tls.workspace = true deadpool-postgres.workspace = true +futures-util.workspace = true +futures-core.workspace = true postgres-types.workspace = true postgres-native-tls.workspace = true tokio-postgres.workspace = true diff --git a/fastn-core/src/catch_panic.rs b/fastn-core/src/catch_panic.rs new file mode 100644 index 0000000000..21985d23f6 --- /dev/null +++ b/fastn-core/src/catch_panic.rs @@ -0,0 +1,151 @@ +// borrowed from https://github.com/robjtede/actix-web-lab/ (MIT) +use std::{ + future::{ready, Ready}, + panic::AssertUnwindSafe, + rc::Rc, +}; + +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + error, +}; +use futures_core::future::LocalBoxFuture; +use futures_util::FutureExt as _; + +/// A middleware to catch panics in wrapped handlers and middleware, returning empty 500 responses. +/// +/// **This middleware should never be used as replacement for proper error handling.** See [this +/// thread](https://github.com/actix/actix-web/issues/1501#issuecomment-627517783) for historical +/// discussion on why Actix Web does not do this by default. +/// +/// It is recommended that this middleware be registered last. That is, `wrap`ed after everything +/// else except `Logger`. +/// +/// # Examples +/// +/// ``` +/// # use actix_web::App; +/// use actix_web_lab::middleware::CatchPanic; +/// +/// App::new().wrap(CatchPanic::default()) +/// # ; +/// ``` +/// +/// ```no_run +/// # use actix_web::App; +/// use actix_web::middleware::{Logger, NormalizePath}; +/// use actix_web_lab::middleware::CatchPanic; +/// +/// // recommended wrap order +/// App::new() +/// .wrap(NormalizePath::default()) +/// .wrap(CatchPanic::default()) // <- after everything except logger +/// .wrap(Logger::default()) +/// # ; +/// ``` +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct CatchPanic; + +impl Transform for CatchPanic +where + S: Service, Error = actix_web::Error> + 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = CatchPanicMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(CatchPanicMiddleware { + service: Rc::new(service), + })) + } +} + +pub struct CatchPanicMiddleware { + service: Rc, +} + +impl Service for CatchPanicMiddleware +where + S: Service, Error = actix_web::Error> + 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + AssertUnwindSafe(self.service.call(req)) + .catch_unwind() + .map(move |res| match res { + Ok(Ok(res)) => Ok(res), + Ok(Err(svc_err)) => Err(svc_err), + Err(_panic_err) => Err(error::ErrorInternalServerError("500 Server Error")), + }) + .boxed_local() + } +} + +#[cfg(test)] +mod tests { + use actix_web::{ + body::{to_bytes, MessageBody}, + dev::{Service as _, ServiceFactory}, + http::StatusCode, + test, web, App, Error, + }; + + use super::*; + + fn test_app() -> App< + impl ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Config = (), + InitError = (), + Error = Error, + >, + > { + App::new() + .wrap(CatchPanic::default()) + .route("/", web::get().to(|| async { "content" })) + .route( + "/disco", + #[allow(unreachable_code)] + web::get().to(|| async { + panic!("the disco"); + "" + }), + ) + } + + #[actix_web::test] + async fn pass_through_no_panic() { + let app = test::init_service(test_app()).await; + + let req = test::TestRequest::default().to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + let body = test::read_body(res).await; + assert_eq!(body, "content"); + } + + #[actix_web::test] + async fn catch_panic_return_internal_server_error_response() { + let app = test::init_service(test_app()).await; + + let req = test::TestRequest::with_uri("/disco").to_request(); + let err = match app.call(req).await { + Ok(_) => panic!("unexpected Ok response"), + Err(err) => err, + }; + let res = err.error_response(); + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + let body = to_bytes(res.into_body()).await.unwrap(); + assert!(body.is_empty()); + } +} diff --git a/fastn-core/src/commands/serve.rs b/fastn-core/src/commands/serve.rs index 41a67dc94a..b3cac66e35 100644 --- a/fastn-core/src/commands/serve.rs +++ b/fastn-core/src/commands/serve.rs @@ -728,7 +728,7 @@ You can try without providing port, it will automatically pick unused port."#, inline_css: inline_css.clone(), package_name: package_name.clone(), })) - .wrap(actix_web_lab::middleware::CatchPanic::default()) + .wrap(fastn_core::catch_panic::CatchPanic::default()) .wrap( actix_web::middleware::Logger::new( r#""%r" %Ts %s %b %a "%{Referer}i" "%{User-Agent}i""#, diff --git a/fastn-core/src/lib.rs b/fastn-core/src/lib.rs index 91360f3e27..51bdd263bc 100644 --- a/fastn-core/src/lib.rs +++ b/fastn-core/src/lib.rs @@ -32,6 +32,7 @@ mod tracker; mod translation; mod version; // mod wasm; +pub(crate) mod catch_panic; mod library2022; mod workspace; diff --git a/fastn-core/src/tutor.rs b/fastn-core/src/tutor.rs index d4b5cd635c..2501cf07aa 100644 --- a/fastn-core/src/tutor.rs +++ b/fastn-core/src/tutor.rs @@ -101,7 +101,7 @@ pub async fn process( }; let state = TutorState { - fs_state, + done: fs_state.done, current: CURRENT_TUTORIAL.read().await.as_ref().map(|t| t.id.clone()), }; @@ -110,7 +110,7 @@ pub async fn process( #[derive(Debug, Default, serde::Serialize)] struct TutorState { - fs_state: TutorStateFS, + done: Vec, current: Option, }