From c2d3341aa5b220c5e371bdcaa6c8eea29e0d7412 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Tue, 24 Dec 2024 23:01:20 +0000 Subject: [PATCH 1/5] support query param for delay --- crates/bsnext_core/src/optional_layers.rs | 18 +++++++++- crates/bsnext_core/tests/delays.rs | 21 ++++++++++++ crates/bsnext_html/src/lib.rs | 3 +- crates/bsnext_input/src/route_cli.rs | 40 ++++++++++++++++++++--- 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index 6c098f5..00ce918 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -1,4 +1,4 @@ -use axum::extract::{Request, State}; +use axum::extract::{Query, Request, State}; use axum::middleware::{map_response_with_state, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::MethodRouter; @@ -55,6 +55,7 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { }); let optional_stack = ServiceBuilder::new() + .layer(middleware::from_fn(delay_mw_dynamic)) .layer(option_layer(inject_layer)) .layer(option_layer(prevent_cache_headers_layer)) .layer(option_layer(set_response_headers_layer)) @@ -89,6 +90,21 @@ async fn delay_mw( } } +#[derive(Debug, serde::Deserialize)] +struct DynamicQueryParams { + /// Allow a request to have a ?bslive.delay.ms=200 style param to simulate a TTFB delay + #[serde(rename = "bslive.delay.ms")] + delay: Option, +} + +async fn delay_mw_dynamic(req: Request, next: Next) -> impl IntoResponse { + if let Ok(Query(DynamicQueryParams { delay: Some(ms) })) = Query::try_from_uri(req.uri()) { + sleep(Duration::from_millis(ms)).await; + } + let res = next.run(req).await; + Ok::<_, Infallible>(res) +} + async fn set_resp_headers_from_strs( State(header_map): State>, mut response: Response, diff --git a/crates/bsnext_core/tests/delays.rs b/crates/bsnext_core/tests/delays.rs index 3936e12..8f85b3d 100644 --- a/crates/bsnext_core/tests/delays.rs +++ b/crates/bsnext_core/tests/delays.rs @@ -32,6 +32,27 @@ async fn test_delays() -> Result<(), anyhow::Error> { Ok(()) } +#[tokio::test] +async fn test_delays_01() -> Result<(), anyhow::Error> { + let input = r#" +servers: + - name: first-byte-delays + routes: + - path: / + raw: hello world! + "#; + let state = from_yaml(&input)?; + + let (parts1, body1, dur1) = uri_to_res_parts(state.clone(), "/?bslive.delay.ms=200").await; + let dur1_ms = dur1.as_millis(); + + assert_eq!(body1, "hello world!"); + assert_eq!(parts1.status, 200); + assert!(dur1_ms > 200 && dur1_ms < 210); + + Ok(()) +} + #[tokio::test] async fn test_proxy_delay() -> Result<(), anyhow::Error> { let proxy_app = Router::new().route("/", get(|| async { "target - proxy delay" })); diff --git a/crates/bsnext_html/src/lib.rs b/crates/bsnext_html/src/lib.rs index 2bbff9b..8a96559 100644 --- a/crates/bsnext_html/src/lib.rs +++ b/crates/bsnext_html/src/lib.rs @@ -72,11 +72,12 @@ fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result Some(format!("{} {}", name, content)), - (Some(name), None) => Some(format!("{}", name)), + (Some(name), None) => Some(name.to_string()), _ => None, }; if let Some(joined) = joined { + tracing::trace!("Joined meta : {}", joined); if let Ok(route) = Route::from_cli_str(joined) { routes.push(route); node_ids_to_remove.push(meta_elem.id()); diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs index 462c3f1..196653a 100644 --- a/crates/bsnext_input/src/route_cli.rs +++ b/crates/bsnext_input/src/route_cli.rs @@ -1,5 +1,5 @@ use crate::path_def::PathDef; -use crate::route::{DirRoute, Route, RouteKind}; +use crate::route::{DelayKind, DelayOpts, DirRoute, Route, RouteKind}; use clap::Parser; use shell_words::split; @@ -22,11 +22,30 @@ impl TryInto for RouteCli { fn try_into(self) -> Result { Ok(match self.command { - SubCommands::ServeDir { dir, path } => Route { + SubCommands::ServeDir { + dir, + path, + delay: Some(0), + } => Route { path: PathDef::try_new(path)?, kind: RouteKind::Dir(DirRoute { dir, base: None }), ..std::default::Default::default() }, + SubCommands::ServeDir { + dir, + path, + delay: ms, + } => { + let mut route = Route { + path: PathDef::try_new(path)?, + kind: RouteKind::Dir(DirRoute { dir, base: None }), + ..std::default::Default::default() + }; + if let Some(ms) = ms { + route.opts.delay = Some(DelayOpts::Delay(DelayKind::Ms(ms))) + } + route + } }) } } @@ -36,11 +55,14 @@ pub enum SubCommands { /// does testing things ServeDir { /// Which path should this directory be served from - #[arg(short, long, default_value = "/")] + #[arg(long, default_value = "/")] path: String, /// Which directory should be served - #[arg(short, long, default_value = ".")] + #[arg(long, default_value = ".")] dir: String, + + #[arg(long)] + delay: Option, }, } @@ -60,4 +82,14 @@ mod test { // assert_debug_snapshot!(parsed); Ok(()) } + #[test] + fn test_serve_dir_delay() -> anyhow::Result<()> { + let input = "bslive serve-dir --path=/ --dir=examples/basic/public --delay=1000"; + let as_args = split(input)?; + let parsed = RouteCli::try_parse_from(as_args)?; + let as_route: Result = parsed.try_into(); + dbg!(&as_route); + // assert_debug_snapshot!(parsed); + Ok(()) + } } From 4cd17c3893da379594522db9b0a6c343e164d612 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 25 Dec 2024 19:33:28 +0000 Subject: [PATCH 2/5] testing --- crates/bsnext_input/src/route_cli.rs | 30 +++++++--------- ...ext_input__route_cli__test__serve_dir.snap | 29 ++++++++++++--- ...put__route_cli__test__serve_dir_delay.snap | 35 +++++++++++++++++++ 3 files changed, 72 insertions(+), 22 deletions(-) create mode 100644 crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap diff --git a/crates/bsnext_input/src/route_cli.rs b/crates/bsnext_input/src/route_cli.rs index 196653a..9fe00e8 100644 --- a/crates/bsnext_input/src/route_cli.rs +++ b/crates/bsnext_input/src/route_cli.rs @@ -1,5 +1,5 @@ use crate::path_def::PathDef; -use crate::route::{DelayKind, DelayOpts, DirRoute, Route, RouteKind}; +use crate::route::{DelayKind, DelayOpts, DirRoute, Opts, Route, RouteKind}; use clap::Parser; use shell_words::split; @@ -35,17 +35,15 @@ impl TryInto for RouteCli { dir, path, delay: ms, - } => { - let mut route = Route { - path: PathDef::try_new(path)?, - kind: RouteKind::Dir(DirRoute { dir, base: None }), + } => Route { + path: PathDef::try_new(path)?, + kind: RouteKind::Dir(DirRoute { dir, base: None }), + opts: Opts { + delay: ms.map(|ms| DelayOpts::Delay(DelayKind::Ms(ms))), ..std::default::Default::default() - }; - if let Some(ms) = ms { - route.opts.delay = Some(DelayOpts::Delay(DelayKind::Ms(ms))) - } - route - } + }, + ..std::default::Default::default() + }, }) } } @@ -77,9 +75,8 @@ mod test { let input = "bslive serve-dir --path=/ --dir=examples/basic/public"; let as_args = split(input)?; let parsed = RouteCli::try_parse_from(as_args)?; - let as_route: Result = parsed.try_into(); - dbg!(&as_route); - // assert_debug_snapshot!(parsed); + let as_route: Route = parsed.try_into().unwrap(); + insta::assert_debug_snapshot!(as_route); Ok(()) } #[test] @@ -87,9 +84,8 @@ mod test { let input = "bslive serve-dir --path=/ --dir=examples/basic/public --delay=1000"; let as_args = split(input)?; let parsed = RouteCli::try_parse_from(as_args)?; - let as_route: Result = parsed.try_into(); - dbg!(&as_route); - // assert_debug_snapshot!(parsed); + let as_route: Route = parsed.try_into().unwrap(); + insta::assert_debug_snapshot!(as_route); Ok(()) } } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap index 6e262f6..83ba872 100644 --- a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir.snap @@ -1,10 +1,29 @@ --- source: crates/bsnext_input/src/route_cli.rs -expression: parsed +expression: as_route --- -RouteCli { - command: ServeDir { - path: "/", - dir: "examples/basic/public", +Route { + path: PathDef { + inner: "/", }, + kind: Dir( + DirRoute { + dir: "examples/basic/public", + base: None, + }, + ), + opts: Opts { + cors: None, + delay: None, + watch: Bool( + true, + ), + inject: Bool( + true, + ), + headers: None, + cache: Prevent, + compression: None, + }, + fallback: None, } diff --git a/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap new file mode 100644 index 0000000..a0cf771 --- /dev/null +++ b/crates/bsnext_input/src/snapshots/bsnext_input__route_cli__test__serve_dir_delay.snap @@ -0,0 +1,35 @@ +--- +source: crates/bsnext_input/src/route_cli.rs +expression: as_route +--- +Route { + path: PathDef { + inner: "/", + }, + kind: Dir( + DirRoute { + dir: "examples/basic/public", + base: None, + }, + ), + opts: Opts { + cors: None, + delay: Some( + Delay( + Ms( + 1000, + ), + ), + ), + watch: Bool( + true, + ), + inject: Bool( + true, + ), + headers: None, + cache: Prevent, + compression: None, + }, + fallback: None, +} From f82a9248dbc05e6a9995032cec4f85486603834b Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 25 Dec 2024 19:36:59 +0000 Subject: [PATCH 3/5] naming --- .../bsnext_core/src/dynamic_query_params.rs | 21 +++++++++++++++++++ crates/bsnext_core/src/lib.rs | 1 + crates/bsnext_core/src/optional_layers.rs | 21 ++++--------------- 3 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 crates/bsnext_core/src/dynamic_query_params.rs diff --git a/crates/bsnext_core/src/dynamic_query_params.rs b/crates/bsnext_core/src/dynamic_query_params.rs new file mode 100644 index 0000000..e302783 --- /dev/null +++ b/crates/bsnext_core/src/dynamic_query_params.rs @@ -0,0 +1,21 @@ +use axum::extract::{Query, Request}; +use axum::middleware::Next; +use axum::response::IntoResponse; +use std::convert::Infallible; +use std::time::Duration; +use tokio::time::sleep; + +#[derive(Debug, serde::Deserialize)] +pub struct DynamicQueryParams { + /// Allow a request to have a ?bslive.delay.ms=200 style param to simulate a TTFB delay + #[serde(rename = "bslive.delay.ms")] + delay: Option, +} + +pub async fn dynamic_query_params_handler(req: Request, next: Next) -> impl IntoResponse { + if let Ok(Query(DynamicQueryParams { delay: Some(ms) })) = Query::try_from_uri(req.uri()) { + sleep(Duration::from_millis(ms)).await; + } + let res = next.run(req).await; + Ok::<_, Infallible>(res) +} diff --git a/crates/bsnext_core/src/lib.rs b/crates/bsnext_core/src/lib.rs index 73048fc..71f2ee0 100644 --- a/crates/bsnext_core/src/lib.rs +++ b/crates/bsnext_core/src/lib.rs @@ -2,6 +2,7 @@ pub mod server; pub mod servers_supervisor; pub mod dir_loader; +mod dynamic_query_params; pub mod export; pub mod handler_stack; pub mod handlers; diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index 00ce918..5b6e8a4 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -1,4 +1,5 @@ -use axum::extract::{Query, Request, State}; +use crate::dynamic_query_params; +use axum::extract::{Request, State}; use axum::middleware::{map_response_with_state, Next}; use axum::response::{IntoResponse, Response}; use axum::routing::MethodRouter; @@ -7,6 +8,7 @@ use axum_extra::middleware::option_layer; use bsnext_input::route::{CompType, CompressionOpts, CorsOpts, DelayKind, DelayOpts, Opts}; use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::{response_modifications_layer, InjectHandling}; +use dynamic_query_params::dynamic_query_params_handler; use http::header::{CACHE_CONTROL, EXPIRES, PRAGMA}; use http::{HeaderName, HeaderValue}; use std::collections::BTreeMap; @@ -55,7 +57,7 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { }); let optional_stack = ServiceBuilder::new() - .layer(middleware::from_fn(delay_mw_dynamic)) + .layer(middleware::from_fn(dynamic_query_params_handler)) .layer(option_layer(inject_layer)) .layer(option_layer(prevent_cache_headers_layer)) .layer(option_layer(set_response_headers_layer)) @@ -90,21 +92,6 @@ async fn delay_mw( } } -#[derive(Debug, serde::Deserialize)] -struct DynamicQueryParams { - /// Allow a request to have a ?bslive.delay.ms=200 style param to simulate a TTFB delay - #[serde(rename = "bslive.delay.ms")] - delay: Option, -} - -async fn delay_mw_dynamic(req: Request, next: Next) -> impl IntoResponse { - if let Ok(Query(DynamicQueryParams { delay: Some(ms) })) = Query::try_from_uri(req.uri()) { - sleep(Duration::from_millis(ms)).await; - } - let res = next.run(req).await; - Ok::<_, Infallible>(res) -} - async fn set_resp_headers_from_strs( State(header_map): State>, mut response: Response, From 0f35eacd69a4371e60535b9099e92a484676f523 Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 25 Dec 2024 19:42:15 +0000 Subject: [PATCH 4/5] added playwright test --- examples/basic/delays.yml | 2 ++ tests/delays.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/examples/basic/delays.yml b/examples/basic/delays.yml index e8197f8..e44b037 100644 --- a/examples/basic/delays.yml +++ b/examples/basic/delays.yml @@ -8,6 +8,8 @@ servers: html: first - 200ms delay delay: ms: 200 + - path: /none + html: no config-based delay - path: /500 html: second - 500ms delay delay: diff --git a/tests/delays.spec.ts b/tests/delays.spec.ts index 47dd682..217bb00 100644 --- a/tests/delays.spec.ts +++ b/tests/delays.spec.ts @@ -23,6 +23,19 @@ test.describe( expect(diff).toBeGreaterThan(200); expect(diff).toBeLessThan(300); }); + test("no config-based delay (url param)", async ({ request, bs }) => { + const start = Date.now(); + const response = await request.get( + bs.path("/none?bslive.delay.ms=200"), + ); + + const body = await response.body(); + const diff = Date.now() - start; + + expect(body.toString()).toBe(`no config-based delay`); + expect(diff).toBeGreaterThan(200); + expect(diff).toBeLessThan(300); + }); test("500ms delay", async ({ request, bs }) => { const start = Date.now(); const response = await request.get(bs.path("/500")); From 7a337d6b90235bb147b9c68e6dec5579df8f173d Mon Sep 17 00:00:00 2001 From: Shane Osbourne Date: Wed, 25 Dec 2024 21:35:12 +0000 Subject: [PATCH 5/5] docs --- .../bsnext_core/src/dynamic_query_params.rs | 50 ++++++- crates/bsnext_core/src/lib.rs | 2 +- crates/bsnext_core/src/optional_layers.rs | 17 +-- crates/bsnext_core/src/query-params.md | 140 ++++++++++++++++++ .../bsnext_core/src/server/router/common.rs | 13 ++ crates/bsnext_core/tests/cache.rs | 77 ++++++++++ crates/bsnext_core/tests/delays.rs | 2 +- crates/bsnext_resp/src/cache_opts.rs | 23 +++ 8 files changed, 304 insertions(+), 20 deletions(-) create mode 100644 crates/bsnext_core/src/query-params.md create mode 100644 crates/bsnext_core/tests/cache.rs diff --git a/crates/bsnext_core/src/dynamic_query_params.rs b/crates/bsnext_core/src/dynamic_query_params.rs index e302783..e39f009 100644 --- a/crates/bsnext_core/src/dynamic_query_params.rs +++ b/crates/bsnext_core/src/dynamic_query_params.rs @@ -1,21 +1,63 @@ use axum::extract::{Query, Request}; use axum::middleware::Next; use axum::response::IntoResponse; +use bsnext_resp::cache_opts::CacheOpts; use std::convert::Infallible; use std::time::Duration; use tokio::time::sleep; +#[doc = include_str!("./query-params.md")] #[derive(Debug, serde::Deserialize)] pub struct DynamicQueryParams { /// Allow a request to have a ?bslive.delay.ms=200 style param to simulate a TTFB delay #[serde(rename = "bslive.delay.ms")] - delay: Option, + pub delay: Option, + /// Control if Browsersync will add cache-busting headers, or not. + #[serde(rename = "bslive.cache")] + pub cache: Option, } pub async fn dynamic_query_params_handler(req: Request, next: Next) -> impl IntoResponse { - if let Ok(Query(DynamicQueryParams { delay: Some(ms) })) = Query::try_from_uri(req.uri()) { - sleep(Duration::from_millis(ms)).await; + let Ok(Query(query_params)) = Query::try_from_uri(req.uri()) else { + let res = next.run(req).await; + return Ok::<_, Infallible>(res); + }; + + // things to apply *before* + #[allow(clippy::single_match)] + match &query_params { + DynamicQueryParams { + delay: Some(ms), .. + } => { + sleep(Duration::from_millis(*ms)).await; + } + _ => {} } - let res = next.run(req).await; + + let mut res = next.run(req).await; + + // things to apply *after* + #[allow(clippy::single_match)] + match query_params { + DynamicQueryParams { + cache: Some(cache_opts), + .. + } => match cache_opts { + CacheOpts::Prevent => { + let headers_to_add = cache_opts.as_headers(); + for (name, value) in headers_to_add { + res.headers_mut().insert(name, value); + } + } + CacheOpts::Default => { + let headers = CacheOpts::Prevent.as_headers(); + for (name, _) in headers { + res.headers_mut().remove(name); + } + } + }, + _ => {} + } + Ok::<_, Infallible>(res) } diff --git a/crates/bsnext_core/src/lib.rs b/crates/bsnext_core/src/lib.rs index 71f2ee0..279beca 100644 --- a/crates/bsnext_core/src/lib.rs +++ b/crates/bsnext_core/src/lib.rs @@ -2,7 +2,7 @@ pub mod server; pub mod servers_supervisor; pub mod dir_loader; -mod dynamic_query_params; +pub mod dynamic_query_params; pub mod export; pub mod handler_stack; pub mod handlers; diff --git a/crates/bsnext_core/src/optional_layers.rs b/crates/bsnext_core/src/optional_layers.rs index 5b6e8a4..fb7daa9 100644 --- a/crates/bsnext_core/src/optional_layers.rs +++ b/crates/bsnext_core/src/optional_layers.rs @@ -6,10 +6,8 @@ use axum::routing::MethodRouter; use axum::{middleware, Extension}; use axum_extra::middleware::option_layer; use bsnext_input::route::{CompType, CompressionOpts, CorsOpts, DelayKind, DelayOpts, Opts}; -use bsnext_resp::cache_opts::CacheOpts; use bsnext_resp::{response_modifications_layer, InjectHandling}; use dynamic_query_params::dynamic_query_params_handler; -use http::header::{CACHE_CONTROL, EXPIRES, PRAGMA}; use http::{HeaderName, HeaderValue}; use std::collections::BTreeMap; use std::convert::Infallible; @@ -44,22 +42,13 @@ pub fn optional_layers(app: MethodRouter, opts: &Opts) -> MethodRouter { .as_ref() .map(|headers| map_response_with_state(headers.clone(), set_resp_headers_from_strs)); - let prevent_cache_headers_layer = matches!(opts.cache, CacheOpts::Prevent).then(|| { - let headers: Vec<(HeaderName, HeaderValue)> = vec![ - ( - CACHE_CONTROL, - HeaderValue::from_static("no-store, no-cache, must-revalidate"), - ), - (PRAGMA, HeaderValue::from_static("no-cache")), - (EXPIRES, HeaderValue::from_static("0")), - ]; - map_response_with_state(headers, set_resp_headers) - }); + let headers = opts.cache.as_headers(); + let prevent_cache_headers_layer = map_response_with_state(headers, set_resp_headers); let optional_stack = ServiceBuilder::new() .layer(middleware::from_fn(dynamic_query_params_handler)) .layer(option_layer(inject_layer)) - .layer(option_layer(prevent_cache_headers_layer)) + .layer(prevent_cache_headers_layer) .layer(option_layer(set_response_headers_layer)) .layer(option_layer(cors_enabled_layer)) .layer(option_layer(delay_enabled_layer)); diff --git a/crates/bsnext_core/src/query-params.md b/crates/bsnext_core/src/query-params.md new file mode 100644 index 0000000..c6bd90c --- /dev/null +++ b/crates/bsnext_core/src/query-params.md @@ -0,0 +1,140 @@ +# [DynamicQueryParams] + +Dynamically adjust how requests and responses will be handled on the fly, using +query params. + +**Features** + +- [delay](#delay-example) - simulate a delay in TTFB. +- [cache](#cache-example) - add or remove the headers that Browsersync to control cache + +--- + +## Delay + +You can control a simulated delay by appending the query param as seen below. + +- Note: Only milliseconds are supported right now. +- Note: If there's a typo, of if the value cannot be converted into a millisecond representation + no error will be thrown, it will simply be ignored. + +**When is this useful?** + +You can use it to optionally cause an asset to be delayed in its response + +### Delay example + +```rust +# use bsnext_core::server::router::common::from_yaml_blocking; +fn main() -> anyhow::Result<()> { + let req = "/abc?bslive.delay.ms=200"; + let server_yaml = r#" + servers: + - name: test + routes: + - path: /abc + html: hello world! + "#; + + let (parts, body, duration) = from_yaml_blocking(server_yaml, req)?; + let duration_millis = duration.as_millis(); + + assert_eq!(body, "hello world!"); + assert_eq!(parts.status, 200); + assert!(duration_millis > 200 && duration_millis < 210); + Ok(()) +} +``` + +### Delay CLI Example + +```bash +bslive examples/basic/public -p 3000 + +# then, in another terminal +curl localhost:3000?bslive.delay.ms=2000 +``` + +### Cache example + +The normal behaviour in Browsersync is to add the following HTTP headers to requests in development. + +- `cache-control: no-store, no-cache, must-revalidate` +- `pragma: no-cache` +- `expires: 0` + +Those indicate that the browser should re-fetch the assets frequently. If you want to override this behavior, you can +provide the query param seen below: + +- `?bslive.cache=default` <- this prevents Browsersync from adding any cache headers (defaulting to whatever the browser + decides) +- `?bslive.cache=prevent` <- this will cause the headers above to be added. + +```rust +# use bsnext_core::server::router::common::{from_yaml_blocking, header_pairs}; +fn main() -> anyhow::Result<()> { + let server_yaml = r#" + servers: + - name: test + routes: + - path: /abc + html: hello world! + "#; + + let (parts1, _, _) = from_yaml_blocking(server_yaml, "/abc?bslive.cache=default")?; + let pairs = header_pairs(&parts1); + + // Note: now the extra 3 headers are present + let expected = vec![ + ("content-type", "text/html; charset=utf-8"), + ("content-length", "12") + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, expected); + Ok(()) +} +``` + +## Cache example, overriding config + +At the route-level, you can remove the headers that Browsersync adds to bust caches, by simply putting +`cache: default` at any route-level config. + +Then, on a case-by-case basis you can re-enable it. + +```rust +# use bsnext_core::server::router::common::{from_yaml_blocking, header_pairs}; +fn main() -> anyhow::Result<()> { + // Note the `cache: default` here. This stops Browsersync adding any headers for cache-busting + let server_yaml = r#" + servers: + - name: test + cache: default + routes: + - path: /abc + html: hello world! + "#; + + // But, now we can re-enable cache-busting on a single URL + let (parts1, _, _) = from_yaml_blocking(server_yaml, "/abc?bslive.cache=prevent")?; + let pairs = header_pairs(&parts1); + + // Note: only the 2 headers are present now, otherwise there would be 5 + let expected = vec![ + ("content-type", "text/html; charset=utf-8"), + ("content-length", "12"), + ("cache-control", "no-store, no-cache, must-revalidate"), + ("pragma", "no-cache"), + ("expires", "0"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, expected); + Ok(()) +} +``` \ No newline at end of file diff --git a/crates/bsnext_core/src/server/router/common.rs b/crates/bsnext_core/src/server/router/common.rs index 71dc803..4cc2f63 100644 --- a/crates/bsnext_core/src/server/router/common.rs +++ b/crates/bsnext_core/src/server/router/common.rs @@ -94,6 +94,19 @@ pub fn from_yaml(yaml: &str) -> anyhow::Result { let state = into_state(config); Ok(state) } +pub fn from_yaml_blocking(yaml: &str, uri: &str) -> anyhow::Result<(Parts, String, Duration)> { + let state = from_yaml(yaml)?; + tokio::runtime::Runtime::new()? + .block_on(async { Ok(uri_to_res_parts(state.clone(), uri).await) }) +} + +pub fn header_pairs(parts: &Parts) -> Vec<(String, String)> { + parts + .headers + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string())) + .collect::>() +} pub struct TestProxy { pub socker_addr: SocketAddr, diff --git a/crates/bsnext_core/tests/cache.rs b/crates/bsnext_core/tests/cache.rs new file mode 100644 index 0000000..8b251d0 --- /dev/null +++ b/crates/bsnext_core/tests/cache.rs @@ -0,0 +1,77 @@ +use bsnext_core::server::router::common::{from_yaml_blocking, header_pairs}; + +#[test] +fn test_cache_query_param() -> Result<(), anyhow::Error> { + let input = r#" +servers: + - name: cache_defaults + routes: + - path: / + raw: hello world! + "#; + + let (parts1, _, _) = from_yaml_blocking(input, "/")?; + let pairs = header_pairs(&parts1); + + let control = vec![ + ("content-type", "text/plain"), + ("content-length", "12"), + ("cache-control", "no-store, no-cache, must-revalidate"), + ("pragma", "no-cache"), + ("expires", "0"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, control); + + let (parts1, _, _) = from_yaml_blocking(input, "/?bslive.cache=default")?; + let pairs = header_pairs(&parts1); + let expected = vec![("content-type", "text/plain"), ("content-length", "12")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, expected); + + Ok(()) +} +#[test] +fn test_cache_query_param_overrides_main() -> Result<(), anyhow::Error> { + let input = r#" +servers: + - name: cache_defaults + routes: + - path: /abc + raw: hello world! + cache: default + "#; + + let (parts1, _, _) = from_yaml_blocking(input, "/abc")?; + let pairs = header_pairs(&parts1); + + let control = vec![("content-type", "text/plain"), ("content-length", "12")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, control); + + let (parts1, _, _) = from_yaml_blocking(input, "/abc?bslive.cache=prevent")?; + let pairs = header_pairs(&parts1); + let expected = vec![ + ("content-type", "text/plain"), + ("content-length", "12"), + ("cache-control", "no-store, no-cache, must-revalidate"), + ("pragma", "no-cache"), + ("expires", "0"), + ] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect::>(); + + assert_eq!(pairs, expected); + + Ok(()) +} diff --git a/crates/bsnext_core/tests/delays.rs b/crates/bsnext_core/tests/delays.rs index 8f85b3d..cc80906 100644 --- a/crates/bsnext_core/tests/delays.rs +++ b/crates/bsnext_core/tests/delays.rs @@ -64,7 +64,7 @@ async fn test_proxy_delay() -> Result<(), anyhow::Error> { // update the proxy target to use local test proxy let mut config: ServerConfig = input.servers.first().expect("first").to_owned(); - let proxy_route = config.routes.get_mut(3).unwrap(); + let proxy_route = config.routes.get_mut(4).unwrap(); assert!( matches!(proxy_route.kind, RouteKind::Proxy(..)), "must be a proxy route, check delays.yml" diff --git a/crates/bsnext_resp/src/cache_opts.rs b/crates/bsnext_resp/src/cache_opts.rs index 1d247f6..c72f04e 100644 --- a/crates/bsnext_resp/src/cache_opts.rs +++ b/crates/bsnext_resp/src/cache_opts.rs @@ -1,3 +1,6 @@ +use http::header::{CACHE_CONTROL, EXPIRES, PRAGMA}; +use http::{HeaderName, HeaderValue}; + #[derive(Debug, Default, PartialEq, Hash, Clone, serde::Deserialize, serde::Serialize)] pub enum CacheOpts { /// Try to prevent browsers from caching the responses. This is the default behaviour @@ -8,3 +11,23 @@ pub enum CacheOpts { #[serde(rename = "default")] Default, } + +impl CacheOpts { + pub fn as_headers(&self) -> Vec<(HeaderName, HeaderValue)> { + match self { + CacheOpts::Prevent => { + vec![ + ( + CACHE_CONTROL, + HeaderValue::from_static("no-store, no-cache, must-revalidate"), + ), + (PRAGMA, HeaderValue::from_static("no-cache")), + (EXPIRES, HeaderValue::from_static("0")), + ] + } + CacheOpts::Default => { + vec![] + } + } + } +}