From 3cf5f99067504c1ef76ea1157e5e8d433951f632 Mon Sep 17 00:00:00 2001 From: Dotan Nahum Date: Wed, 4 Sep 2024 14:52:59 +0300 Subject: [PATCH 1/3] impl: fallback behavior --- src/config.rs | 17 ++++++++ src/controller/app_routes.rs | 47 +++++++++++++++++++-- src/controller/fallback.html | 59 +++++++++++++++++++++++++++ src/tests_cfg/config.rs | 1 + starters/saas/config/development.yaml | 21 +++++++--- 5 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 src/controller/fallback.html diff --git a/src/config.rs b/src/config.rs index d10d28bf..e465de2c 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 b3103ed5..ac8556f7 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(x) = &fallback.not_found { + let x = x.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, x) }) + } 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 00000000..610b6aee --- /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 ae57066e..03b7a83c 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 415f0686..13693e99 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: @@ -140,7 +145,7 @@ mailer: # Enable/Disable smtp mailer. enable: true # SMTP server host. e.x localhost, smtp.gmail.com - host: {{ get_env(name="MAILER_HOST", default="localhost") }} + host: { { get_env(name="MAILER_HOST", default="localhost") } } # SMTP server port port: 1025 # Use secure connection (SSL/TLS). @@ -159,7 +164,13 @@ mailer: # Database Configuration database: # Database connection URI - uri: {{ get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_app") }} + uri: + { + { + get_env(name="DATABASE_URL", + default="postgres://loco:loco@localhost:5432/loco_app"), + }, + } # When enabled, the sql query will be logged. enable_logging: false # Set the timeout duration when acquiring a connection. @@ -180,7 +191,7 @@ database: # Queue Configuration queue: # Redis connection URI - uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }} + uri: { { get_env(name="REDIS_URL", default="redis://127.0.0.1") } } # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode dangerously_flush: false From 27e4c1b820bc7702763d0cf14ce437ed98da1ad0 Mon Sep 17 00:00:00 2001 From: Dotan Nahum Date: Wed, 4 Sep 2024 15:06:36 +0300 Subject: [PATCH 2/3] fix: ci --- .github/workflows/e2e-cli-master.yaml | 2 +- docs-site/content/docs/the-app/controller.md | 37 +++++++++++++++++++ .../cmd/starters/generate-starters.trycmd | 3 ++ src/controller/app_routes.rs | 6 +-- starters/saas/config/development.yaml | 12 ++---- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/.github/workflows/e2e-cli-master.yaml b/.github/workflows/e2e-cli-master.yaml index 181cd5f9..693daf53 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 a292d172..a1898832 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 ebebe203..61c2ec58 100644 --- a/loco-cli/tests/cmd/starters/generate-starters.trycmd +++ b/loco-cli/tests/cmd/starters/generate-starters.trycmd @@ -4,6 +4,7 @@ $ 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 +13,7 @@ $ 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 +22,5 @@ $ 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/controller/app_routes.rs b/src/controller/app_routes.rs index ac8556f7..b1bb79a1 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -322,15 +322,15 @@ impl AppRoutes { ) -> Result> { let app = if let Some(path) = &fallback.file { app.fallback_service(ServeFile::new(path)) - } else if let Some(x) = &fallback.not_found { - let x = x.to_string(); + } 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, x) }) + app.fallback(move || async move { (code, not_found) }) } else { //app.fallback(handler) let code = fallback diff --git a/starters/saas/config/development.yaml b/starters/saas/config/development.yaml index 13693e99..b932c4aa 100644 --- a/starters/saas/config/development.yaml +++ b/starters/saas/config/development.yaml @@ -145,7 +145,7 @@ mailer: # Enable/Disable smtp mailer. enable: true # SMTP server host. e.x localhost, smtp.gmail.com - host: { { get_env(name="MAILER_HOST", default="localhost") } } + host: {{ get_env(name="MAILER_HOST", default="localhost") }} # SMTP server port port: 1025 # Use secure connection (SSL/TLS). @@ -164,13 +164,7 @@ mailer: # Database Configuration database: # Database connection URI - uri: - { - { - get_env(name="DATABASE_URL", - default="postgres://loco:loco@localhost:5432/loco_app"), - }, - } + uri: {{ get_env(name="DATABASE_URL", default="postgres://loco:loco@localhost:5432/loco_app") }} # When enabled, the sql query will be logged. enable_logging: false # Set the timeout duration when acquiring a connection. @@ -191,7 +185,7 @@ database: # Queue Configuration queue: # Redis connection URI - uri: { { get_env(name="REDIS_URL", default="redis://127.0.0.1") } } + uri: {{ get_env(name="REDIS_URL", default="redis://127.0.0.1") }} # Dangerously flush all data in Redis on startup. dangerous operation, make sure that you using this flag only on dev environments or test mode dangerously_flush: false From 2fd465b6332d361502dbb0f9560e9b8130b71d5c Mon Sep 17 00:00:00 2001 From: Dotan Nahum Date: Wed, 4 Sep 2024 15:09:22 +0300 Subject: [PATCH 3/3] fix: ci --- loco-cli/tests/cmd/starters/generate-starters.trycmd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/loco-cli/tests/cmd/starters/generate-starters.trycmd b/loco-cli/tests/cmd/starters/generate-starters.trycmd index 61c2ec58..40ade282 100644 --- a/loco-cli/tests/cmd/starters/generate-starters.trycmd +++ b/loco-cli/tests/cmd/starters/generate-starters.trycmd @@ -5,6 +5,7 @@ $ ALLOW_IN_GIT_REPO="" LOCO_APP_NAME="test_saas_template" LOCO_FOLDER_NAME=saas_ [CWD]/test_saas_template - database: You've selected `postgres` as your DB provider (you should have a postgres instance to connect to) + ``` ```console @@ -14,6 +15,7 @@ $ ALLOW_IN_GIT_REPO="true" LOCO_APP_NAME="test_rest_api_template" LOCO_FOLDER_NA [CWD]/test_rest_api_template + ``` ```console @@ -23,4 +25,5 @@ $ ALLOW_IN_GIT_REPO="true" LOCO_APP_NAME="test_lightweight_template" LOCO_FOLDER [CWD]/test_lightweight_template + ```