diff --git a/.github/workflows/e2e-cli-master.yaml b/.github/workflows/e2e-cli-master.yaml index 181cd5f9b..693daf53c 100644 --- a/.github/workflows/e2e-cli-master.yaml +++ b/.github/workflows/e2e-cli-master.yaml @@ -28,7 +28,7 @@ jobs: cargo install --path . working-directory: ./loco-cli - run: | - loco new -n saas -t saas --db sqlite --bg async --assets none + loco new -n saas -t saas --db sqlite --bg async --assets serverside env: ALLOW_IN_GIT_REPO: true - run: | diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index a292d1723..a18988320 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -299,6 +299,43 @@ async fn current( } ``` +## Fallback + +When choosing the SaaS starter (or any starter that is not API-first), you get a default fallback behavior with the _Loco welcome screen_. This is a development-only mode where a `404` request shows you a nice and friendly page that tells you what happened and what to do next. + + +You can disable or customize this behavior in your `development.yaml` file. You can set a few options: + + +```yaml +# the default pre-baked welcome screen +fallback: + enable: true +``` + +```yaml +# a different predefined 404 page +fallback: + enable: true + file: assets/404.html +``` + +```yaml +# a message, and customizing the status code to return 200 instead of 404 +fallback: + enable: true + code: 200 + not_found: cannot find this resource +``` + +For production, it's recommended to disable this. + +```yaml +# disable. you can also remove the `fallback` section entirely to disable +fallback: + enable: false +``` + ## Remote IP When your app is under a proxy or a load balancer (e.g. Nginx, ELB, etc.), it does not face the internet directly, which is why if you want to find out the connecting client IP, you'll get a socket which indicates an IP that is actually your load balancer instead. diff --git a/loco-cli/tests/cmd/starters/generate-starters.trycmd b/loco-cli/tests/cmd/starters/generate-starters.trycmd index ebebe2033..40ade2828 100644 --- a/loco-cli/tests/cmd/starters/generate-starters.trycmd +++ b/loco-cli/tests/cmd/starters/generate-starters.trycmd @@ -4,6 +4,8 @@ $ ALLOW_IN_GIT_REPO="" LOCO_APP_NAME="test_saas_template" LOCO_FOLDER_NAME=saas_ 🚂 Loco app generated successfully in: [CWD]/test_saas_template +- database: You've selected `postgres` as your DB provider (you should have a postgres instance to connect to) + ``` ```console @@ -12,6 +14,8 @@ $ ALLOW_IN_GIT_REPO="true" LOCO_APP_NAME="test_rest_api_template" LOCO_FOLDER_NA 🚂 Loco app generated successfully in: [CWD]/test_rest_api_template + + ``` ```console @@ -20,4 +24,6 @@ $ ALLOW_IN_GIT_REPO="true" LOCO_APP_NAME="test_lightweight_template" LOCO_FOLDER 🚂 Loco app generated successfully in: [CWD]/test_lightweight_template + + ``` diff --git a/src/config.rs b/src/config.rs index d10d28bf6..e465de2c8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -391,8 +391,25 @@ pub struct Middlewares { pub secure_headers: Option, /// Calculates a remote IP based on `X-Forwarded-For` when behind a proxy pub remote_ip: Option, + /// Configure fallback behavior when hitting a missing URL + pub fallback: Option, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct FallbackConfig { + /// By default when enabled, returns a prebaked 404 not found page optimized + /// for development. For production set something else (see fields below) + pub enable: bool, + /// For the unlikely reason to return something different than `404`, you + /// can set it here + pub code: Option, + /// Returns content from a file pointed to by this field with a `404` status + /// code. + pub file: Option, + /// Returns a "404 not found" with a single message string. This sets the + /// message. + pub not_found: Option, +} /// Static asset middleware configuration #[derive(Debug, Clone, Deserialize, Serialize)] pub struct StaticAssetsMiddleware { diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index b3103ed53..b1bb79a16 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -4,7 +4,12 @@ use std::{fmt, path::PathBuf, time::Duration}; -use axum::{http, response::IntoResponse, Router as AXRouter}; +use axum::{ + http, + response::{Html, IntoResponse}, + Router as AXRouter, +}; +use hyper::StatusCode; use lazy_static::lazy_static; use regex::Regex; use tower_http::{ @@ -25,14 +30,14 @@ use super::{ }; use crate::{ app::AppContext, - config, + config::{self, FallbackConfig}, controller::middleware::{ etag::EtagLayer, remote_ip::RemoteIPLayer, request_id::{request_id_middleware, LocoRequestId}, }, environment::Environment, - errors, Result, + errors, Error, Result, }; lazy_static! { @@ -297,6 +302,12 @@ impl AppRoutes { tracing::info!("[Middleware] +secure headers"); } + if let Some(fallback) = &ctx.config.server.middlewares.fallback { + if fallback.enable { + app = Self::add_fallback(app, fallback)?; + } + } + app = Self::add_powered_by_header(app, &ctx.config.server); app = Self::add_request_id_middleware(app); @@ -305,6 +316,36 @@ impl AppRoutes { Ok(router) } + fn add_fallback( + app: AXRouter, + fallback: &FallbackConfig, + ) -> Result> { + let app = if let Some(path) = &fallback.file { + app.fallback_service(ServeFile::new(path)) + } else if let Some(not_found) = &fallback.not_found { + let not_found = not_found.to_string(); + let code = fallback + .code + .map(StatusCode::from_u16) + .transpose() + .map_err(|e| Error::Message(format!("{e}")))? + .unwrap_or(StatusCode::NOT_FOUND); + app.fallback(move || async move { (code, not_found) }) + } else { + //app.fallback(handler) + let code = fallback + .code + .map(StatusCode::from_u16) + .transpose() + .map_err(|e| Error::Message(format!("{e}")))? + .unwrap_or(StatusCode::NOT_FOUND); + let content = include_str!("fallback.html"); + app.fallback(move || async move { (code, Html(content)) }) + }; + tracing::info!("[Middleware] +fallback"); + Ok(app) + } + fn add_request_id_middleware(app: AXRouter) -> AXRouter { let app = app.layer(axum::middleware::from_fn(request_id_middleware)); tracing::info!("[Middleware] +request id"); diff --git a/src/controller/fallback.html b/src/controller/fallback.html new file mode 100644 index 000000000..610b6aee9 --- /dev/null +++ b/src/controller/fallback.html @@ -0,0 +1,59 @@ + + + + + + + Welcome to Loco! + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +

Welcome to Loco!

+

It looks like you've just started your Loco server, and this is the fallback page displayed when a route doesn't exist.

+ +
+
+

Remove this Fallback Page

+

To remove this fallback page, adjust the configuration in your config/development.yaml file. Look for the fallback: setting and disable or customize it.

+
+
+

Scaffold Your Application

+

Use the Loco CLI to scaffold your application:

+
cargo loco generate scaffold movie title:string
+

This creates models, controllers, and views.

+
+
+ +
+

Need More Help?

+

If you need further assistance, check out the Loco documentation or post on our discussions forum.

+
+
+
+ + + + diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index ae57066e4..03b7a83c5 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -30,6 +30,7 @@ pub fn test_config() -> Config { static_assets: None, secure_headers: None, remote_ip: None, + fallback: None, }, }, #[cfg(feature = "with-db")] diff --git a/starters/saas/config/development.yaml b/starters/saas/config/development.yaml index 415f0686b..b932c4aa7 100644 --- a/starters/saas/config/development.yaml +++ b/starters/saas/config/development.yaml @@ -22,6 +22,13 @@ server: host: http://localhost # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block middlewares: + # Fallback file for not found (404) routes + # disable this for production or use your own file + fallback: + enable: true + # use a file if you want a different 404 page + # file: path/to/file.html + # Enable Etag cache header middleware etag: enable: true @@ -74,7 +81,6 @@ server: # - POST # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds # max_age: 3600 - # ############################################# # Full stack SaaS asset serving @@ -123,7 +129,6 @@ server: # fallback: "frontend/dist/index.html" # (client-block-end) # - # Worker Configuration workers: