diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md index 277e59d..bdad5fe 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn-apps-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) @@ -197,10 +197,10 @@ impl HttpContext for HelloWorld { | Callback | Phase | Description | | ----------------------------------------------------------------- | ---------------- | --------------------------------------------------- | -| `on_http_request_headers(num_headers, end_of_stream) -> Action` | Request headers | Inspect or modify request headers before forwarding | -| `on_http_request_body(body_size, end_of_stream) -> Action` | Request body | Inspect or modify request body before forwarding | -| `on_http_response_headers(num_headers, end_of_stream) -> Action` | Response headers | Inspect or modify response headers from origin | -| `on_http_response_body(body_size, end_of_stream) -> Action` | Response body | Inspect or modify response body from origin | +| `on_http_request_headers(num_headers: usize, end_of_stream: bool) -> Action` | Request headers | Inspect or modify request headers before forwarding | +| `on_http_request_body(body_size: usize, end_of_stream: bool) -> Action` | Request body | Inspect or modify request body before forwarding | +| `on_http_response_headers(num_headers: usize, end_of_stream: bool) -> Action` | Response headers | Inspect or modify response headers from origin | +| `on_http_response_body(body_size: usize, end_of_stream: bool) -> Action` | Response body | Inspect or modify response body from origin | All callbacks have default no-op implementations. Override only the phases your app needs to process. diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md index 0205b81..a8e55fb 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-ab-testing-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -248,191 +248,3 @@ No additional dependencies. Uses `std::env` and `std::time::UNIX_EPOCH` from the - FastEdge environment variable configuration (setting `EXPERIMENT_NAME`, `VARIANT_A_PATH`, `VARIANT_B_PATH` at deploy time) - examples-geoblock-rust reference (similar CDN proxy-wasm filter structure) - examples-auth-jwt-rust reference (CDN Rust example with cross-hook state pattern) - -## Source Material - -### FILE: examples/cdn/ab_testing/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating A/B traffic splitting at the CDN layer. - -Uses a cookie to assign users to variant A or B, then rewrites the -request path to route to different parts of the origin server. - -Required configuration: - - Environment variable: EXPERIMENT_NAME - - Environment variable: VARIANT_A_PATH (path prefix for variant A) - - Environment variable: VARIANT_B_PATH (path prefix for variant B) -*/ - -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::env; -use std::time::UNIX_EPOCH; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(AbTestingRoot) }); -}} - -struct AbTestingRoot; - -impl Context for AbTestingRoot {} - -impl RootContext for AbTestingRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(AbTestingContext)) - } -} - -struct AbTestingContext; - -impl Context for AbTestingContext {} - -impl HttpContext for AbTestingContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Ok(experiment_name) = env::var("EXPERIMENT_NAME") else { - self.send_http_response( - 500, - vec![], - Some(b"App misconfigured - EXPERIMENT_NAME must be set"), - ); - return Action::Pause; - }; - - let Ok(variant_a_path) = env::var("VARIANT_A_PATH") else { - self.send_http_response( - 500, - vec![], - Some(b"App misconfigured - VARIANT_A_PATH must be set"), - ); - return Action::Pause; - }; - - let Ok(variant_b_path) = env::var("VARIANT_B_PATH") else { - self.send_http_response( - 500, - vec![], - Some(b"App misconfigured - VARIANT_B_PATH must be set"), - ); - return Action::Pause; - }; - - let cookie_name = format!("fe_exp_{}", experiment_name); - - // Check for existing experiment cookie - let cookie_header = self - .get_http_request_header("Cookie") - .unwrap_or_default(); - let mut assigned = get_cookie_value(&cookie_header, &cookie_name); - - // Assign variant if not already set - if assigned != "A" && assigned != "B" { - let now = self - .get_current_time() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - assigned = if now % 2 == 0 { "A" } else { "B" }.to_string(); - } - - // Rewrite request path - let path = self - .get_property(vec!["request.path"]) - .and_then(|bytes| String::from_utf8(bytes).ok()) - .unwrap_or_else(|| "/".to_string()); - - let variant_path = if assigned == "A" { - &variant_a_path - } else { - &variant_b_path - }; - let new_path = format!("{}{}", variant_path, path); - - // Update the request path directly to avoid ambiguous URL rewriting. - self.set_property(vec!["request.path"], Some(new_path.as_bytes())); - - // Add variant headers for upstream visibility - self.add_http_request_header("X-Experiment", &experiment_name); - self.add_http_request_header("X-Variant", &assigned); - - println!( - "A/B test \"{}\": variant {}, path {}", - experiment_name, assigned, new_path - ); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Recover the assigned variant and experiment name from the request headers set in - // on_http_request_headers. Instance state does not survive the nginx -> core-proxy hop. - let Some(variant) = self.get_http_request_header("X-Variant") else { - return Action::Continue; - }; - let Some(experiment_name) = self.get_http_request_header("X-Experiment") else { - return Action::Continue; - }; - - let cookie = format!( - "fe_exp_{}={}; Path=/; Max-Age=86400; SameSite=Lax", - experiment_name, variant - ); - self.add_http_response_header("Set-Cookie", &cookie); - self.add_http_response_header("X-Variant", &variant); - - Action::Continue - } -} - -fn get_cookie_value(cookie_header: &str, name: &str) -> String { - if cookie_header.is_empty() { - return String::new(); - } - let prefix = format!("{}=", name); - for pair in cookie_header.split(';') { - let pair = pair.trim(); - if let Some(value) = pair.strip_prefix(&prefix) { - return value.to_string(); - } - } - String::new() -} -``` - - -### FILE: examples/cdn/ab_testing/Cargo.toml - -```toml -[workspace] - -[package] -name = "ab_testing" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -``` - - -### FILE: examples/cdn/ab_testing/README.md - -``` -[← Back to examples](../../README.md) - -# A/B Testing (CDN) - -Cookie-based A/B traffic splitting at the CDN layer, routing requests to different origin paths based on variant assignment. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md index 1de9cd9..5aeb1a4 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # API Key Validation — CDN (Rust) @@ -159,3 +159,120 @@ return Action::Pause; - Host services reference — secrets API (`fastedge::proxywasm::secret`) and other CDN host services - CDN apps reference — proxy-wasm app structure, `RootContext`/`HttpContext` setup, `Action` enum, and request property encodings - SDK API reference — Rust CDN SDK traits and types + +## Source Material + +### FILE: examples/cdn/api_key/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating API key validation. + +Validates requests using an X-API-Key header checked against a stored +secret. Simpler alternative to JWT when token expiry and claims are +not needed. + +Required configuration: + - Secret: API_KEY +*/ + +use fastedge::proxywasm::secret; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(ApiKeyRoot) }); +}} + +struct ApiKeyRoot; + +impl Context for ApiKeyRoot {} + +impl RootContext for ApiKeyRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(ApiKeyContext)) + } +} + +struct ApiKeyContext; + +impl Context for ApiKeyContext {} + +impl HttpContext for ApiKeyContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let expected_key = match secret::get("API_KEY") { + Ok(Some(bytes)) => match String::from_utf8(bytes) { + Ok(s) if !s.is_empty() => s, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }, + _ => { + self.send_http_response(500, vec![], Some(b"App misconfigured")); + return Action::Pause; + } + }; + + let provided_key = match self.get_http_request_header("X-API-Key") { + Some(k) if !k.is_empty() => k, + _ => { + self.send_http_response( + 401, + vec![("WWW-Authenticate", "API-Key")], + Some(b"Missing X-API-Key header"), + ); + return Action::Pause; + } + }; + + if provided_key != expected_key { + println!("API key validation failed"); + self.send_http_response(403, vec![], Some(b"Invalid API key")); + return Action::Pause; + } + + // Strip the API key header before forwarding to upstream + self.set_http_request_header("X-API-Key", None); + + println!("API key validated successfully"); + Action::Continue + } +} +``` + +### FILE: examples/cdn/api_key/Cargo.toml + +```toml +[workspace] + +[package] +name = "api_key" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + +### FILE: examples/cdn/api_key/README.md + +``` +[← Back to examples](../../README.md) + +# API Key (CDN) + +Validates requests using an `X-API-Key` header checked against a stored secret. Returns 401 if missing, 403 if invalid, and strips the header before forwarding to upstream. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md index 5d68f6c..047c4eb 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-auth-jwt-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md index e927177..78eaab7 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-body-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md index 7b9f6ae..e0bb9aa 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cache-control-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md index baedbd0..cf31db4 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # CDN Example: Convert Image (Rust) @@ -202,3 +202,300 @@ Transformation is skipped (returning `Action::Continue` without modifying the re - FastEdge CDN app platform overview - FastEdge SDK Rust reference - FastEdge error codes reference + +## Source Material + +### FILE: examples/cdn/convert_image/src/lib.rs + +```rust +use image::*; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::{env, env::VarError, io::Cursor, str::from_utf8}; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); +}} + +struct ConvertImageRoot; + +impl Context for ConvertImageRoot {} + +impl RootContext for ConvertImageRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(ConvertImageContext)) + } +} + +struct ConvertImageContext; + +impl Context for ConvertImageContext {} + +impl HttpContext for ConvertImageContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + // this header is used to select correct image version from cache + self.add_http_request_header("Image-Format", "original"); + + // get extension + let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); + println!("request.path={path:?}"); + let raw_ext = self.get_property(vec!["request.extension"]); + println!("request.extension={raw_ext:?}"); + let Some(ext) = raw_ext else { + println!("No extension in request path, not transforming"); + return Action::Continue; + }; + let Ok(ext) = from_utf8(&ext) else { + println!("Invalid UTF-8 in request extension, not transforming"); + return Action::Continue; + }; + if ext.is_empty() { + println!("No extension in request path, not transforming"); + return Action::Continue; + } + + // FORMATS_TO_TRANSFORM contains list of file extensions to transfor + // note that jpg and jpeg are different extensions + let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { + println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); + return Action::Continue; + }; + if !image_list.split(',').any(|entry| entry == ext) { + println!( + "extension {} is not in the list of formats to transform: {}, not transforming", + ext, image_list + ); + return Action::Continue; + } + + // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed + let Some(ua) = self.get_http_request_header("User-Agent") else { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + }; + if ua.is_empty() { + println!("User-Agent header is not set, not transforming"); + return Action::Continue; + } + if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { + if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { + println!("User-Agent is in ignore list, not transforming"); + return Action::Continue; + } + } + + // indicator for on_response_headers and for cache key + self.set_http_request_header("Image-Format", Some("image/avif")); + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // only process 200 responses + if let Some(status) = self.rsp_status() { + if status != 200 { + println!( + "Response status is {} instead of expected 200, not transforming", + status + ); + return Action::Continue; + } + } else { + println!("Response status is not set, not transforming"); + return Action::Continue; + } + + // if "Image-Format" request header is not set, don't convert the image + let Some(content_type) = self.get_http_request_header("Image-Format") else { + return Action::Continue; + }; + // instruct cache to vary by this header so "original" and "image/avif" are cached separately + self.add_http_response_header("Vary", "Image-Format"); + + if content_type == "original" { + return Action::Continue; + }; + + // image to be transformed, set headers accordingly + self.set_http_response_header("Content-Length", None); + self.set_http_response_header("Transfer-Encoding", Some("Chunked")); + self.set_http_response_header("Content-Type", Some(content_type.as_str())); + + // indicate to on_http_response_body that transformation is needed + self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); + + Action::Continue + } + + fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { + if !end_of_stream { + // wait till we get complete body + return Action::Pause; + } + + let Some(content_type) = self.get_property(vec!["response.content-type"]) else { + return Action::Continue; + }; + + let Ok(content_type) = from_utf8(&content_type) else { + // should never happen + println!("Invalid UTF-8 in Content-Type"); + self.send_http_response(500, vec![], None); + return Action::Pause; + }; + + if content_type != "image/avif" { + // should never happen + println!( + "Content-Type {} is not supported, not transforming", + content_type + ); + return Action::Continue; + } + + if let Some(body_bytes) = self.get_http_response_body(0, body_size) { + let buf = body_bytes.as_bytes(); + let img = match load_from_memory(buf) { + Ok(i) => i, + Err(e) => { + println!("cannot load image to memory {}, not converting", e); + return Action::Continue; + } + }; + + let mut out = Vec::new(); + let mut c = Cursor::new(&mut out); + let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( + &mut c, + u8_param("AVIF_SPEED", 1, 10, 5), + u8_param("AVIF_QUALITY", 1, 100, 70), + )); + + match res { + Ok(_) => { + println!( + "{} bytes -> {} bytes {}", + body_size, + out.len(), + content_type + ); + self.set_http_response_body(0, body_size, &out) + } + Err(e) => println!("cannot store transformed image {}", e), + } + } else { + println!("No response body to transform"); + } + + Action::Continue + } +} + +impl ConvertImageContext { + fn rsp_status(&mut self) -> Option { + if let Some(status) = self.get_property(vec!["response.status"]) { + if status.len() != 2 { + println!("HTTP status property is not 2 bytes"); + return None; + } + return Some(u16::from_be_bytes([status[0], status[1]])); + } + None + } +} + +fn str_param(name: &str) -> Result { + let val = env::var(name)?; + if val.is_empty() { + return Err(VarError::NotPresent); + } + + Ok(val) +} + +fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { + let Ok(val) = env::var(name) else { + println!("Param {} is not set, using default value {}", name, default); + return default; + }; + if val.is_empty() { + println!("Param {} is not set, using default value {}", name, default); + return default; + } + + let val = match val.parse() { + Err(_) => { + println!( + "Param {} is not a valid number, using default value {}", + name, default + ); + return default; + } + Ok(v) => v, + }; + if val < min { + println!( + "Param {} is below minimum {}, using default value {}", + name, min, default + ); + return default; + } + if val > max { + println!( + "Param {} is above maximum {}, using default value {}", + name, max, default + ); + return default; + } + + val +} +``` + + +### FILE: examples/cdn/convert_image/Cargo.toml + +```toml +[workspace] + +[package] +name = "convert_image" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +image = "0.25" +``` + + +### FILE: examples/cdn/convert_image/README.md + +``` +[← Back to examples](../../README.md) + +# Convert Image (CDN) + +Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. + +## Configuration + +- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) +- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip +- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) +- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) + +## How it works + +1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation +2. **on_response_headers** — sets response headers for AVIF content type on 200 responses +3. **on_response_body** — decodes the original image and re-encodes it as AVIF +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md index 6da1290..0412a56 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-cors-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md index 90a1205..ee84089 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> ## Custom Error Pages — CDN (Rust) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md index ec7dc83..d049948 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -250,123 +250,3 @@ self.send_http_response(status_code: u32, headers: Vec<(&str, &str)>, body: Opti - CDN app pattern guide (see the _docs-pattern-cdn reference) - Host services reference for Rust (see the host-services-rust reference) - Platform overview (see the platform-overview reference) - -## Source Material - -### FILE: examples/cdn/custom/src/lib.rs - -```rust -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::time::Duration; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); -}} - -const BAD_REQUEST: u32 = 400; - -struct HttpHeadersRoot; - -impl Context for HttpHeadersRoot {} - -impl RootContext for HttpHeadersRoot { - fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(HttpHeaders)) - } - - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } -} - -struct HttpHeaders; - -impl Context for HttpHeaders {} - -impl HttpContext for HttpHeaders { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Some(path) = self.get_property(vec!["request.path"]) else { - self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); - return Action::Pause; - }; - - let Ok(path) = std::str::from_utf8(&path) else { - self.send_http_response( - BAD_REQUEST, - vec![], - Some(b"Malformed request - not utf8 string"), - ); - return Action::Pause; - }; - - //trim first '/' - let path = if path.starts_with('/') { - &path[1..] - } else { - path - }; - let mut segments = path.split('/'); - - let Some(status_code) = segments.next() else { - return Action::Continue; - }; - - if let Some(delay) = segments.next() { - if let Ok(delay) = delay.parse::() { - std::thread::sleep(Duration::from_millis(delay)); - } - } - - let Ok(status_code) = status_code.parse::() else { - self.send_http_response( - BAD_REQUEST, - vec![], - Some(b"Malformed request - invalid status code"), - ); - return Action::Pause; - }; - - match status_code { - 0 | 200 => Action::Continue, - code if code < 600 => { - self.send_http_response(code, vec![], None); - Action::Pause - } - _ => { - self.send_http_response(BAD_REQUEST, vec![], None); - Action::Pause - } - } - } -} -``` - -### FILE: examples/cdn/custom/Cargo.toml - -```toml -[workspace] - -[package] -name = "custom" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` - -### FILE: examples/cdn/custom/README.md - -``` -[← Back to examples](../../README.md) - -# Custom (CDN) - -Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-rust.md index 0d8d824..0239461 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-env-secrets-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- type: example diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md index fb251dd..2d22742 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geo-redirect-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md index 8c66081..bc7fabe 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-geoblock-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md index 97aea43..bbfc088 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -230,356 +230,3 @@ All error paths return `Action::Pause`. - examples-body-cdn-rust (body manipulation API) - examples-shared-data-cdn-rust (shared data API) - host-services-rust reference (full ABI surface) - -## Source Material - -### FILE: examples/cdn/headers/src/lib.rs - -```rust -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::collections::HashSet; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); -}} - -struct HttpHeadersRoot; - -impl Context for HttpHeadersRoot {} - -impl RootContext for HttpHeadersRoot { - fn create_http_context(&self, context_id: u32) -> Option> { - Some(Box::new(HttpHeaders { context_id })) - } - - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } -} - -struct HttpHeaders { - context_id: u32, -} - -impl Context for HttpHeaders {} - -impl HttpContext for HttpHeaders { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let mut original_headers = HashSet::new(); - let mut original_headers_bytes = HashSet::new(); - - // iterate over the headers and print them - for (name, value) in self.get_http_request_headers().into_iter() { - println!("#{} -> {}: {}", self.context_id, name, value); - original_headers.insert((name, value)); - } - for (name, value) in self.get_http_request_headers_bytes().into_iter() { - println!("#{} -> {}: {:?}", self.context_id, name, value); - original_headers_bytes.insert((name, value)); - } - if original_headers.is_empty() || original_headers_bytes.is_empty() { - self.send_http_response(550, vec![], None); - return Action::Pause; - } - - // check if the host header is present - if self.get_http_request_header("host").is_none() { - self.send_http_response(551, vec![], None); - return Action::Pause; - } - if self.get_http_request_header_bytes("host").is_none() { - self.send_http_response(551, vec![], None); - return Action::Pause; - } - - // add new headers - self.add_http_request_header("new-header-01", "value-01"); - self.add_http_request_header_bytes("new-header-bytes-01", b"value-bytes-01"); - - self.add_http_request_header("new-header-02", "value-02"); - self.add_http_request_header_bytes("new-header-bytes-02", b"value-bytes-02"); - - self.add_http_request_header("new-header-03", "value-03"); - self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03"); - - //remove header new-headter-01, expected empty value - self.set_http_request_header("new-header-01", None); - self.set_http_request_header_bytes("new-header-bytes-01", None); - - // changing header value - self.set_http_request_header("new-header-02", Some("new-value-02")); - self.set_http_request_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); - - // add new header with existing name - self.add_http_request_header("new-header-03", "value-03-a"); - self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); - - // try to set/add response headers - self.add_http_response_header("new-response-header", "value-01"); - self.set_http_response_header("cache-control", None); - self.set_http_response_header("new-response-header", Some("value-02")); - - // get new headers - let headers = self - .get_http_request_headers() - .into_iter() - .collect::>(); - let headers_bytes = self - .get_http_request_headers_bytes() - .into_iter() - .collect::>(); - - let expected = [ - ("new-header-01".to_string(), "".to_string()), - ("new-header-bytes-01".to_string(), "".to_string()), - ("new-header-02".to_string(), "new-value-02".to_string()), - ( - "new-header-bytes-02".to_string(), - "new-value-bytes-02".to_string(), - ), - ("new-header-03".to_string(), "value-03".to_string()), - ( - "new-header-bytes-03".to_string(), - "value-bytes-03".to_string(), - ), - ("new-header-03".to_string(), "value-03-a".to_string()), - ( - "new-header-bytes-03".to_string(), - "value-bytes-03-a".to_string(), - ), - ]; - - let expected = expected.iter().collect::>(); - - let expected_bytes = [ - ("new-header-01".to_string(), b"".to_vec()), - ("new-header-bytes-01".to_string(), b"".to_vec()), - ("new-header-02".to_string(), b"new-value-02".to_vec()), - ( - "new-header-bytes-02".to_string(), - b"new-value-bytes-02".to_vec(), - ), - ("new-header-03".to_string(), b"value-03".to_vec()), - ( - "new-header-bytes-03".to_string(), - b"value-bytes-03".to_vec(), - ), - ("new-header-03".to_string(), b"value-03-a".to_vec()), - ( - "new-header-bytes-03".to_string(), - b"value-bytes-03-a".to_vec(), - ), - ]; - - let expected_bytes = expected_bytes.iter().collect::>(); - - let diff = headers - .difference(&original_headers) - .collect::>(); - - let diff_bytes = headers_bytes - .difference(&original_headers_bytes) - .collect::>(); - - let diff = diff.difference(&expected).collect::>(); - - if !diff.is_empty() { - println!("different headers: {:?}", diff); - self.send_http_response(552, vec![], None); - return Action::Pause; - } - - let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); - if !diff_bytes.is_empty() { - println!("different headers bytes: {:?}", diff_bytes); - self.send_http_response(552, vec![], None); - return Action::Pause; - } - - // check if the response header is not returned - if self.get_http_response_header("host").is_some() { - self.send_http_response(553, vec![], None); - return Action::Pause; - }; - if self.get_http_response_header_bytes("host").is_some() { - self.send_http_response(553, vec![], None); - return Action::Pause; - }; - - let response_headers = self.get_http_response_headers(); - if response_headers.len() != 1 { - self.send_http_response(555, vec![], None); - return Action::Pause; - } - let Some((name, value)) = response_headers.into_iter().next() else { - self.send_http_response(555, vec![], None); - return Action::Pause; - }; - if name != "new-response-header" || value != "value-02" { - self.send_http_response(556, vec![], None); - return Action::Pause; - } - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - let mut original_headers = HashSet::new(); - let mut original_headers_bytes = HashSet::new(); - - // iterate over the headers and print them - for (name, value) in self.get_http_response_headers().into_iter() { - println!("#{} -> {}: {}", self.context_id, name, value); - original_headers.insert((name, value)); - } - for (name, value) in self.get_http_response_headers_bytes().into_iter() { - println!("#{} -> {}: {:?}", self.context_id, name, value); - original_headers_bytes.insert((name, value)); - } - if original_headers.is_empty() || original_headers_bytes.is_empty() { - self.send_http_response(550, vec![], None); - return Action::Pause; - } - - // check if the host header is present - if self.get_http_response_header("host").is_none() { - self.send_http_response(551, vec![], None); - return Action::Pause; - } - if self.get_http_response_header_bytes("host").is_none() { - self.send_http_response(551, vec![], None); - return Action::Pause; - } - - // add new headers - self.add_http_response_header("new-header-01", "value-01"); - self.add_http_response_header_bytes("new-header-bytes-01", b"value-bytes-01"); - - self.add_http_response_header("new-header-02", "value-02"); - self.add_http_response_header_bytes("new-header-bytes-02", b"value-bytes-02"); - - self.add_http_response_header("new-header-03", "value-03"); - self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03"); - - //remove header new-headter-01, expected empty value - self.set_http_response_header("new-header-01", None); - self.set_http_response_header_bytes("new-header-bytes-01", None); - - // changing header value - self.set_http_response_header("new-header-02", Some("new-value-02")); - self.set_http_response_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); - - // add new header with existing name - self.add_http_response_header("new-header-03", "value-03-a"); - self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); - - // get new headers - let headers = self - .get_http_response_headers() - .into_iter() - .collect::>(); - let headers_bytes = self - .get_http_response_headers_bytes() - .into_iter() - .collect::>(); - - let expected = [ - ("new-header-01".to_string(), "".to_string()), - ("new-header-bytes-01".to_string(), "".to_string()), - ("new-header-02".to_string(), "new-value-02".to_string()), - ( - "new-header-bytes-02".to_string(), - "new-value-bytes-02".to_string(), - ), - ("new-header-03".to_string(), "value-03".to_string()), - ( - "new-header-bytes-03".to_string(), - "value-bytes-03".to_string(), - ), - ("new-header-03".to_string(), "value-03-a".to_string()), - ( - "new-header-bytes-03".to_string(), - "value-bytes-03-a".to_string(), - ), - ]; - - let expected = expected.iter().collect::>(); - - let expected_bytes = [ - ("new-header-01".to_string(), b"".to_vec()), - ("new-header-bytes-01".to_string(), b"".to_vec()), - ("new-header-02".to_string(), b"new-value-02".to_vec()), - ( - "new-header-bytes-02".to_string(), - b"new-value-bytes-02".to_vec(), - ), - ("new-header-03".to_string(), b"value-03".to_vec()), - ( - "new-header-bytes-03".to_string(), - b"value-bytes-03".to_vec(), - ), - ("new-header-03".to_string(), b"value-03-a".to_vec()), - ( - "new-header-bytes-03".to_string(), - b"value-bytes-03-a".to_vec(), - ), - ]; - - let expected_bytes = expected_bytes.iter().collect::>(); - - let diff = headers - .difference(&original_headers) - .collect::>(); - - let diff_bytes = headers_bytes - .difference(&original_headers_bytes) - .collect::>(); - - let diff = diff.difference(&expected).collect::>(); - - if !diff.is_empty() { - println!("different headers: {:?}", diff); - self.send_http_response(552, vec![], None); - return Action::Pause; - } - - let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); - if !diff_bytes.is_empty() { - println!("different headers bytes: {:?}", diff_bytes); - self.send_http_response(552, vec![], None); - return Action::Pause; - } - - Action::Continue - } -} -``` - -### FILE: examples/cdn/headers/Cargo.toml - -```toml -[workspace] - -[package] -name = "headers" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -``` - -### FILE: examples/cdn/headers/README.md - -``` -[← Back to examples](../../README.md) - -# Headers (CDN) - -Validates and manipulates HTTP request and response headers using the proxy-wasm ABI. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md index 3959d1b..35d0930 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-http-call-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # HTTP Call — CDN (Rust) @@ -365,6 +365,7 @@ fn to_status_code(status: Status) -> u32 { - The callback `on_http_call_response` is defined on the `Context` trait, not `HttpContext` — implement it on the per-request struct, not the root context. - `num_headers == 0` is the only reliable signal for call failure; do not rely on `body_size` or `token_id` for failure detection. - `:authority` in headers must match the `upstream` argument passed to `dispatch_http_call`. +- The callback may fire before or after response hooks depending on runtime timing — state tracking via struct fields is the correct synchronization mechanism. --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md index 4fe50fc..56aad05 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-kv-store-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -165,7 +165,7 @@ let result = match action { "zrange" => self.handle_zrange(&store, ¶ms), "zscan" => self.handle_zscan(&store, ¶ms), "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!("Invalid action '{}'", action)), + _ => Err(format!("Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", action)), }; ``` @@ -240,276 +240,3 @@ Sorted set entries shape: `[{"value": "", "score": }, ...]` - Host services reference — full KV store, secrets, and dictionary API documentation for CDN (proxy-wasm) apps - CDN apps reference — proxy-wasm lifecycle hooks, request properties, header and body manipulation patterns - SDK reference (Rust) — `fastedge` crate modules, feature flags, and component model vs. proxy-wasm differences - -## Source Material - -### FILE: examples/cdn/key_value/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating KV Store operations via the proxy-wasm interface. - -Supports all KV Store operations via query parameters: - ?store=&action=get&key= - ?store=&action=scan&match= - ?store=&action=zrange&key=&min=&max= - ?store=&action=zscan&key=&match= - ?store=&action=bfExists&key=&item= - -Defaults to action=get if not specified. -*/ - -use fastedge::proxywasm::key_value::Store; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde_json::json; -use std::collections::HashMap; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(KvStoreRoot) }); -}} - -struct KvStoreRoot; - -impl Context for KvStoreRoot {} - -impl RootContext for KvStoreRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(KvStoreContext)) - } -} - -struct KvStoreContext; - -impl Context for KvStoreContext {} - -impl HttpContext for KvStoreContext { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // Remove content-length since we replace the body - self.set_http_response_header("content-length", None); - self.set_http_response_header("content-type", Some("application/json")); - self.set_http_response_header("transfer-encoding", Some("chunked")); - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - return Action::Pause; - } - - let query = self - .get_property(vec!["request", "query"]) - .and_then(|bytes| String::from_utf8(bytes).ok()) - .unwrap_or_default(); - - if query.is_empty() { - self.send_error("App must be called with query parameters", body_size); - return Action::Continue; - } - - let params: HashMap<&str, &str> = querystring::querify(&query).into_iter().collect(); - - let Some(store_name) = params.get("store") else { - self.send_error("Missing required param 'store'", body_size); - return Action::Continue; - }; - - let action = params.get("action").copied().unwrap_or("get"); - - let store = match Store::open(store_name) { - Ok(s) => s, - Err(e) => { - self.send_error(&format!("Failed to open KvStore '{}': {}", store_name, e), body_size); - return Action::Continue; - } - }; - - let result = match action { - "get" => self.handle_get(&store, ¶ms), - "scan" => self.handle_scan(&store, ¶ms), - "zrange" => self.handle_zrange(&store, ¶ms), - "zscan" => self.handle_zscan(&store, ¶ms), - "bfExists" => self.handle_bf_exists(&store, ¶ms), - _ => Err(format!( - "Invalid action '{}'. Supported: get, scan, zrange, zscan, bfExists", - action - )), - }; - - let body = match result { - Ok(json) => json, - Err(msg) => { - self.send_error(&msg, body_size); - return Action::Continue; - } - }; - - self.set_http_response_body(0, body_size, body.as_bytes()); - - Action::Continue - } -} - -impl KvStoreContext { - fn handle_get(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'get' action")?; - match store.get(key) { - Ok(Some(value)) => { - let value_str = String::from_utf8_lossy(&value); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "get", - "key": key, - "response": value_str.as_ref() - }).to_string()) - } - Ok(None) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "get", - "key": key, - "response": null - }).to_string()), - Err(e) => Err(format!("KV get error: {}", e)), - } - } - - fn handle_scan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'scan' action")?; - match store.scan(pattern) { - Ok(keys) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "scan", - "match": pattern, - "response": keys - }).to_string()), - Err(e) => Err(format!("KV scan error: {}", e)), - } - } - - fn handle_zrange(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zrange' action")?; - let min: f64 = params - .get("min") - .ok_or("Missing required param 'min' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'min' value: must be a number".to_string())?; - let max: f64 = params - .get("max") - .ok_or("Missing required param 'max' for 'zrange' action")? - .parse() - .map_err(|_| "Invalid 'max' value: must be a number".to_string())?; - - match store.zrange_by_score(key, min, max) { - Ok(entries) => { - let entries_json: Vec = entries - .iter() - .map(|(value, score)| { - let value_str = String::from_utf8_lossy(value); - json!({"value": value_str.as_ref(), "score": score}) - }) - .collect(); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "zrange", - "key": key, - "min": min, - "max": max, - "response": entries_json - }).to_string()) - } - Err(e) => Err(format!("KV zrange error: {}", e)), - } - } - - fn handle_zscan(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'zscan' action")?; - let pattern = *params.get("match").ok_or("Missing required param 'match' for 'zscan' action")?; - - match store.zscan(key, pattern) { - Ok(entries) => { - let entries_json: Vec = entries - .iter() - .map(|(value, score)| { - let value_str = String::from_utf8_lossy(value); - json!({"value": value_str.as_ref(), "score": score}) - }) - .collect(); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "zscan", - "key": key, - "match": pattern, - "response": entries_json - }).to_string()) - } - Err(e) => Err(format!("KV zscan error: {}", e)), - } - } - - fn handle_bf_exists(&self, store: &Store, params: &HashMap<&str, &str>) -> Result { - let key = *params.get("key").ok_or("Missing required param 'key' for 'bfExists' action")?; - let item = *params.get("item").ok_or("Missing required param 'item' for 'bfExists' action")?; - - match store.bf_exists(key, item) { - Ok(exists) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "bfExists", - "key": key, - "item": item, - "response": exists - }).to_string()), - Err(e) => Err(format!("KV bfExists error: {}", e)), - } - } - - fn send_error(&self, msg: &str, body_size: usize) { - println!("{}", msg); - self.set_property( - vec!["response", "status"], - Some(b"500"), - ); - let error_body = json!({"error": msg}).to_string(); - self.set_http_response_body(0, body_size, error_body.as_bytes()); - } -} -``` - - -### FILE: examples/cdn/key_value/Cargo.toml - -```toml -[workspace] - -[package] -name = "key_value" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -querystring = "1.1" -serde_json = "1" -``` - - -### FILE: examples/cdn/key_value/README.md - -``` -[← Back to examples](../../README.md) - -# Key Value (CDN) - -Implements KV store operations via query parameters — get, scan, zrange, zscan, and bfExists — using the proxy-wasm ABI. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md index 78af977..48de0d0 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-large-dictionary-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md index 934c352..c4b7b5c 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-log-time-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md index e88ed80..0ebd4e7 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-md2html-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md index 575b2ab..364c99f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/cdn/examples-properties-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md index 92a6ebf..7bcf9c1 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/host-services-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Rust Host Services Reference diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md index 1640164..75ff192 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-ab-testing-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md index 27532f3..6699d03 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-api-wrapper-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # examples-api-wrapper-basic-rust diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md index d754a7e..6de33d5 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-backend-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,114 @@ cargo build --release --target wasm32-wasip1 - platform-overview (FastEdge request lifecycle, outbound request capabilities) - sdk-reference-rust (`fastedge::send_request`, `Body`, HTTP types) - best-practices (body buffering considerations, error handling patterns) + +## Source Material + +### FILE: examples/http/basic/backend/src/lib.rs + +```rust +use anyhow::{anyhow, Error, Result}; +use fastedge::body::Body; +use fastedge::http::{Method, Request, Response, StatusCode}; + +#[allow(dead_code)] +#[fastedge::http] +fn main(req: Request) -> Result> { + let (parts, body) = req.into_parts(); + let query = parts + .uri + .query() + .ok_or(anyhow!("missing uri query parameter"))?; + let params = querystring::querify(query); + let url = params + .iter() + .find(|(k, _)| k == &"url") + .ok_or(anyhow!("missing url parameter"))?; + let url = urlencoding::decode(url.1)?.to_string(); + println!("url = {:?}", url); + let request = Request::builder().uri(url).method(Method::GET).body(body)?; + + let response = fastedge::send_request(request).map_err(Error::msg)?; + + Response::builder() + .status(StatusCode::OK) + .body(Body::from(format!( + "len = {}, content-type = {:?}", + response.body().len(), + response.headers().get("Content-Type") + ))) + .map_err(Error::msg) +} +``` + + +### FILE: examples/http/basic/backend/Cargo.toml + +```toml +[workspace] + +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +anyhow = "1" +querystring = "1.1" +urlencoding = "2.1" +``` + + +### FILE: examples/http/basic/backend/README.md + +``` +[← Back to examples](../../../README.md) + +# Backend (URL Proxy) + +A FastEdge application that accepts a `?url=` query parameter, makes an outbound GET request to that URL via `fastedge::send_request`, and returns a summary of the upstream response (`len` and `content-type`) in the response body. + +> **When to use this example:** When you want to see how to make outbound HTTP requests from a FastEdge edge function using the legacy sync handler (`#[fastedge::http]`). For new apps, prefer the async WASI handler — see [`examples/http/wasi/hello_world`](../../wasi/hello_world/README.md). + +## What it does + +1. Parses the `?url=` query parameter from the request URI (percent-decodes it via `urlencoding::decode`). +2. Builds an outbound `GET` request to that URL using `fastedge::send_request`. +3. Returns HTTP 200 with a plain-text body: + ``` + len = , content-type = + ``` +4. Returns HTTP 500 with an error message if `?url=` is absent or the query string is missing. + +## APIs used + +| API | Purpose | +|---|---| +| `#[fastedge::http]` | Sync request-response handler macro | +| `fastedge::send_request(request)` | Blocking outbound HTTP request | +| `fastedge::http::{Request, Response, StatusCode, Method}` | HTTP types | +| `fastedge::body::Body` | Request and response bodies | +| `querystring::querify` | Parse query string into key-value pairs | +| `urlencoding::decode` | Percent-decode the `?url=` value | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/backend.wasm +``` + +## Expected behavior + +| Request | Response status | Response body | +|---|---|---| +| `GET /?url=https%3A%2F%2Fhttpbin.org%2Fget` | 200 | `len = , content-type = Some("")` | +| `GET /?q=hello` (no `url` key) | 500 | `missing url parameter` | +| `GET /` (no query string) | 500 | `missing uri query parameter` | + +The `len` value is the byte length of the upstream response body. The `content-type` value is the `Content-Type` header returned by the upstream server, formatted as a Rust `Option` debug string (e.g. `Some("application/json")`). +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md index 21d90c6..d9377ed 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -150,162 +150,3 @@ Uses the `wstd` WASI Component Model HTTP server macro. - KV Store provisioning and population via FastEdge API - examples-bloom-filter-denylist-js (JavaScript mirror of this example) - platform-overview (KV Store concepts) - -## Source Material - -### FILE: examples/http/wasi/bloom_filter_denylist/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Bloom-filter IP denylist example. - -Checks the client IP (from the `x-real-ip` request header, falling back to -`x-forwarded-for`) against a bloom filter stored in FastEdge KV. Returns 403 -on a hit, 200 otherwise. - -Required configuration: - - Environment variable: DENYLIST_STORE (KV store name holding the bloom filter) - -The bloom-filter key is hardcoded to `blocked-ips`. The handler is read-only; -populate the filter out of band. - -Mirror of the FastEdge-sdk-js `bloom-filter-denylist` example. -*/ - -use std::env; - -use anyhow::anyhow; -use fastedge::key_value::{Error as StoreError, Store}; -use serde_json::json; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -const BLOOM_KEY: &str = "blocked-ips"; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let store_name = match env::var("DENYLIST_STORE") { - Ok(s) if !s.trim().is_empty() => s, - _ => { - return json_response( - 500, - json!({ "error": "DENYLIST_STORE environment variable is not configured" }), - ); - } - }; - - let headers = req.headers(); - let client_ip = headers - .get("x-real-ip") - .or_else(|| headers.get("x-forwarded-for")) - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.split(',').next()) - .map(str::trim) - .filter(|s| !s.is_empty()); - - let Some(ip) = client_ip else { - return json_response(500, json!({ "error": "client IP not available" })); - }; - - let store = match Store::open(&store_name) { - Ok(s) => s, - Err(StoreError::AccessDenied) => { - return json_response( - 403, - json!({ "error": "access denied opening denylist store" }), - ); - } - Err(e) => { - return json_response(500, json!({ "error": format!("store open error: {e}") })); - } - }; - - let blocked = store - .bf_exists(BLOOM_KEY, ip) - .map_err(|e| anyhow!("bf_exists error: {e}"))?; - - if blocked { - // Bloom filter says "maybe in set" — a small fraction of hits will be false - // positives. Acceptable for a denylist (you over-block some legitimate users); - // not acceptable for allowlists — use `store.get()` against a regular key instead. - return json_response(403, json!({ "allowed": false, "ip": ip })); - } - - json_response(200, json!({ "allowed": true, "ip": ip })) -} - -fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result> { - Ok(Response::builder() - .status(status) - .header("content-type", "application/json") - .body(Body::from(value.to_string()))?) -} -``` - -### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml - -```toml -[workspace] - -[package] -name = "bloom_filter_denylist_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -serde_json = "1" -``` - -### FILE: examples/http/wasi/bloom_filter_denylist/README.md - -``` -[← Back to examples](../../../README.md) - -# Bloom Filter — IP Denylist (WASI) - -Rejects requests from IPs present in a KV Store bloom filter. Reads the client IP from the -`x-real-ip` request header (falling back to `x-forwarded-for`), checks it against a -pre-populated bloom filter, and returns **403** on a hit or **200** otherwise. - -Demonstrates `fastedge::key_value::Store` + `bf_exists()` and the conventional way to obtain -the client IP from a Component Model HTTP handler. - -## Configuration - -- Environment variable `DENYLIST_STORE` — name of the KV store that holds the bloom filter. -- Bloom-filter key — hardcoded to `blocked-ips`. Change `BLOOM_KEY` in `src/lib.rs` if your - key is different. - -## Behaviour - -| `bf_exists("blocked-ips", ip)` | Response | -| --- | --- | -| `true` | `403` `{ "allowed": false, "ip": "..." }` | -| `false` | `200` `{ "allowed": true, "ip": "..." }` | - -## Tradeoff: false positives - -Bloom filters answer "**definitely not** in set" vs "**maybe** in set". When `bf_exists` -returns `true`, the IP *probably* was added — but a small fraction of hits will be false -positives, meaning some legitimate IPs will be over-blocked. Acceptable for a denylist; for -allowlists or anything requiring exact membership, use `store.get()` against a regular key -instead. - -## Populating the filter - -The edge handler is read-only. Populate `blocked-ips` out of band (for example, via the -FastEdge API). - -## Related - -Mirror of `FastEdge-sdk-js/examples/bloom-filter-denylist/`. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md index b723ff8..cc1509e 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md index 4193334..a84bdad 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md index fe37566..05dd5fe 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md index 41b44c6..156e1a1 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md index fe47695..aa0c50f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md index 68f0752..0d256e6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md index 2f17c5e..7fe9ca6 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-hello-world-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -133,77 +133,3 @@ Hello, you made a wasi request to http:///? - fastedge-docs reference: best-practices - fastedge-docs reference: error-codes - Scaffold skill blueprints: http/base (Rust) - -## Source Material - -### FILE: examples/http/wasi/hello_world/src/lib.rs - -```rust -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(request: Request) -> anyhow::Result> { - let url = request.uri().to_string(); - - Ok(Response::builder() - .status(200) - .header("content-type", "text/plain;charset=UTF-8") - .body(Body::from(format!( - "Hello, you made a wasi request to {url}" - )))?) -} -``` - -### FILE: examples/http/wasi/hello_world/Cargo.toml - -```toml -[workspace] - -[package] -name = "hello_world" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -``` - -### FILE: examples/http/wasi/hello_world/README.md - -``` -[← Back to examples](../../../README.md) - -# Hello World (WASI) - -The simplest possible async FastEdge application — echoes the full request URI in the response body. - -Demonstrates the `#[wstd::http_server]` entry-point macro and the async handler signature used by all WASI HTTP examples. - -## What it returns - -``` -HTTP/1.1 200 OK -content-type: text/plain;charset=UTF-8 - -Hello, you made a wasi request to http:///? -``` - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/hello_world.wasm -``` - -## APIs used - -- `#[wstd::http_server]` — WASI HTTP entry-point macro -- `wstd::http::{Request, Response}` — request/response types -- `wstd::http::body::Body` — response body construction -- `request.uri().to_string()` — full absolute request URI -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md index 3766914..99f207d 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -378,277 +378,3 @@ cargo build --release - platform-overview (store provisioning and access control) - host-services-rust (other host service integrations) - examples-kv-store-js (JavaScript equivalent) - -## Source Material - -### FILE: examples/http/wasi/key_value/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example app demonstrating KV Store operations via the WASI-HTTP interface. - -Supports all KV Store operations via query parameters: - ?store=&action=get&key= - ?store=&action=scan&match= - ?store=&action=zrange&key=&min=&max= - ?store=&action=zscan&key=&match= - ?store=&action=bfExists&key=&item= - -Defaults to action=get if not specified. -*/ - -use std::collections::HashMap; - -use anyhow::anyhow; -use fastedge::key_value::{Store, Error as StoreError}; -use serde_json::json; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let query = req.uri().query().ok_or(anyhow!("no query parameters"))?; - let params: HashMap<&str, &str> = querystring::querify(query).into_iter().collect(); - - let store_name = *params - .get("store") - .ok_or(anyhow!("missing param 'store'"))?; - - let action = params.get("action").copied().unwrap_or("get"); - - let store = match Store::open(store_name) { - Ok(s) => s, - Err(StoreError::AccessDenied) => { - return Ok(Response::builder() - .status(403) - .header("content-type", "application/json") - .body(Body::from(json!({"error": "access denied"}).to_string()))?); - } - Err(e) => { - return Ok(Response::builder() - .status(500) - .header("content-type", "application/json") - .body(Body::from(json!({"error": format!("store open error: {e}")}).to_string()))?); - } - }; - - let body = match action { - "get" => handle_get(&store, ¶ms)?, - "scan" => handle_scan(&store, ¶ms)?, - "zrange" => handle_zrange(&store, ¶ms)?, - "zscan" => handle_zscan(&store, ¶ms)?, - "bfExists" => handle_bf_exists(&store, ¶ms)?, - _ => { - return Ok(Response::builder() - .status(400) - .header("content-type", "application/json") - .body(Body::from(json!({"error": format!("Invalid action '{action}'. Supported: get, scan, zrange, zscan, bfExists")}).to_string()))?); - } - }; - - Ok(Response::builder() - .status(200) - .header("content-type", "application/json") - .body(Body::from(body))?) -} - -fn handle_get(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { - let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; - match store.get(key) { - Ok(Some(value)) => { - let value_str = String::from_utf8_lossy(&value); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "get", - "key": key, - "response": value_str.as_ref() - }).to_string()) - } - Ok(None) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "get", - "key": key, - "response": null - }).to_string()), - Err(e) => Err(anyhow!("KV get error: {e}")), - } -} - -fn handle_scan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { - let pattern = *params - .get("match") - .ok_or(anyhow!("missing param 'match'"))?; - match store.scan(pattern) { - Ok(keys) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "scan", - "match": pattern, - "response": keys - }).to_string()), - Err(e) => Err(anyhow!("KV scan error: {e}")), - } -} - -fn handle_zrange(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { - let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; - let min: f64 = params - .get("min") - .ok_or(anyhow!("missing param 'min'"))? - .parse() - .map_err(|_| anyhow!("invalid 'min': must be a number"))?; - let max: f64 = params - .get("max") - .ok_or(anyhow!("missing param 'max'"))? - .parse() - .map_err(|_| anyhow!("invalid 'max': must be a number"))?; - - match store.zrange_by_score(key, min, max) { - Ok(entries) => { - let entries_json: Vec = entries - .iter() - .map(|(value, score)| { - let value_str = String::from_utf8_lossy(value); - json!({"value": value_str.as_ref(), "score": score}) - }) - .collect(); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "zrange", - "key": key, - "min": min, - "max": max, - "response": entries_json - }).to_string()) - } - Err(e) => Err(anyhow!("KV zrange error: {e}")), - } -} - -fn handle_zscan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { - let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; - let pattern = *params - .get("match") - .ok_or(anyhow!("missing param 'match'"))?; - - match store.zscan(key, pattern) { - Ok(entries) => { - let entries_json: Vec = entries - .iter() - .map(|(value, score)| { - let value_str = String::from_utf8_lossy(value); - json!({"value": value_str.as_ref(), "score": score}) - }) - .collect(); - Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "zscan", - "key": key, - "match": pattern, - "response": entries_json - }).to_string()) - } - Err(e) => Err(anyhow!("KV zscan error: {e}")), - } -} - -fn handle_bf_exists(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { - let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; - let item = *params - .get("item") - .ok_or(anyhow!("missing param 'item'"))?; - - match store.bf_exists(key, item) { - Ok(exists) => Ok(json!({ - "store": params.get("store").unwrap_or(&""), - "action": "bfExists", - "key": key, - "item": item, - "response": exists - }).to_string()), - Err(e) => Err(anyhow!("KV bfExists error: {e}")), - } -} -``` - -### FILE: examples/http/wasi/key_value/Cargo.toml - -```toml -[workspace] - -[package] -name = "key_value_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -querystring = "1.1" -serde_json = "1" -``` - -### FILE: examples/http/wasi/key_value/README.md - -``` -[← Back to examples](../../../README.md) - -# Key Value (WASI) - -Demonstrates all KV store operations via a query-parameter driven HTTP API: get, scan, zrange, zscan, and bfExists. - -## Usage - -All operations require `?store=` and `?action=` query parameters: - -| Action | Additional params | Description | -|---|---|---| -| `get` | `key=` | Fetch a single value by key | -| `scan` | `match=` | List keys matching a glob pattern | -| `zrange` | `key=&min=&max=` | Fetch sorted-set members by score range | -| `zscan` | `key=&match=` | List sorted-set members matching a pattern | -| `bfExists` | `key=&item=` | Check bloom filter membership | - -`action` defaults to `get` if omitted. - -## Example - -``` -GET /?store=my-store&action=get&key=hello -→ 200 {"store":"my-store","action":"get","key":"hello","response":"world"} - -GET /?store=my-store&action=scan&match=user:* -→ 200 {"store":"my-store","action":"scan","match":"user:*","response":["user:1","user:2"]} -``` - -## Error responses - -| Condition | Status | Body | -|---|---|---| -| Store not found / access denied | 403 | `{"error":"access denied"}` | -| Missing required params | 530 | Runtime error | -| Store open error | 500 | `{"error":"store open error: ..."}` | -| Invalid action | 400 | `{"error":"Invalid action '...'. Supported: ..."}` | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/key_value_wasi.wasm -``` - -## APIs used - -- `fastedge::key_value::Store::open(name)` — open a named KV store -- `store.get(key)` — fetch value by key; returns `Ok(Option>)` -- `store.scan(pattern)` — list keys by glob pattern -- `store.zrange_by_score(key, min, max)` — range query on sorted set -- `store.zscan(key, pattern)` — pattern scan on sorted set -- `store.bf_exists(key, item)` — bloom filter membership test -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md index e48ca3b..5215a84 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-large-env-variable-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Large Environment Variable (WASI) — Rust HTTP Example diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md index f98adf3..7e480df 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-markdown-render-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md index f25c8e7..50f4454 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -186,120 +186,3 @@ cargo build --release - sdk-reference-rust (full `fastedge` crate API reference) - examples-basic-http-rust (minimal sync handler without outbound fetch) - platform-overview (FastEdge execution model and WASM target context) - -## Source Material - -### FILE: examples/http/basic/outbound_fetch/src/lib.rs - -```rust -use anyhow::{Error, Result}; -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; -use serde_json::{json, Value}; - -#[fastedge::http] -fn main(_req: Request) -> Result> { - let upstream_req = Request::builder() - .uri("http://jsonplaceholder.typicode.com/users") - .body(Body::empty())?; - - let upstream_resp = fastedge::send_request(upstream_req).map_err(Error::msg)?; - - let body_bytes = upstream_resp.body().to_vec(); - let users: Value = serde_json::from_slice(&body_bytes)?; - - let sliced_users = match users.as_array() { - Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), - None => Value::Array(vec![]), - }; - - let result = json!({ - "users": sliced_users, - "total": 5, - "skip": 0, - "limit": 30, - }); - - Response::builder() - .status(StatusCode::OK) - .header("content-type", "application/json") - .body(Body::from(result.to_string())) - .map_err(Into::into) -} -``` - - -### FILE: examples/http/basic/outbound_fetch/Cargo.toml - -```toml -[workspace] - -[package] -name = "outbound_fetch" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -serde_json = "1" -``` - - -### FILE: examples/http/basic/outbound_fetch/README.md - -``` -[← Back to examples](../../../README.md) - -# Outbound Fetch (Basic HTTP) - -Demonstrates outbound HTTP from a FastEdge application using the **legacy sync handler** (`#[fastedge::http]`). Fetches user data from the [JSONPlaceholder](https://jsonplaceholder.typicode.com) public API, selects the first 5 users, and returns them in a paginated JSON envelope. - -> **When to use this example:** If you need a synchronous, single-function HTTP handler with outbound requests targeting `wasm32-wasip1`. For new apps, prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). - -## What it does - -On any incoming request: - -1. Makes a GET request to `http://jsonplaceholder.typicode.com/users` using `fastedge::send_request`. -2. Parses the JSON response body. -3. Takes the first 5 users from the array. -4. Returns a JSON envelope with pagination metadata. - -Example response body: - -```json -{ - "users": [ { "id": 1, "name": "Leanne Graham", ... }, ... ], - "total": 5, - "skip": 0, - "limit": 30 -} -``` - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::send_request` | Outbound HTTP request to upstream API | -| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | -| `fastedge::body::Body` | Request and response bodies | -| `serde_json` | JSON parsing and serialisation | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/outbound_fetch.wasm -``` - -## Expected behavior - -| Request | Response status | Response content-type | Response body | -|---|---|---|---| -| `GET /` | 200 | `application/json` | JSON object with `users` (array of ≤5), `total`, `skip`, `limit` | -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md index 051e70d..05d01a2 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Outbound Fetch — WASI (Rust) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md index 6be9762..83b2f9f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -183,7 +183,7 @@ Ok(Response::builder() - wstd HTTP client documentation - serde_json crate documentation -## Source Material +## Full Source ### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md index 31ed106..b0699e3 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-print-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md index 2f2a35a..f789dad 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-s3upload-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -182,3 +182,236 @@ cargo build --release - sdk-reference-rust (`fastedge::send_request`, `Body`, `Request`, `Response`) - host-services-rust (outbound HTTP) - rusty-s3 crate documentation (external) + +## Source Material + +### FILE: examples/http/basic/s3upload/src/lib.rs + +```rust +use std::time::Duration; + +use fastedge::{ + body::Body, + http::{header, Error, Method, Request, Response, StatusCode}, +}; +use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle}; +use std::{collections::HashMap, env}; +use url::Url; + +#[fastedge::http] +fn main(req: Request) -> Result, Error> { + match req.method() { + // Allow only POST and PUT requests + &Method::POST | &Method::PUT => (), + + &Method::OPTIONS => { + return Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()); + } + + // Deny anything else + _ => { + return Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .header(header::ALLOW, "PUT, POST") + .body(Body::from("This method is not allowed\n")); + } + }; + + /* get request params */ + let query_pairs = |q: &str| { + q.split('&') + .filter_map(|q| { + let mut i = q.splitn(2, '='); + let k = i.next()?; + let v = i.next()?; + Some((k, v)) + }) + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>() + }; + let hash_query: HashMap = req.uri().query().map_or(HashMap::new(), query_pairs); + + let fname = match hash_query.get("name") { + None => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")) + } + Some(i) => i, + }; + if req.body().is_empty() { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Malformed request\n")); + } + let content_type = match req.headers().get("Content-Type") { + None => "application/octet-stream", + Some(v) => v.to_str().unwrap_or("application/octet-stream"), + }; + let content_type = content_type.to_owned(); + let content = req.into_body(); + + match env::var("MAX_FILE_SIZE").ok() { + None => {} + Some(l) => match l.parse::() { + Err(_) => {} + Ok(v) => { + if content.len() > v { + let msg = format!("File exceeds allowed limit of {} bytes\n", v); + return Response::builder() + .status(StatusCode::PAYLOAD_TOO_LARGE) + .body(Body::from(msg.as_str().to_owned())); + } + } + }, + } + + let (signed_url, host) = match prepare_s3(fname) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("App misconfigured\n")) + } + Ok((u, h)) => (u, h), + }; + + /* build outgoing req */ + let out_req = Request::builder() + .method(Method::PUT) + .uri(signed_url.as_str()) + .header("Host", host) + .header("Accept-Encoding", "identity") + .header("Content-Length", content.len().to_string()) + .header("Content-Type", content_type); + + let Ok(req) = out_req.body(content) else { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Malformed request\n")); + }; + + let rsp = match fastedge::send_request(req) { + Err(_) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + } + Ok(r) => r, + }; + let (parts, body) = rsp.into_parts(); + let body = if parts.status == StatusCode::OK { + let mut tmp_url = signed_url.clone(); + tmp_url.set_query(None); + Body::from(tmp_url.to_string()) + } else { + body + }; + Ok(Response::from_parts(parts, body)) +} + +fn prepare_s3(fname: &str) -> anyhow::Result<(Url, String)> { + /* read S3 access params from env */ + let access_key = env::var("ACCESS_KEY")?; + let secret_key = env::var("SECRET_KEY")?; + let region = env::var("REGION")?; + let base_hostname = env::var("BASE_HOSTNAME")?; + let bucket = env::var("BUCKET")?; + let scheme = env::var("SCHEME").unwrap_or_else(|_| "http".to_string()); + + /* set S3 request params */ + let host = region.clone() + "." + base_hostname.as_str(); + let upload_url = scheme + "://" + host.as_str(); + let parsed_url = upload_url.parse()?; + let bucket = Bucket::new(parsed_url, UrlStyle::Path, bucket, region)?; + + let creds = Credentials::new(access_key, secret_key); + let action = bucket.put_object(Some(&creds), fname); + let signed_url = action.sign(Duration::from_secs(60 * 60)); + + Ok((signed_url, host)) +} +``` + + +### FILE: examples/http/basic/s3upload/Cargo.toml + +```toml +[workspace] + +[package] +name = "s3upload" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fastedge = "0.4" +url = "2.3" +rusty-s3 = "0.5" +anyhow = "1" +``` + + +### FILE: examples/http/basic/s3upload/README.md + +``` +[← Back to examples](../../../README.md) + +# S3 Upload + +FastEdge edge function that accepts a file upload, signs an S3 PUT request on the fly, uploads the file directly to an S3-compatible bucket, and returns the clean object URL to the caller. + +> **Legacy handler:** Uses `#[fastedge::http]` (sync, `wasm32-wasip1`). For new apps prefer the async WASI handler — see [`examples/http/wasi/`](../../wasi/). + +## What it does + +1. Accepts `POST` or `PUT` only — returns 405 for other methods +2. Requires `?name=` query parameter and a non-empty body — returns 400 otherwise +3. Enforces `MAX_FILE_SIZE` if set — returns 413 if exceeded +4. Calls `prepare_s3()` to build a 1-hour presigned `PUT` URL using `rusty_s3` +5. Forwards the file body to S3 via `fastedge::send_request` +6. On success (S3 returns 200): responds with the clean object URL (no query string) +7. On S3 error: forwards the S3 status and error body back to the caller + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ACCESS_KEY` | ✅ | S3 access key | +| `SECRET_KEY` | ✅ | S3 secret key | +| `REGION` | ✅ | S3 region (e.g. `s-ed1`) | +| `BASE_HOSTNAME` | ✅ | S3 base hostname (e.g. `cloud.gcore.lu`) | +| `BUCKET` | ✅ | S3 bucket name | +| `SCHEME` | optional | URL scheme — defaults to `http` | +| `MAX_FILE_SIZE` | optional | Maximum upload size in bytes — no limit if unset | + +The constructed endpoint is `://.//`. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip1/release/s3upload.wasm +``` + +## Usage + +``` +POST /upload?name=photo.jpg +Content-Type: image/jpeg + + +``` + +On success (200), the response body is the clean S3 object URL (presign query parameters stripped). + +## Notes + +- The `OPTIONS` method returns 204 but does **not** include CORS headers — add `Access-Control-Allow-*` headers if browser preflight support is needed. +- The presigned URL expires after 1 hour, but since the upload is performed server-side this has no practical impact. +- `MAX_FILE_SIZE` is silently ignored if set to a non-numeric value. +``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md index 3d9715b..d65296b 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Secret Access — Rust HTTP Example @@ -251,185 +251,3 @@ node tools/fixture-validator/index.mjs \ - fastedge-docs skill reference: sdk-reference-rust - fastedge-docs skill reference: error-codes - manage skill: secret management subcommands (set, list, delete) - -## Source Material - -### FILE: examples/http/basic/secret/src/lib.rs - -```rust -use anyhow::{Error, Result}; -use std::time::SystemTime; - -use fastedge::body::Body; -use fastedge::http::{Request, Response, StatusCode}; -use fastedge::secret; - -#[allow(dead_code)] -#[fastedge::http] -fn main(_req: Request) -> Result> { - let value = match secret::get("SECRET") { - Ok(value) => value, - Err(secret::Error::AccessDenied) => { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .map_err(Error::msg); - } - Err(secret::Error::Other(msg)) => { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::from(msg)) - .map_err(Error::msg); - } - Err(secret::Error::DecryptError) => { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .map_err(Error::msg); - } - }; - - if value.is_none() { - return Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()) - .map_err(Error::msg); - } - - let ts = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let effective_at_value = match secret::get_effective_at("SECRET", ts as u32) { - Ok(value) => value, - Err(secret::Error::AccessDenied) => { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::empty()) - .map_err(Error::msg); - } - Err(secret::Error::Other(msg)) => { - return Response::builder() - .status(StatusCode::FORBIDDEN) - .body(Body::from(msg)) - .map_err(Error::msg); - } - Err(secret::Error::DecryptError) => { - return Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .map_err(Error::msg); - } - }; - - if effective_at_value.is_none() { - return Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()) - .map_err(Error::msg); - } - - Response::builder() - .status(StatusCode::OK) - .body(Body::from(format!( - "get={:?}\nget_efective_at={:?}\n", - value, effective_at_value - ))) - .map_err(Error::msg) -} -``` - - -### FILE: examples/http/basic/secret/Cargo.toml - -```toml -[workspace] - -[package] -name = "secret" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -fastedge = "0.4" -anyhow = "1" -``` - - -### FILE: examples/http/basic/secret/README.md - -``` -[← Back to examples](../../../README.md) - -# Secret - -Demonstrates accessing encrypted secrets injected by the FastEdge platform using `secret::get()` and `secret::get_effective_at()`. Shows how to handle all error variants (access denied, decrypt error) and the time-based secret rotation API. - -## What it does - -On every request, the handler: - -1. Calls `secret::get("SECRET")` — retrieves the current value of the secret named `SECRET`. -2. If the secret is missing (returned as `None`), returns **404**. -3. Calls `secret::get_effective_at("SECRET", )` — retrieves the secret value effective at the current Unix timestamp, demonstrating the rotation/versioning API. -4. If that value is also missing, returns **404**. -5. On success, returns **200** with both values in the body (Debug format). - -## APIs used - -| API | Purpose | -|---|---| -| `#[fastedge::http]` | Sync request-response handler macro | -| `fastedge::secret::get(key)` | Retrieve the current value of a named secret | -| `fastedge::secret::get_effective_at(key, timestamp)` | Retrieve the secret value effective at a specific Unix timestamp | -| `fastedge::secret::Error` | Error variants: `AccessDenied`, `Other(msg)`, `DecryptError` | -| `fastedge::http::{Request, Response, StatusCode}` | HTTP types | -| `fastedge::body::Body` | Response body | - -## Secret error variants - -| Variant | HTTP response | Meaning | -|---|---|---| -| `Ok(Some(value))` | 200 with body | Secret found | -| `Ok(None)` | 404 empty | Secret name is valid but not set | -| `Err(AccessDenied)` | 403 empty | App is not permitted to read this secret | -| `Err(Other(msg))` | 403 with `msg` body | Other denial with a human-readable message | -| `Err(DecryptError)` | 500 empty | Secret exists but could not be decrypted | - -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip1/release/secret.wasm -``` - -## Expected behavior - -| Scenario | Secret `SECRET` | Response status | Response body | -|---|---|---|---| -| Happy path | `"my-value"` | 200 | `get=Some("my-value")\nget_efective_at=Some("my-value")\n` | -| Secret not set | (absent) | 404 | (empty) | -| Access denied | — | 403 | (empty) | - -> **Note:** The response body contains a typo in the field name (`get_efective_at` instead of `get_effective_at`). This is a known cosmetic issue in the source. - -## Local testing - -Inject the secret via a `.env` file in your fixtures directory using the `FASTEDGE_VAR_SECRET_` prefix: - -``` -# fixtures/.env -FASTEDGE_VAR_SECRET_SECRET=my-test-value -``` - -Run with the fixture validator: - -```sh -node tools/fixture-validator/index.mjs \ - FastEdge-sdk-rust/examples/http/basic/secret/ \ - --wasm FastEdge-sdk-rust/examples/http/basic/secret/target/wasm32-wasip1/release/secret.wasm -``` -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md index 483e53a..df75287 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Secret Rollover (WASI, Rust) @@ -124,28 +124,56 @@ Slots are ordered by slot number; any non-negative `u32` value is valid as a slo ## Implementation Pattern ```rust -use fastedge::secret; +use std::time::{SystemTime, UNIX_EPOCH}; -// Read slot from header, default to current unix timestamp -let slot: u32 = request - .headers() - .get("x-slot") - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.parse().ok()) - .unwrap_or_else(|| { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs() as u32 +use anyhow::anyhow; +use fastedge::secret; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + // Read slot from header, default to current unix timestamp + let slot: u32 = request + .headers() + .get("x-slot") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse().ok()) + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as u32 + }); + + let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + + // Get current (latest) value + let current = secret::get(secret_name) + .map_err(|e| anyhow!("secret::get failed: {e}"))?; + + // Get value effective at the given slot + let effective = secret::get_effective_at(secret_name, slot) + .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; + + let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, }); -// Get current (latest) value -let current = secret::get(secret_name) - .map_err(|e| anyhow!("secret::get failed: {e}"))?; - -// Get value effective at the given slot -let effective = secret::get_effective_at(secret_name, slot) - .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} ``` ## Constraints and Gotchas diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md index 3370f1b..5d19fe4 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -147,123 +147,3 @@ package = "component:simple_fetch" - sdk-reference-rust - examples-simple-request-rust (basic sync HTTP handler, `fastedge` crate) - host-services-rust (outbound fetch via host services) - -## Source Material - -### FILE: examples/http/wasi/simple_fetch/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example app demonstrating the WASI-HTTP interface via the wstd crate. - -The app receives an incoming HTTP request and makes an outbound HTTP request -to the URL specified in the `x-fetch-url` header (defaults to https://httpbin.org/get). - -Build with cargo-component: - cargo component build --release -*/ - -use anyhow::anyhow; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(request: Request) -> anyhow::Result> { - let target_url = request - .headers() - .get("x-fetch-url") - .and_then(|v| v.to_str().ok()) - .unwrap_or("https://httpbin.org/get") - .to_string(); - - println!("Fetching: {target_url}"); - - let upstream_req = Request::get(&target_url) - .header("accept", "application/json") - .body(Body::empty()) - .map_err(|e| anyhow!("failed to build request: {e}"))?; - - let client = Client::new(); - let response = client - .send(upstream_req) - .await - .map_err(|e| anyhow!("request failed: {e}"))?; - - println!("Response status: {}", response.status()); - - Ok(response) -} -``` - - -### FILE: examples/http/wasi/simple_fetch/Cargo.toml - -```toml -[workspace] - -[package] -name = "simple_fetch" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" - -[package.metadata.component] -package = "component:simple_fetch" -``` - - -### FILE: examples/http/wasi/simple_fetch/README.md - -``` -[← Back to examples](../../../README.md) - -# Simple Fetch - -A minimal example demonstrating outbound HTTP requests using the [WASI-HTTP](https://github.com/WebAssembly/wasi-http) interface via the [`wstd`](https://crates.io/crates/wstd) crate. - -Uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). The same async pattern is used by all examples in `examples/http/wasi/`. - -## How it works - -The app receives an incoming request, reads the target URL from the `x-fetch-url` header, makes an outbound GET request to that URL, and streams the response back to the caller. - -If the `x-fetch-url` header is absent, it defaults to `https://httpbin.org/get`. - -## Request headers - -| Header | Required | Description | -|--------|----------|-------------| -| `x-fetch-url` | No | URL to fetch. Defaults to `https://httpbin.org/get` | - -## Example - -```bash -curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// -``` - -## Build - -```bash -cargo build --release -# Output: target/wasm32-wasip2/release/simple_fetch.wasm -``` - -## Key differences from basic HTTP examples - -| | Basic HTTP (`fastedge` crate) | WASI HTTP (`wstd` crate) | -|---|---|---| -| Handler | `fn main(req)` — sync | `async fn main(req)` — async | -| Macro | `#[fastedge::http]` | `#[wstd::http_server]` | -| Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | -| Build target | `wasm32-wasip1` | `wasm32-wasip2` | -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md index 8c84135..caea588 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-smart-switch-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -183,6 +183,7 @@ Build output: `target/wasm32-wasip1/release/smart_switch.wasm` - **`fastedge::send_request` errors map to `StatusCode`**: These are not `anyhow::Error` or `std::error::Error`; match on `fastedge::Error` variants explicitly. - **`"ACCEPTED"` string determines success**: The command result is compared as a string; only `"ACCEPTED"` maps to `204`. Any other value maps to `404`. - **Auth is a plain header match**: `Authorization` header value is compared directly to `PASSWORD` env var — no bearer prefix stripping, no hashing. +- **`serde` listed as dependency but not directly used in source**: `serde_json` handles all JSON parsing; `serde` is a transitive requirement. --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md index 4093ad9..c3a255f 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-static-assets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md index b8e56f8..8a6e7c9 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -143,105 +143,3 @@ chunk 4 - examples-streaming-js reference (JavaScript mirror of this example) - wstd HTTP body reference - FastEdge SDK Rust reference - -## Source Material - -### FILE: examples/http/wasi/streaming/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Streaming response example. - -Generates a response body on the fly — five text chunks, one every 200ms — -using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime -polls the stream as the body is sent, so chunks flow to the client as they -are produced instead of all at once at the end. - -Watch it stream with `curl -N https:///` (`-N` disables client-side -buffering). - -Mirror of the FastEdge-sdk-js `streaming` example. -*/ - -use futures_lite::stream; -use wstd::http::body::Body; -use wstd::http::{Request, Response}; -use wstd::time::{Duration, Timer}; - -#[wstd::http_server] -async fn main(_request: Request) -> anyhow::Result> { - let chunk_stream = stream::unfold(0u32, |i| async move { - if i >= 5 { - return None; - } - Timer::after(Duration::from_millis(200)).wait().await; - Some((format!("chunk {i}\n"), i + 1)) - }); - - Ok(Response::builder() - .status(200) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from_stream(chunk_stream))?) -} -``` - - -### FILE: examples/http/wasi/streaming/Cargo.toml - -```toml -[workspace] - -[package] -name = "streaming_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -anyhow = "1" -futures-lite = "1" -``` - - -### FILE: examples/http/wasi/streaming/README.md - -``` -[← Back to examples](../../../README.md) - -# Streaming Response (WASI) - -Generates a response body on the fly — five text chunks, one every 200 ms — using -`Body::from_stream` backed by a `futures_lite::Stream`. Each chunk flows to the client as it -is produced, not all at once at the end. - -Demonstrates `wstd::http::body::Body::from_stream`, `futures_lite::stream::unfold` for async -stream generation, and `wstd::time::Timer` for per-chunk delays. - -## Testing the streaming behaviour - -```sh -curl -N https://.fastedge.cdn.gc.onl/ -``` - -`-N` disables curl's client-side buffering; without it you won't see chunks appear one at a -time. You should see `chunk 0`…`chunk 4` print at ~200ms intervals. - -## Other streaming patterns - -- **Pass-through streaming** — return an upstream response's body directly. See - [outbound_fetch/](../outbound_fetch/) for the no-buffer variant. -- **Transform streaming** — use `http_body_util::BodyExt::map_frame` on the incoming body, - then `Body::from_http_body` to wrap it back. Useful for chunk-level rewrites. -- **Stream from bytes** — `Body::from_stream(futures_lite::stream::iter(chunks))` where - `chunks` is any iterable of `Into`. - -## Related - -Mirror of `FastEdge-sdk-js/examples/streaming/`. -``` diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md index 84f7561..7fa38af 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-variables-and-secrets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md index 4585b12..c4910c8 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-watermark-basic-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # HTTP Example: Watermark (Basic, Rust) diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md index 85ac605..6d38e26 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/quickstart-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # FastEdge Rust SDK — Quickstart diff --git a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md index 7b86940..9c8406d 100644 --- a/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md +++ b/plugins/gcore-fastedge/skills/fastedge-docs/reference/sdk-reference-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> # Rust SDK Reference (`fastedge` crate + `fastedge-derive`) diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-rust.md index ba5b29f..ba87cbd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/ab-testing-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-rust.md index 4e444bf..4295b4b 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/api-key-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -212,122 +212,3 @@ target = "wasm32-wasip1" - FastEdge secrets configuration (dashboard secret variable setup) - auth-jwt-rust reference (JWT-based authentication — use when token expiry and claims are needed) - platform-overview reference (CDN vs HTTP app type distinction) - -## Source Material - -### FILE: examples/cdn/api_key/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating API key validation. - -Validates requests using an X-API-Key header checked against a stored -secret. Simpler alternative to JWT when token expiry and claims are -not needed. - -Required configuration: - - Secret: API_KEY -*/ - -use fastedge::proxywasm::secret; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(ApiKeyRoot) }); -}} - -struct ApiKeyRoot; - -impl Context for ApiKeyRoot {} - -impl RootContext for ApiKeyRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(ApiKeyContext)) - } -} - -struct ApiKeyContext; - -impl Context for ApiKeyContext {} - -impl HttpContext for ApiKeyContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let expected_key = match secret::get("API_KEY") { - Ok(Some(bytes)) => match String::from_utf8(bytes) { - Ok(s) if !s.is_empty() => s, - _ => { - self.send_http_response(500, vec![], Some(b"App misconfigured")); - return Action::Pause; - } - }, - _ => { - self.send_http_response(500, vec![], Some(b"App misconfigured")); - return Action::Pause; - } - }; - - let provided_key = match self.get_http_request_header("X-API-Key") { - Some(k) if !k.is_empty() => k, - _ => { - self.send_http_response( - 401, - vec![("WWW-Authenticate", "API-Key")], - Some(b"Missing X-API-Key header"), - ); - return Action::Pause; - } - }; - - if provided_key != expected_key { - println!("API key validation failed"); - self.send_http_response(403, vec![], Some(b"Invalid API key")); - return Action::Pause; - } - - // Strip the API key header before forwarding to upstream - self.set_http_request_header("X-API-Key", None); - - println!("API key validated successfully"); - Action::Continue - } -} -``` - - -### FILE: examples/cdn/api_key/Cargo.toml - -```toml -[workspace] - -[package] -name = "api_key" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -``` - - -### FILE: examples/cdn/api_key/README.md - -``` -[← Back to examples](../../README.md) - -# API Key (CDN) - -Validates requests using an `X-API-Key` header checked against a stored secret. Returns 401 if missing, 403 if invalid, and strips the header before forwarding to upstream. -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-rust.md index 28c7eda..c3da37d 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/auth-jwt-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -259,3 +259,142 @@ target = "wasm32-wasip1" - FastEdge SDK Rust reference (proxywasm module, secret API) - FastEdge secrets configuration (dashboard secret variable setup) - platform-overview reference (CDN vs HTTP app type distinction) + +## Source Material + +### FILE: examples/cdn/jwt/src/lib.rs + +```rust +use std::time::{SystemTime, UNIX_EPOCH}; +use headers::HeaderValue; +use headers::authorization::{Bearer, Credentials}; + +use fastedge::proxywasm::secret; +use jsonwebtoken::{decode, DecodingKey, Validation}; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use serde::Deserialize; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); +}} + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, _context_id: u32) -> Option> { + Some(Box::new(HttpHeaders {})) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders {} + +impl Context for HttpHeaders {} + +const UNAUTHORIZED: u32 = 401; +const FORBIDDEN: u32 = 403; +const INTERNAL_SERVER_ERROR: u32 = 500; + +#[derive(Debug, Deserialize, Default)] +struct Claims { + exp: u64, +} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(Some(secret)) = secret::get("secret") else { + println!("'secret' param not set"); + self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); + return Action::Pause; + }; + let Some(value) = self.get_http_request_header("Authorization") else { + println!("No auth header"); + self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); + return Action::Pause; + }; + + if value.is_empty() { + println!("Auth header is empty"); + self.send_http_response(UNAUTHORIZED, vec![], Some(b"No Authorization header")); + return Action::Pause; + }; + + let Ok(header) = value.parse::() else { + println!("Auth header is invalid"); + self.send_http_response(UNAUTHORIZED, vec![], Some(b"Invalid Authorization header")); + return Action::Pause; + }; + + + let Some(bearer) = Bearer::decode(&header) else { + println!("Auth header doesn't contain token"); + self.send_http_response(FORBIDDEN, vec![], Some(b"Token not found")); + return Action::Pause; + }; + + let token = bearer.token(); + + let decoding_key = DecodingKey::from_secret(&secret); + let mut validation = Validation::default(); + validation.set_required_spec_claims(&["exp"]); + // skip validation af aud and nbf claims + validation.validate_aud = false; + validation.validate_nbf = false; + validation.validate_exp = false; // will validate expiration separately + + let token_data = match decode::(token, &decoding_key, &validation) { + Ok(token_data) => token_data, + Err(error) => { + println!("Token is invalid"); + self.send_http_response(FORBIDDEN, vec![], Some(format!("Could not decode token {}: {}", token, error).as_bytes())); + return Action::Pause; + } + }; + + let claims = token_data.claims; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if now > claims.exp { + println!("Token expired"); + self.send_http_response(FORBIDDEN, vec![], Some(b"Token expired")); + return Action::Pause; + } + + println!("Token ok"); + Action::Continue + } +} +``` + + +### FILE: examples/cdn/jwt/Cargo.toml + +```toml +[workspace] + +[package] +name = "jwt" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +jsonwebtoken = "9" +serde = { version = "1", features = ["derive"] } +headers = "0.4" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-rust.md index d4f417a..d3ad5df 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: cdn-base source_repo: https://github.com/G-Core/FastEdge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-17 +updated: 2026-08-20 --- # Base Skeleton: CDN Rust @@ -208,3 +208,77 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { Action::Continue } ``` + +## Source Material + +### FILE: examples/cdn/hello_world/src/lib.rs + +```rust +use log::info; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HelloWorldRoot) }); +}} + +struct HelloWorldRoot; + +impl Context for HelloWorldRoot {} + +impl RootContext for HelloWorldRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(HelloWorld)) + } +} + +struct HelloWorld; + +impl Context for HelloWorld {} + +impl HttpContext for HelloWorld { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_headers"); + Action::Continue + } + + fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_request_body"); + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + self.add_http_response_header("x-powered-by", "FastEdge"); + info!("Hello from on_http_response_headers"); + Action::Continue + } + + fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { + info!("Hello from on_http_response_body"); + Action::Continue + } +} +``` + +### FILE: examples/cdn/hello_world/Cargo.toml + +```toml +[workspace] + +[package] +name = "hello_world" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +log = "0.4" +proxy-wasm = "0.2" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-rust.md index e8b9ba2..4b49934 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/body-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -243,125 +243,3 @@ Requires `log = "0.4"` in `Cargo.toml`. Log level is set to `Trace` at startup v - proxy-wasm HttpContext trait reference - host-services-rust reference (property API, logging) - platform-overview reference (CDN app lifecycle) - -## Source Material - -### FILE: examples/cdn/body/src/lib.rs - -```rust -use log::info; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpBodyRoot) }); -}} - -struct HttpBodyRoot; - -impl Context for HttpBodyRoot {} - -impl RootContext for HttpBodyRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(HttpBody)) - } -} - -struct HttpBody; - -impl Context for HttpBody {} - -impl HttpContext for HttpBody { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - self.set_http_request_header("content-length", None); - Action::Continue - } - - fn on_http_request_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // Wait -- we'll be called again when the complete body is buffered - // at the host side. - return Action::Pause; - } - - if let Some(body_bytes) = self.get_http_request_body(0, body_size) { - let body_str = String::from_utf8(body_bytes).unwrap(); - if body_str.contains("Client") { - let new_body = - format!("Client's original message body ({body_size} bytes) redacted.\n"); - self.set_http_request_body(0, body_size, &new_body.into_bytes()); - } - } - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // remove content-length as we plan to change the body size - self.set_http_response_header("content-length", None); - // set transfer-encoding to chunked as we don't know body length - self.set_http_response_header("transfer-encoding", Some("Chunked")); - - if let Some(content_type) = self.get_http_response_header("content-type") { - self.set_property(vec!["response.content_type"], Some(content_type.as_bytes())); - } - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - return Action::Pause; - } - - let url = if let Some(value) = self.get_property(vec!["request.url"]) { - let url = String::from_utf8_lossy(&value); - info!("url={}", url); - url.to_string() - } else { - "".to_string() - }; - - let content_type = - if let Some(content_type) = self.get_property(vec!["response.content_type"]) { - let content_type = String::from_utf8_lossy(&content_type); - info!("content_type={}", content_type); - content_type.to_string() - } else { - "NONE".to_string() - }; - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let body_str = String::from_utf8(body_bytes).unwrap(); - if body_str.contains("Client") { - let new_body = - format!("Original message body ({body_size} bytes) redacted.\nURL: {url}\nContent-Type: {content_type}\n"); - self.set_http_response_body(0, body_size, &new_body.into_bytes()); - } - } - Action::Continue - } -} -``` - -### FILE: examples/cdn/body/Cargo.toml - -```toml -[workspace] - -[package] -name = "body" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-rust.md index b49ed2a..0565961 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cache-control-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -219,3 +219,144 @@ use std::env; - proxy-wasm HttpContext trait reference - FastEdge-sdk-rust CDN examples overview - host-services-rust reference (hostcalls, logging) + +## Source Material + +### FILE: examples/cdn/cache_control/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating content-type-aware cache control. + +Sets Cache-Control response headers based on the content type and +response status, providing fine-grained control over CDN caching. + +Optional configuration: + - Environment variable: STATIC_MAX_AGE (default: 31536000) + - Environment variable: HTML_MAX_AGE (default: 3600) + - Environment variable: API_MAX_AGE (default: 0 = no-cache) +*/ + +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::env; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(CacheControlRoot) }); +}} + +struct CacheControlRoot; + +impl Context for CacheControlRoot {} + +impl RootContext for CacheControlRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(CacheControlContext)) + } +} + +struct CacheControlContext; + +impl Context for CacheControlContext {} + +impl HttpContext for CacheControlContext { + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + // Read response status + let status_code = self + .get_property(vec!["response.status"]) + .and_then(|bytes| { + if bytes.len() == 2 { + Some(u16::from_be_bytes([bytes[0], bytes[1]])) + } else { + None + } + }) + .unwrap_or(200); + + // Error responses should never be cached + if !(200..400).contains(&status_code) { + self.set_http_response_header("Cache-Control", Some("no-store")); + return Action::Continue; + } + + let content_type = self + .get_http_response_header("Content-Type") + .unwrap_or_default(); + + let static_max_age = env::var("STATIC_MAX_AGE").unwrap_or_else(|_| "31536000".to_string()); + let html_max_age = env::var("HTML_MAX_AGE").unwrap_or_else(|_| "3600".to_string()); + let api_max_age = env::var("API_MAX_AGE").unwrap_or_else(|_| "0".to_string()); + + let cache_control = if is_static_asset(&content_type) { + format!("public, max-age={}, immutable", static_max_age) + } else if content_type.contains("text/html") { + self.add_http_response_header("Vary", "Accept-Encoding"); + format!("public, max-age={}, must-revalidate", html_max_age) + } else if content_type.contains("application/json") + || content_type.contains("application/xml") + { + self.add_http_response_header("Vary", "Accept, Authorization"); + if api_max_age == "0" { + "no-cache, no-store, must-revalidate".to_string() + } else { + format!("private, max-age={}, must-revalidate", api_max_age) + } + } else { + "public, max-age=600".to_string() + }; + + self.set_http_response_header("Cache-Control", Some(&cache_control)); + + println!( + "Cache-Control: {} (content-type: {})", + cache_control, content_type + ); + + Action::Continue + } +} + +fn is_static_asset(content_type: &str) -> bool { + content_type.starts_with("image/") + || content_type.starts_with("font/") + || content_type.contains("application/javascript") + || content_type.contains("text/css") + || content_type.contains("text/javascript") + || content_type.contains("application/wasm") +} +``` + +### FILE: examples/cdn/cache_control/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_control" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/cache_control/README.md + +``` +[← Back to examples](../../README.md) + +# Cache Control (CDN) + +Sets `Cache-Control` response headers based on content type and response status, providing fine-grained control over CDN caching behaviour. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md index 69d0e83..e156022 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/convert-image-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -84,6 +84,9 @@ fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action **Logic:** 1. Always add `Image-Format: original` — serves as the cache key for the unconverted response. + ```rust + self.add_http_request_header("Image-Format", "original"); + ``` 2. Read `request.extension` property: ```rust let raw_ext = self.get_property(vec!["request.extension"]); @@ -284,300 +287,3 @@ proxy_wasm::main! {{ - scaffold reference for CDN app type - FastEdge SDK Rust host services reference (proxy-wasm ABI, `get_property`, `set_property`) - platform overview (CDN app lifecycle, cache variation with `Vary` headers) - -## Source Material - -### FILE: examples/cdn/convert_image/src/lib.rs - -```rust -use image::*; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::{env, env::VarError, io::Cursor, str::from_utf8}; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(ConvertImageRoot) }); -}} - -struct ConvertImageRoot; - -impl Context for ConvertImageRoot {} - -impl RootContext for ConvertImageRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(ConvertImageContext)) - } -} - -struct ConvertImageContext; - -impl Context for ConvertImageContext {} - -impl HttpContext for ConvertImageContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - // this header is used to select correct image version from cache - self.add_http_request_header("Image-Format", "original"); - - // get extension - let path = self.get_property(vec!["request.path"]).map(|v| String::from_utf8(v).unwrap_or_default()).unwrap_or_default(); - println!("request.path={path:?}"); - let raw_ext = self.get_property(vec!["request.extension"]); - println!("request.extension={raw_ext:?}"); - let Some(ext) = raw_ext else { - println!("No extension in request path, not transforming"); - return Action::Continue; - }; - let Ok(ext) = from_utf8(&ext) else { - println!("Invalid UTF-8 in request extension, not transforming"); - return Action::Continue; - }; - if ext.is_empty() { - println!("No extension in request path, not transforming"); - return Action::Continue; - } - - // FORMATS_TO_TRANSFORM contains list of file extensions to transfor - // note that jpg and jpeg are different extensions - let Ok(image_list) = str_param("FORMATS_TO_TRANSFORM") else { - println!("FORMATS_TO_TRANSFORM param is not set, not transforming"); - return Action::Continue; - }; - if !image_list.split(',').any(|entry| entry == ext) { - println!( - "extension {} is not in the list of formats to transform: {}, not transforming", - ext, image_list - ); - return Action::Continue; - } - - // requests from User agents that match substrings in the IGNORED_UA_LIST param are not transformed - let Some(ua) = self.get_http_request_header("User-Agent") else { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - }; - if ua.is_empty() { - println!("User-Agent header is not set, not transforming"); - return Action::Continue; - } - if let Ok(ua_to_ignore) = str_param("IGNORED_UA_LIST") { - if ua_to_ignore.split(",").any(|entry| ua.contains(entry)) { - println!("User-Agent is in ignore list, not transforming"); - return Action::Continue; - } - } - - // indicator for on_response_headers and for cache key - self.set_http_request_header("Image-Format", Some("image/avif")); - - Action::Continue - } - - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - // only process 200 responses - if let Some(status) = self.rsp_status() { - if status != 200 { - println!( - "Response status is {} instead of expected 200, not transforming", - status - ); - return Action::Continue; - } - } else { - println!("Response status is not set, not transforming"); - return Action::Continue; - } - - // if "Image-Format" request header is not set, don't convert the image - let Some(content_type) = self.get_http_request_header("Image-Format") else { - return Action::Continue; - }; - // instruct cache to vary by this header so "original" and "image/avif" are cached separately - self.add_http_response_header("Vary", "Image-Format"); - - if content_type == "original" { - return Action::Continue; - }; - - // image to be transformed, set headers accordingly - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some(content_type.as_str())); - - // indicate to on_http_response_body that transformation is needed - self.set_property(vec!["response.content-type"], Some(content_type.as_bytes())); - - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - if !end_of_stream { - // wait till we get complete body - return Action::Pause; - } - - let Some(content_type) = self.get_property(vec!["response.content-type"]) else { - return Action::Continue; - }; - - let Ok(content_type) = from_utf8(&content_type) else { - // should never happen - println!("Invalid UTF-8 in Content-Type"); - self.send_http_response(500, vec![], None); - return Action::Pause; - }; - - if content_type != "image/avif" { - // should never happen - println!( - "Content-Type {} is not supported, not transforming", - content_type - ); - return Action::Continue; - } - - if let Some(body_bytes) = self.get_http_response_body(0, body_size) { - let buf = body_bytes.as_bytes(); - let img = match load_from_memory(buf) { - Ok(i) => i, - Err(e) => { - println!("cannot load image to memory {}, not converting", e); - return Action::Continue; - } - }; - - let mut out = Vec::new(); - let mut c = Cursor::new(&mut out); - let res = img.write_with_encoder(codecs::avif::AvifEncoder::new_with_speed_quality( - &mut c, - u8_param("AVIF_SPEED", 1, 10, 5), - u8_param("AVIF_QUALITY", 1, 100, 70), - )); - - match res { - Ok(_) => { - println!( - "{} bytes -> {} bytes {}", - body_size, - out.len(), - content_type - ); - self.set_http_response_body(0, body_size, &out) - } - Err(e) => println!("cannot store transformed image {}", e), - } - } else { - println!("No response body to transform"); - } - - Action::Continue - } -} - -impl ConvertImageContext { - fn rsp_status(&mut self) -> Option { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() != 2 { - println!("HTTP status property is not 2 bytes"); - return None; - } - return Some(u16::from_be_bytes([status[0], status[1]])); - } - None - } -} - -fn str_param(name: &str) -> Result { - let val = env::var(name)?; - if val.is_empty() { - return Err(VarError::NotPresent); - } - - Ok(val) -} - -fn u8_param(name: &str, min: u8, max: u8, default: u8) -> u8 { - let Ok(val) = env::var(name) else { - println!("Param {} is not set, using default value {}", name, default); - return default; - }; - if val.is_empty() { - println!("Param {} is not set, using default value {}", name, default); - return default; - } - - let val = match val.parse() { - Err(_) => { - println!( - "Param {} is not a valid number, using default value {}", - name, default - ); - return default; - } - Ok(v) => v, - }; - if val < min { - println!( - "Param {} is below minimum {}, using default value {}", - name, min, default - ); - return default; - } - if val > max { - println!( - "Param {} is above maximum {}, using default value {}", - name, max, default - ); - return default; - } - - val -} -``` - - -### FILE: examples/cdn/convert_image/Cargo.toml - -```toml -[workspace] - -[package] -name = "convert_image" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -image = "0.25" -``` - - -### FILE: examples/cdn/convert_image/README.md - -``` -[← Back to examples](../../README.md) - -# Convert Image (CDN) - -Converts images to AVIF format on the fly using the proxy-wasm ABI. Only transforms requests matching configured file extensions and skips specified user agents. - -## Configuration - -- Environment variable: `FORMATS_TO_TRANSFORM` — comma-separated list of file extensions to convert (e.g. `jpg,jpeg,png`) -- Environment variable: `IGNORED_UA_LIST` — (optional) comma-separated list of User-Agent substrings to skip -- Environment variable: `AVIF_SPEED` — (optional) AVIF encoding speed, 1-10 (default: 5) -- Environment variable: `AVIF_QUALITY` — (optional) AVIF encoding quality, 1-100 (default: 70) - -## How it works - -1. **on_request_headers** — checks file extension and User-Agent, sets `Image-Format` header for cache variation -2. **on_response_headers** — sets response headers for AVIF content type on 200 responses -3. **on_response_body** — decodes the original image and re-encodes it as AVIF -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-rust.md index 019bcb3..f518e8f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/cors-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-rust.md index d8d3907..eca26cd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-error-pages-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -310,207 +310,3 @@ Edit `public/styles.css` directly. No build tools required. Styles are embedded - host-services-rust reference (property API) - platform-overview reference (CDN app lifecycle) - body-rust blueprint (general body manipulation pattern) - -## Source Material - -### FILE: examples/cdn/custom_error_pages/src/lib.rs - -```rust -use handlebars::Handlebars; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use serde_json::json; -use std::collections::HashMap; -use std::env; - -// Include the generated image map -include!(concat!(env!("OUT_DIR"), "/image_map.rs")); -include!(concat!(env!("OUT_DIR"), "/message_map.rs")); - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpBodyRoot) }); -}} - -struct HttpBodyRoot; - -impl Context for HttpBodyRoot {} - -impl RootContext for HttpBodyRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(HttpBody)) - } -} - -struct HttpBody; - -impl Context for HttpBody {} - -impl HttpContext for HttpBody { - fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { - if let Some(status) = self.get_property(vec!["response.status"]) { - if status.len() == 2 { - let status_code = u16::from_be_bytes([status[0], status[1]]); - if (400..600).contains(&status_code) { - // Remove the Content-Length header if it exists, we are going to change the response body - self.set_http_response_header("Content-Length", None); - self.set_http_response_header("Transfer-Encoding", Some("Chunked")); - self.set_http_response_header("Content-Type", Some("text/html")); - } - } - } - Action::Continue - } - - fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Action { - // only process 4xx/5xx error responses - let Some(status) = self.get_property(vec!["response.status"]) else { - return Action::Continue; - }; - if status.len() != 2 { - return Action::Continue; - } - let status_code = u16::from_be_bytes([status[0], status[1]]); - if !(400..600).contains(&status_code) { - return Action::Continue; - } - - if !end_of_stream { - // wait for complete body - return Action::Pause; - } - - // Get the image and message maps - let image_map = get_image_map(); - let message_map = get_message_map(); - - // Get the Base64-encoded image for the status code or its fallback - let base64_image = image_map - .get(&status_code) - .or_else(|| { - if (400..500).contains(&status_code) { - image_map.get(&4000) - } else if (500..600).contains(&status_code) { - image_map.get(&5000) - } else { - None - } - }) - .unwrap_or(&""); - - // Get the message and description for the status code or its fallback - let (message, description) = message_map - .get(&status_code) - .or_else(|| { - if (400..500).contains(&status_code) { - message_map.get(&4000) - } else if (500..600).contains(&status_code) { - message_map.get(&5000) - } else { - None - } - }) - .map(|(msg, desc)| (msg.to_string(), desc.to_string())) - .unwrap_or_else(|| { - ( - "Unexpected Error".to_string(), - "The server responded with a {{status}} error.".to_string(), - ) - }); - - let mut handlebars = Handlebars::new(); - // Use handlebars to complete message and description text allowing for usage of {{ status }} variable - handlebars - .register_template_string("message_template", message) - .unwrap(); - handlebars - .register_template_string("description_template", description) - .unwrap(); - - let msg_data = json!({ - "status": status_code.to_string(), - }); - - let complete_message = handlebars.render("message_template", &msg_data).unwrap(); - let complete_description = handlebars - .render("description_template", &msg_data) - .unwrap(); - - // Render the error page using Handlebars - let error_template = include_str!("../templates/error_page.hbs"); - handlebars - .register_template_string("error_template", error_template) - .unwrap(); - - let styles = include_str!("../public/styles.css"); - let page_data = json!({ - "styles": styles, - "status": status_code.to_string(), - "message": complete_message, - "description": complete_description, - "image": base64_image, - }); - - let html_body = handlebars.render("error_template", &page_data).unwrap(); - let body = html_body.as_bytes(); - self.set_http_response_body(0, body_size, body); - - Action::Continue - } -} -``` - -### FILE: examples/cdn/custom_error_pages/Cargo.toml - -```toml -[workspace] - -[package] -name = "custom_error_pages" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -handlebars = "6.3" -serde_json = "1.0" -regex = "1.10" - -[build-dependencies] -base64 = "0.22" -``` - -### FILE: examples/cdn/custom_error_pages/README.md - -``` -[← Back to examples](../../README.md) - -# Custom Error Pages (CDN) - -Intercepts 4xx and 5xx error responses and replaces them with branded HTML error pages using Handlebars templates. - -## How it works - -A [build script](./build.rs) runs at compile time to embed images and messages from the `public/` folder into the WASM binary (since there is no filesystem at runtime). - -At runtime, when an error response is detected: -1. **on_response_headers** — sets `Content-Type` to `text/html` for error responses -2. **on_response_body** — looks up the status code in the embedded image/message maps, falls back to generic `4xx`/`5xx` templates, and renders the error page using Handlebars - -## Adding a custom error page - -1. Add an image: `public/images/.jpg` -2. Add a message file: `public/messages/.hbs` (first line = title, second line = description) -3. Recompile and deploy - -## Styling - -Styles are in [`public/styles.css`](./public/styles.css) — plain CSS, no build tools required. Edit directly. -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md index de7cb9f..a95115f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/custom-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -88,7 +88,7 @@ impl HttpContext for HttpHeaders { return Action::Pause; }; - // Trim leading '/' + //trim first '/' let path = if path.starts_with('/') { &path[1..] } else { @@ -260,123 +260,3 @@ Action::Pause - host-services-rust reference (property access, send_http_response ABI details) - sdk-reference-rust reference (proxy-wasm trait hierarchy) - best-practices reference (error handling patterns, Action semantics) - -## Source Material - -### FILE: examples/cdn/custom/src/lib.rs - -```rust -use proxy_wasm::traits::*; -use proxy_wasm::types::*; -use std::time::Duration; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); -}} - -const BAD_REQUEST: u32 = 400; - -struct HttpHeadersRoot; - -impl Context for HttpHeadersRoot {} - -impl RootContext for HttpHeadersRoot { - fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(HttpHeaders)) - } - - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } -} - -struct HttpHeaders; - -impl Context for HttpHeaders {} - -impl HttpContext for HttpHeaders { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Some(path) = self.get_property(vec!["request.path"]) else { - self.send_http_response(BAD_REQUEST, vec![], Some(b"Malformed request - no path")); - return Action::Pause; - }; - - let Ok(path) = std::str::from_utf8(&path) else { - self.send_http_response( - BAD_REQUEST, - vec![], - Some(b"Malformed request - not utf8 string"), - ); - return Action::Pause; - }; - - //trim first '/' - let path = if path.starts_with('/') { - &path[1..] - } else { - path - }; - let mut segments = path.split('/'); - - let Some(status_code) = segments.next() else { - return Action::Continue; - }; - - if let Some(delay) = segments.next() { - if let Ok(delay) = delay.parse::() { - std::thread::sleep(Duration::from_millis(delay)); - } - } - - let Ok(status_code) = status_code.parse::() else { - self.send_http_response( - BAD_REQUEST, - vec![], - Some(b"Malformed request - invalid status code"), - ); - return Action::Pause; - }; - - match status_code { - 0 | 200 => Action::Continue, - code if code < 600 => { - self.send_http_response(code, vec![], None); - Action::Pause - } - _ => { - self.send_http_response(BAD_REQUEST, vec![], None); - Action::Pause - } - } - } -} -``` - -### FILE: examples/cdn/custom/Cargo.toml - -```toml -[workspace] - -[package] -name = "custom" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` - -### FILE: examples/cdn/custom/README.md - -``` -[← Back to examples](../../README.md) - -# Custom (CDN) - -Returns HTTP status codes based on the request path, with optional delay support. Useful for testing and debugging CDN behaviour. -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-rust.md index 9336819..e8a660a 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/env-secrets-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -175,104 +175,3 @@ println!("PASSWORD: {}", password); - FastEdge app secrets management (platform docs) - proxy-wasm HttpContext trait reference - fastedge crate proxywasm feature documentation - -## Source Material - -### FILE: examples/cdn/variables_and_secrets/src/lib.rs - -```rust -/* -* Copyright 2025 G-Core Innovations SARL -*/ -/* -Example CDN app demonstrating environment variables and secrets access. - -Reads USERNAME from environment variables and PASSWORD from secrets, -then forwards both as request headers to the upstream origin. - -Required configuration: - - Environment variable: USERNAME - - Secret: PASSWORD -*/ - -use fastedge::proxywasm::secret; -use std::env; -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Info); - proxy_wasm::set_root_context(|_| -> Box { Box::new(VariablesRoot) }); -}} - -struct VariablesRoot; - -impl Context for VariablesRoot {} - -impl RootContext for VariablesRoot { - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } - - fn create_http_context(&self, _: u32) -> Option> { - Some(Box::new(VariablesContext)) - } -} - -struct VariablesContext; - -impl Context for VariablesContext {} - -impl HttpContext for VariablesContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let username = env::var("USERNAME").unwrap_or_default(); - let password = secret::get("PASSWORD") - .ok() - .flatten() - .and_then(|v| String::from_utf8(v).ok()) - .unwrap_or_default(); - - println!("USERNAME: {}", username); - // WARNING: Secrets are stored and retrieved as plaintext. Never log secret values - // in production code — platform logs are visible to operators and may be persisted. - // This line is shown for demonstration only; remove it in any real application. - println!("PASSWORD: {}", password); - - self.add_http_request_header("x-env-username", &username); - // WARNING: Forwarding a secret in a request header exposes it to the upstream origin - // and any intermediary that can inspect headers. Only do this when the upstream - // channel is trusted and the header is required by the destination API. - self.add_http_request_header("x-env-password", &password); - - Action::Continue - } -} -``` - -### FILE: examples/cdn/variables_and_secrets/Cargo.toml - -```toml -[workspace] - -[package] -name = "variables_and_secrets" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -fastedge = { version = "0.4", features = ["proxywasm"] } -``` - -### FILE: examples/cdn/variables_and_secrets/README.md - -``` -[← Back to examples](../../README.md) - -# Variables and Secrets (CDN) - -Reads `USERNAME` from environment variables and `PASSWORD` from secrets for request forwarding using the proxy-wasm ABI. -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-rust.md index dab63e7..a71fca0 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geo-redirect-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -152,3 +152,128 @@ proxy_wasm::main! {{ - FastEdge-sdk-rust CDN examples overview - proxy-wasm HttpContext trait reference - host-services-rust reference (property access, header manipulation) + +## Source Material + +### FILE: examples/cdn/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating geo-based origin routing. + +Routes requests to country-specific origins based on the request's +geo-IP country code. Falls back to a DEFAULT origin when no +country-specific mapping is configured. + +Required configuration: + - Environment variable: DEFAULT (fallback origin URL) + - Environment variable: (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use proxy_wasm::traits::*; +use std::env; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoRedirectRoot) }); +}} + +struct GeoRedirectRoot; + +impl Context for GeoRedirectRoot {} + +impl RootContext for GeoRedirectRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(GeoRedirectContext)) + } +} + +struct GeoRedirectContext; + +impl Context for GeoRedirectContext {} + +impl HttpContext for GeoRedirectContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let Ok(default_origin) = env::var("DEFAULT") else { + self.send_http_response(500, vec![], Some(b"App misconfigured - DEFAULT must be set")); + return Action::Pause; + }; + + let country_code = self + .get_property(vec!["request.country"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_default(); + + if country_code.is_empty() { + self.send_http_response(502, vec![], Some(b"Missing country information")); + return Action::Pause; + } + + let origin = env::var(&country_code).unwrap_or(default_origin); + let origin = origin.trim_end_matches('/'); + + let path = self + .get_property(vec!["request.path"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| "/".to_string()); + + // Preserve the Host header if present + if let Some(host) = self + .get_property(vec!["request.host"]) + .and_then(|bytes| String::from_utf8(bytes).ok()) + { + self.set_http_request_header("Host", Some(&host)); + } + + let request_url = format!("{}{}", origin, path); + + println!("Redirecting to: {}", request_url); + + self.set_property(vec!["request.url"], Some(request_url.as_bytes())); + + Action::Continue + } +} +``` + + +### FILE: examples/cdn/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + + +### FILE: examples/cdn/geo_redirect/README.md + +``` +[← Back to examples](../../README.md) + +# Geo Redirect (CDN) + +Routes CDN requests to country-specific origins based on the geoIP country code using the proxy-wasm ABI. + +## Configuration + +- Environment variable: `DEFAULT` — fallback origin URL +- Environment variable: `` — optional per-country origin URLs (e.g. `US`, `DE`, `GB`) +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-rust.md index 189abf0..1f39fe8 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/geoblock-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -206,112 +206,3 @@ Output binary: `target/wasm32-wasip1/release/.wasm` - FastEdge SDK Rust reference (proxy-wasm trait definitions and types) - Platform overview reference (request.country property and other injected properties) - deploy skill reference (uploading and registering the compiled WASM binary) - -## Source Material - -### FILE: examples/cdn/geoblock/src/lib.rs - -```rust -use std::env; -use std::time::{SystemTime, UNIX_EPOCH}; - -use proxy_wasm::traits::*; -use proxy_wasm::types::*; - -proxy_wasm::main! {{ - proxy_wasm::set_log_level(LogLevel::Trace); - proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoblockRoot) }); -}} - -struct GeoblockRoot; - -impl Context for GeoblockRoot {} - -impl RootContext for GeoblockRoot { - fn create_http_context(&self, _context_id: u32) -> Option> { - Some(Box::new(GeoblockContext {})) - } - - fn get_type(&self) -> Option { - Some(ContextType::HttpContext) - } -} - -struct GeoblockContext {} - -impl Context for GeoblockContext {} - -const BAD_GATEWAY: u32 = 502; -const FORBIDDEN: u32 = 403; -const INTERNAL_SERVER_ERROR: u32 = 500; - -impl HttpContext for GeoblockContext { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - let Ok(blacklist) = env::var("BLACKLIST") else { - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - - let mut blacklist = blacklist.split(','); - - let Some(country) = self.get_property(vec!["request.country"]) else { - self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - no country field")); - return Action::Pause; - }; - - let Ok(country) = std::str::from_utf8(&country) else { - self.send_http_response(BAD_GATEWAY, vec![], Some(b"Malformed request - country not utf8 string")); - return Action::Pause; - }; - - if blacklist.any(|b| country.eq_ignore_ascii_case(b)) { - let tw_start = env::var("BLACKLIST_TW_START").ok(); - let tw_end = env::var("BLACKLIST_TW_END").ok(); - - if let Some((tw_start, tw_end)) = tw_start.zip(tw_end) { - let Ok(tw_start) = tw_start.parse::() else { - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - - let Ok(tw_end) = tw_end.parse::() else { - self.send_http_response(INTERNAL_SERVER_ERROR, vec![], Some(b"App misconfigured")); - return Action::Pause; - }; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - if now >= tw_start && now <= tw_end { - self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); - return Action::Pause; - } - } else { - self.send_http_response(FORBIDDEN, vec![], Some(b"Request blacklisted")); - return Action::Pause; - } - } - - - Action::Continue - } -} -``` - -### FILE: examples/cdn/geoblock/Cargo.toml - -```toml -[workspace] - -[package] -name = "geoblock" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -proxy-wasm = "0.2" -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-rust.md index f2b6798..0fb98ea 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/headers-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -327,3 +327,356 @@ Called after the request/response cycle completes. Use for per-request logging o - proxy-wasm Rust SDK reference (sdk-reference-rust) - FastEdge CDN app examples index (examples CDN) - Host services Rust reference (host-services-rust) + +## Source Material + +### FILE: examples/cdn/headers/src/lib.rs + +```rust +use proxy_wasm::traits::*; +use proxy_wasm::types::*; +use std::collections::HashSet; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Trace); + proxy_wasm::set_root_context(|_| -> Box { Box::new(HttpHeadersRoot) }); +}} + +struct HttpHeadersRoot; + +impl Context for HttpHeadersRoot {} + +impl RootContext for HttpHeadersRoot { + fn create_http_context(&self, context_id: u32) -> Option> { + Some(Box::new(HttpHeaders { context_id })) + } + + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } +} + +struct HttpHeaders { + context_id: u32, +} + +impl Context for HttpHeaders {} + +impl HttpContext for HttpHeaders { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_request_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_request_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_request_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_request_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_request_header("new-header-01", "value-01"); + self.add_http_request_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_request_header("new-header-02", "value-02"); + self.add_http_request_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_request_header("new-header-03", "value-03"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_request_header("new-header-01", None); + self.set_http_request_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_request_header("new-header-02", Some("new-value-02")); + self.set_http_request_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_request_header("new-header-03", "value-03-a"); + self.add_http_request_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // try to set/add response headers + self.add_http_response_header("new-response-header", "value-01"); + self.set_http_response_header("cache-control", None); + self.set_http_response_header("new-response-header", Some("value-02")); + + // get new headers + let headers = self + .get_http_request_headers() + .into_iter() + .collect::>(); + let headers_bytes = self + .get_http_request_headers_bytes() + .into_iter() + .collect::>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::>(); + + let diff = headers + .difference(&original_headers) + .collect::>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::>(); + + let diff = diff.difference(&expected).collect::>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + // check if the response header is not returned + if self.get_http_response_header("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + if self.get_http_response_header_bytes("host").is_some() { + self.send_http_response(553, vec![], None); + return Action::Pause; + }; + + let response_headers = self.get_http_response_headers(); + if response_headers.len() != 1 { + self.send_http_response(555, vec![], None); + return Action::Pause; + } + let Some((name, value)) = response_headers.into_iter().next() else { + self.send_http_response(555, vec![], None); + return Action::Pause; + }; + if name != "new-response-header" || value != "value-02" { + self.send_http_response(556, vec![], None); + return Action::Pause; + } + + Action::Continue + } + + fn on_http_response_headers(&mut self, _: usize, _: bool) -> Action { + let mut original_headers = HashSet::new(); + let mut original_headers_bytes = HashSet::new(); + + // iterate over the headers and print them + for (name, value) in self.get_http_response_headers().into_iter() { + println!("#{} -> {}: {}", self.context_id, name, value); + original_headers.insert((name, value)); + } + for (name, value) in self.get_http_response_headers_bytes().into_iter() { + println!("#{} -> {}: {:?}", self.context_id, name, value); + original_headers_bytes.insert((name, value)); + } + if original_headers.is_empty() || original_headers_bytes.is_empty() { + self.send_http_response(550, vec![], None); + return Action::Pause; + } + + // check if the host header is present + if self.get_http_response_header("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + if self.get_http_response_header_bytes("host").is_none() { + self.send_http_response(551, vec![], None); + return Action::Pause; + } + + // add new headers + self.add_http_response_header("new-header-01", "value-01"); + self.add_http_response_header_bytes("new-header-bytes-01", b"value-bytes-01"); + + self.add_http_response_header("new-header-02", "value-02"); + self.add_http_response_header_bytes("new-header-bytes-02", b"value-bytes-02"); + + self.add_http_response_header("new-header-03", "value-03"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03"); + + //remove header new-headter-01, expected empty value + self.set_http_response_header("new-header-01", None); + self.set_http_response_header_bytes("new-header-bytes-01", None); + + // changing header value + self.set_http_response_header("new-header-02", Some("new-value-02")); + self.set_http_response_header_bytes("new-header-bytes-02", Some(b"new-value-bytes-02")); + + // add new header with existing name + self.add_http_response_header("new-header-03", "value-03-a"); + self.add_http_response_header_bytes("new-header-bytes-03", b"value-bytes-03-a"); + + // get new headers + let headers = self + .get_http_response_headers() + .into_iter() + .collect::>(); + let headers_bytes = self + .get_http_response_headers_bytes() + .into_iter() + .collect::>(); + + let expected = [ + ("new-header-01".to_string(), "".to_string()), + ("new-header-bytes-01".to_string(), "".to_string()), + ("new-header-02".to_string(), "new-value-02".to_string()), + ( + "new-header-bytes-02".to_string(), + "new-value-bytes-02".to_string(), + ), + ("new-header-03".to_string(), "value-03".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03".to_string(), + ), + ("new-header-03".to_string(), "value-03-a".to_string()), + ( + "new-header-bytes-03".to_string(), + "value-bytes-03-a".to_string(), + ), + ]; + + let expected = expected.iter().collect::>(); + + let expected_bytes = [ + ("new-header-01".to_string(), b"".to_vec()), + ("new-header-bytes-01".to_string(), b"".to_vec()), + ("new-header-02".to_string(), b"new-value-02".to_vec()), + ( + "new-header-bytes-02".to_string(), + b"new-value-bytes-02".to_vec(), + ), + ("new-header-03".to_string(), b"value-03".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03".to_vec(), + ), + ("new-header-03".to_string(), b"value-03-a".to_vec()), + ( + "new-header-bytes-03".to_string(), + b"value-bytes-03-a".to_vec(), + ), + ]; + + let expected_bytes = expected_bytes.iter().collect::>(); + + let diff = headers + .difference(&original_headers) + .collect::>(); + + let diff_bytes = headers_bytes + .difference(&original_headers_bytes) + .collect::>(); + + let diff = diff.difference(&expected).collect::>(); + + if !diff.is_empty() { + println!("different headers: {:?}", diff); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + let diff_bytes = diff_bytes.difference(&expected_bytes).collect::>(); + if !diff_bytes.is_empty() { + println!("different headers bytes: {:?}", diff_bytes); + self.send_http_response(552, vec![], None); + return Action::Pause; + } + + Action::Continue + } +} +``` + +### FILE: examples/cdn/headers/Cargo.toml + +```toml +[workspace] + +[package] +name = "headers" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +``` + +### FILE: examples/cdn/headers/README.md + +``` +[← Back to examples](../../README.md) + +# Headers (CDN) + +Validates and manipulates HTTP request and response headers using the proxy-wasm ABI. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-rust.md index c9efc2c..d382b3a 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/http-call-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-rust.md index 6a0769b..ae5fef9 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/kv-store-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-rust.md index a8334a3..dda7cb2 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/large-dictionary-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -153,3 +153,111 @@ println!("LARGE_CONFIG size: {} bytes", size); - proxy-wasm HttpContext trait reference - FastEdge environment variable configuration docs - `std::env::var` (standard Rust env access for values under 64KB) + +## Source Material + +### FILE: examples/cdn/large_env_variable/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example CDN app demonstrating access to large environment variables. + +Uses `fastedge::proxywasm::dictionary` to read environment variables that +may exceed the 64KB WASI environment variable size limit. + +For normal-sized environment variables (< 64KB), prefer `std::env::var()` +instead. The dictionary API is only required when your variable value +may be larger than 64KB. + +Required configuration: + - Environment variable: LARGE_CONFIG (a large configuration payload, e.g. JSON) +*/ + +use fastedge::proxywasm::dictionary; +use proxy_wasm::traits::*; +use proxy_wasm::types::*; + +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(LargeEnvRoot) }); +}} + +struct LargeEnvRoot; + +impl Context for LargeEnvRoot {} + +impl RootContext for LargeEnvRoot { + fn get_type(&self) -> Option { + Some(ContextType::HttpContext) + } + + fn create_http_context(&self, _: u32) -> Option> { + Some(Box::new(LargeEnvContext)) + } +} + +struct LargeEnvContext; + +impl Context for LargeEnvContext {} + +impl HttpContext for LargeEnvContext { + fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + // Use dictionary::get for environment variables that may exceed 64KB. + // For normal-sized env vars, use std::env::var() instead. + let config = dictionary::get("LARGE_CONFIG").unwrap_or_default(); + + let size = config.len(); + println!("LARGE_CONFIG size: {} bytes", size); + + self.add_http_request_header("x-config-size", &size.to_string()); + + Action::Continue + } +} +``` + +### FILE: examples/cdn/large_env_variable/Cargo.toml + +```toml +[workspace] + +[package] +name = "large_env_variable" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +proxy-wasm = "0.2" +fastedge = { version = "0.4", features = ["proxywasm"] } +``` + +### FILE: examples/cdn/large_env_variable/README.md + +``` +[← Back to examples](../../README.md) + +# Large Environment Variable (CDN) + +Demonstrates how to read **large environment variables** (> 64KB) using `fastedge::proxywasm::dictionary`. + +## When to use `dictionary` vs `std::env` + +| Method | Use when | +|--------|----------| +| `std::env::var("KEY")` | Variable value is under 64KB (most cases) | +| `fastedge::proxywasm::dictionary::get("KEY")` | Variable value may exceed the 64KB WASI env var size limit | + +The WASI environment variable interface has a **64KB size limit** per variable. If your app needs to read larger values (e.g. large JSON configs, certificates, policy documents), use the `dictionary` API which bypasses this limit. + +For all other environment variable access, prefer `std::env::var()` as it is the standard, idiomatic Rust approach. + +## Required configuration + +- **Environment variable**: `LARGE_CONFIG` - a large configuration payload (e.g. JSON, PEM certificate) +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md index a39c42e..4c8ed4f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/log-time-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md index ef3b630..84d4b88 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/md2html-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-rust.md index a76a8e3..c29b799 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/cdn/properties-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-wasi-rust.md index 9f1a6a0..0edcedd 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/ab-testing-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -217,3 +217,214 @@ Response::builder() - fastedge-sdk-rust platform overview - host-services-rust reference (outbound HTTP, environment variables) - best-practices reference (cookie security, entropy sources) + +## Source Material + +### FILE: examples/http/wasi/ab_testing/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Cookie-based A/B testing example. + +Reads or creates an `x-fastedge-abid` cookie, uses its value to deterministically +assign the visitor to weighted variants of each configured test, then proxies the +request to `OUTBOUND_URL` with the variant assignments attached as `ab-test-` +headers. The origin response is returned verbatim with a `set-cookie` header so +returning visitors receive the same variants on subsequent visits. + +Required configuration: + - Environment variable: OUTBOUND_URL (downstream origin to proxy to) + +Mirror of the FastEdge-sdk-js `ab-testing` example. +*/ + +use std::env; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +struct VariantWeight { + variant: &'static str, + weight: f64, +} + +struct AbTest { + name: &'static str, + variants: &'static [VariantWeight], +} + +static TESTS: &[AbTest] = &[ + AbTest { + name: "logo", + variants: &[ + VariantWeight { variant: "hops", weight: 50.0 }, + VariantWeight { variant: "bottle", weight: 50.0 }, + ], + }, + AbTest { + name: "font", + variants: &[ + VariantWeight { variant: "exo2", weight: 40.0 }, + VariantWeight { variant: "gloria", weight: 65.0 }, + VariantWeight { variant: "standard", weight: 45.0 }, + ], + }, +]; + +const AB_COOKIE: &str = "x-fastedge-abid"; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let outbound_url = match env::var("OUTBOUND_URL") { + Ok(u) if !u.trim().is_empty() => u, + _ => { + return Ok(Response::builder() + .status(500) + .body(Body::from( + "OUTBOUND_URL environment variable is not configured", + ))?); + } + }; + + let raw_cookie = req + .headers() + .get("cookie") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let (xid, cleaned_cookie) = match extract_abid(&raw_cookie) { + Some(existing) if is_valid_xid(existing) => { + (existing.to_string(), strip_abid(&raw_cookie)) + } + _ => (generate_xid(), raw_cookie.clone()), + }; + + // Build the outbound request: copy incoming headers except `host` and + // `cookie` (which we handle specially), replace the cookie with a version + // that has the abid stripped (so the origin never sees it), and attach an + // `ab-test-` header for every configured test. + let mut builder = Request::get(&outbound_url); + for (name, value) in req.headers() { + let n = name.as_str(); + if n == "host" || n == "cookie" { + continue; + } + if let Ok(v) = value.to_str() { + builder = builder.header(n, v); + } + } + if !cleaned_cookie.trim().is_empty() { + builder = builder.header("cookie", cleaned_cookie); + } + for test in TESTS { + if let Some(variant) = assign_variant(&xid, test) { + builder = builder.header(format!("ab-test-{}", test.name), variant); + } + } + + let outbound_req = builder + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build outbound request: {e}"))?; + + let outbound_resp = Client::new() + .send(outbound_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (parts, mut body) = outbound_resp.into_parts(); + let body_bytes = body.contents().await?; + + let content_type = parts + .headers + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + + let xid_cookie = + format!("{AB_COOKIE}={xid}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"); + + Ok(Response::builder() + .status(parts.status) + .header("content-type", content_type) + .header("set-cookie", xid_cookie) + .body(Body::from(body_bytes))?) +} + +fn extract_abid(cookie_header: &str) -> Option<&str> { + let needle = format!("{AB_COOKIE}="); + cookie_header + .split(';') + .map(str::trim) + .find_map(|p| p.strip_prefix(needle.as_str())) +} + +fn strip_abid(cookie_header: &str) -> String { + let needle = format!("{AB_COOKIE}="); + cookie_header + .split(';') + .map(str::trim) + .filter(|p| !p.is_empty()) + .filter(|p| !p.starts_with(needle.as_str())) + .collect::>() + .join("; ") +} + +fn is_valid_xid(xid: &str) -> bool { + matches!(xid.parse::(), Ok(v) if (0.0..1.0).contains(&v)) +} + +/// Generate a pseudo-random A/B id of the form `"0.NNNN"`. +/// +/// Uses request-time nanoseconds as a weak entropy source. For production, +/// prefer a cryptographic RNG (e.g. `rand` wired to `wasi-random`). +fn generate_xid() -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + format!("0.{:04}", now.subsec_nanos() % 10000) +} + +fn assign_variant(xid: &str, test: &AbTest) -> Option<&'static str> { + let xid_value: f64 = xid.parse().ok()?; + let xid_percentage = xid_value * 100.0; + let total: f64 = test.variants.iter().map(|v| v.weight).sum(); + if total == 0.0 { + return None; + } + let mut start = 0.0; + for vw in test.variants { + let percentage = (vw.weight / total) * 100.0; + let end = start + percentage; + if xid_percentage >= start && xid_percentage < end { + return Some(vw.variant); + } + start = end; + } + None +} +``` + +### FILE: examples/http/wasi/ab_testing/Cargo.toml + +```toml +[workspace] + +[package] +name = "ab_testing_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md index f2b3419..671c3e6 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/base-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-08-17 +updated: 2026-08-20 --- @@ -194,10 +194,8 @@ async fn main(request: Request) -> anyhow::Result> { "Hello, you made a wasi request to {url}" )))?) } - ``` - ### FILE: examples/http/wasi/hello_world/Cargo.toml ```toml @@ -214,5 +212,4 @@ crate-type = ["cdylib"] [dependencies] wstd = "0.6" anyhow = "1" - ``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md index ad66102..c90f65f 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/bloom-filter-denylist-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -163,6 +163,9 @@ async fn main(req: Request) -> anyhow::Result> { .map_err(|e| anyhow!("bf_exists error: {e}"))?; if blocked { + // Bloom filter says "maybe in set" — a small fraction of hits will be false + // positives. Acceptable for a denylist (you over-block some legitimate users); + // not acceptable for allowlists — use `store.get()` against a regular key instead. return json_response(403, json!({ "allowed": false, "ip": ip })); } @@ -190,117 +193,3 @@ fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result) -> anyhow::Result> { - let store_name = match env::var("DENYLIST_STORE") { - Ok(s) if !s.trim().is_empty() => s, - _ => { - return json_response( - 500, - json!({ "error": "DENYLIST_STORE environment variable is not configured" }), - ); - } - }; - - let headers = req.headers(); - let client_ip = headers - .get("x-real-ip") - .or_else(|| headers.get("x-forwarded-for")) - .and_then(|v| v.to_str().ok()) - .and_then(|v| v.split(',').next()) - .map(str::trim) - .filter(|s| !s.is_empty()); - - let Some(ip) = client_ip else { - return json_response(500, json!({ "error": "client IP not available" })); - }; - - let store = match Store::open(&store_name) { - Ok(s) => s, - Err(StoreError::AccessDenied) => { - return json_response( - 403, - json!({ "error": "access denied opening denylist store" }), - ); - } - Err(e) => { - return json_response(500, json!({ "error": format!("store open error: {e}") })); - } - }; - - let blocked = store - .bf_exists(BLOOM_KEY, ip) - .map_err(|e| anyhow!("bf_exists error: {e}"))?; - - if blocked { - // Bloom filter says "maybe in set" — a small fraction of hits will be false - // positives. Acceptable for a denylist (you over-block some legitimate users); - // not acceptable for allowlists — use `store.get()` against a regular key instead. - return json_response(403, json!({ "allowed": false, "ip": ip })); - } - - json_response(200, json!({ "allowed": true, "ip": ip })) -} - -fn json_response(status: u16, value: serde_json::Value) -> anyhow::Result> { - Ok(Response::builder() - .status(status) - .header("content-type", "application/json") - .body(Body::from(value.to_string()))?) -} -``` - -### FILE: examples/http/wasi/bloom_filter_denylist/Cargo.toml - -```toml -[workspace] - -[package] -name = "bloom_filter_denylist_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -serde_json = "1" -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md index 20e88de..78af466 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/cache-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -176,6 +176,18 @@ let body_bytes = body.contents().await?.to_vec(); Must call `.into_body()` before reading; `.contents().await?` buffers the full response body. +### Collect Upstream Headers + +```rust +let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); +``` + +Headers must be collected before consuming the response body via `.into_body()`. + ### Conditional Cache Write ```rust @@ -215,6 +227,7 @@ Purge all example: ```rust if path_and_query == "/purge" { let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); return Ok(Response::builder().status(204).body(Body::empty())?); } ``` @@ -225,6 +238,7 @@ Purge prefix example: if let Some(prefix) = path_and_query.strip_prefix("/purge/") { let prefix = format!("cache:/{prefix}"); let deleted = cache::purge_prefix(&prefix)?; + println!("purge prefix '{prefix}': {deleted} keys removed"); return Ok(Response::builder().status(204).body(Body::empty())?); } ``` @@ -235,6 +249,7 @@ Delete single key example: if let Some(prefix) = path_and_query.strip_prefix("/delete/") { let prefix = format!("cache:/{prefix}"); cache::delete(&prefix)?; + println!("prefix '{prefix}': removed"); return Ok(Response::builder().status(204).body(Body::empty())?); } ``` @@ -273,3 +288,201 @@ cargo build --release - http-base skeleton - outbound-http feature blueprint - platform-overview (environment variable configuration) + +## Source Material + +### FILE: examples/http/wasi/cache/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Example app demonstrating response caching and cache purging via the cache interface. + +The app reads ORIGIN_HOST from the environment, forwards the incoming request +to that origin, and caches the response body keyed by the request path. +On subsequent requests for the same path the cached body is returned directly +without hitting the origin. + +Cache reads and writes use the synchronous `fastedge::cache` API; upstream +HTTP I/O still uses the async `wstd` client. + +Special purge routes (handled before any origin call): + GET /purge — purge all cached keys; returns 200 with deleted count + GET /purge/ — purge keys whose cache key starts with cache:/ + +Environment variables: + ORIGIN_HOST Base URL of the upstream origin, e.g. https://api.example.com + CACHE_TTL_MS How long to cache responses in milliseconds (default: 60000) + +Build: + cargo build --release +*/ + +use std::env; + +use anyhow::anyhow; +use fastedge::cache; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let origin = env::var("ORIGIN_HOST") + .map_err(|_| anyhow!("ORIGIN_HOST environment variable is not set"))?; + + let ttl_ms = req.headers().get("cache-ttl-ms").and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or_else(|| { + env::var("CACHE_TTL_MS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(60_000) + }); + + // Build cache key from the request path (and query string if present) + let path_and_query = req + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + + + // Handle purge requests before any cache/origin logic + if path_and_query == "/purge" { + let deleted = cache::purge()?; + println!("purge all: {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + if let Some(prefix) = path_and_query.strip_prefix("/purge/") { + let prefix = format!("cache:/{prefix}"); + let deleted = cache::purge_prefix(&prefix)?; + println!("purge prefix '{prefix}': {deleted} keys removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + if let Some(prefix) = path_and_query.strip_prefix("/delete/") { + let prefix = format!("cache:/{prefix}"); + cache::delete(&prefix)?; + println!("prefix '{prefix}': removed"); + return Ok(Response::builder() + .status(204) + .body(Body::empty())?); + } + + let cache_key = format!("cache:{path_and_query}"); + + // Return cached response if available + if let Some(cached) = cache::get(&cache_key)? { + println!("cache hit: {cache_key}"); + return Ok(Response::builder() + .status(200) + .header("content-type", "application/octet-stream") + .header("x-cache", "hit") + .body(Body::from(cached))?); + } + + // Cache miss — forward request to origin + let upstream_url = format!("{}{}", origin.trim_end_matches('/'), path_and_query); + println!("cache miss: {cache_key} → {upstream_url}"); + + let upstream_req = Request::get(&upstream_url) + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build upstream request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("upstream request failed: {e}"))?; + + let status = upstream_resp.status(); + let headers: Vec<(String, String)> = upstream_resp + .headers() + .iter() + .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string())) + .collect(); + + // Read body bytes + let mut body = upstream_resp.into_body(); + let body_bytes = body.contents().await?.to_vec(); + + // Only cache successful responses + if status.is_success() { + cache::set(&cache_key, &body_bytes, Some(ttl_ms))?; + } + + // Replay original response + let mut builder = Response::builder() + .status(status) + .header("x-cache", "miss"); + for (k, v) in &headers { + builder = builder.header(k, v); + } + Ok(builder.body(Body::from(body_bytes))?) +} +``` + + +### FILE: examples/http/wasi/cache/Cargo.toml + +```toml +[workspace] + +[package] +name = "cache_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/cache/README.md + +``` +[← Back to examples](../../../README.md) + +# Cache (WASI) + +Demonstrates the cache-aside pattern with origin forwarding using `fastedge::cache`. Forwards incoming requests to `ORIGIN_HOST`, caches successful response bodies keyed by path and query string, and serves cached bytes directly on subsequent matching requests. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `ORIGIN_HOST` | Yes | Base URL of the upstream origin (e.g. `https://api.example.com`). Returns 500 if unset. | +| `CACHE_TTL_MS` | No | How long to cache responses in milliseconds. Default: `60000` (60 s). | + +## How it works + +``` +GET /data?id=1 → cache miss → forward to ORIGIN_HOST/data?id=1 → cache 2xx body → 200 (x-cache: miss) +GET /data?id=1 → cache hit → return cached body → 200 (x-cache: hit) +``` + +Cache key is `cache:?`. Only 2xx responses from the origin are cached — error responses pass through without being stored. The origin's response headers are replayed on cache miss; cache-hit responses use `content-type: application/octet-stream` since the original content-type is not stored alongside the body bytes. + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/cache_wasi.wasm +``` + +## APIs used + +- `fastedge::cache::get(key)` — retrieve cached bytes by key; returns `Ok(Option>)` +- `fastedge::cache::set(key, bytes, ttl_ms)` — store bytes with optional TTL in milliseconds; `None` means no expiry +- `wstd::http::Client::new().send(req).await` — async outbound HTTP request to origin +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md index 64f2d27..c4395fb 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/diagnostic-logging-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -44,8 +44,8 @@ crate-type = ["cdylib"] ## Required Configuration -| Variable | Type | Required | Description | -|--------------|--------|----------|--------------------------------| +| Variable | Type | Required | Description | +|--------------|--------|----------|---------------------------------| | `ORIGIN_URL` | string | yes | Origin URL to proxy requests to | ## API @@ -190,103 +190,3 @@ async fn main(req: Request) -> anyhow::Result> { - http-base skeleton (base handler structure, entry point macro) - platform-overview (log viewer, per-request diagnostics context) - fastedge SDK Rust reference (full `fastedge::utils` API surface) - -## Source Material - -### FILE: examples/http/wasi/diagnostic_logging/src/lib.rs - -```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Diagnostic logging example. - -Tiny pass-through proxy that writes a single `set_user_diag` tag per request -summarising the outcome (config_error, origin_unreachable, or proxied). The -tag appears in the FastEdge platform's per-request log viewer and is distinct -from stdout — it's intended for filterable outcome labels, not verbose -traces. - -Required configuration: - - Environment variable: ORIGIN_URL (origin to proxy to) - -Uses `logfmt`-ish formatting (`outcome= key=value ...`) so the tag is -easy to slice in log search tooling. -*/ - -use std::env; - -use fastedge::utils::set_user_diag; -use wstd::http::body::Body; -use wstd::http::{Client, Request, Response}; - -#[wstd::http_server] -async fn main(req: Request) -> anyhow::Result> { - let method = req.method().as_str().to_string(); - let path = req.uri().path().to_string(); - - let origin = match env::var("ORIGIN_URL") { - Ok(u) if !u.trim().is_empty() => u, - _ => { - set_user_diag("outcome=config_error reason=origin_missing"); - return Ok(Response::builder() - .status(500) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("ORIGIN_URL is not configured"))?); - } - }; - - let outbound = Request::get(&origin).body(Body::empty())?; - let resp = match Client::new().send(outbound).await { - Ok(r) => r, - Err(e) => { - set_user_diag(&format!( - "outcome=origin_unreachable method={method} path={path} err={e}" - )); - return Ok(Response::builder() - .status(502) - .header("content-type", "text/plain; charset=utf-8") - .body(Body::from("origin unreachable"))?); - } - }; - - let status = resp.status().as_u16(); - set_user_diag(&format!( - "outcome=proxied method={method} path={path} status={status}" - )); - - let (parts, mut body) = resp.into_parts(); - let bytes = body.contents().await?; - let content_type = parts - .headers - .get("content-type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/octet-stream") - .to_string(); - - Ok(Response::builder() - .status(parts.status) - .header("content-type", content_type) - .body(Body::from(bytes))?) -} -``` - -### FILE: examples/http/wasi/diagnostic_logging/Cargo.toml - -```toml -[workspace] - -[package] -name = "diagnostic_logging_wasi" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wstd = "0.6" -fastedge = "0.4" -anyhow = "1" -``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md index 85ad7d9..4554a90 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/geo-redirect-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -154,3 +154,119 @@ cargo build --release - http-base skeleton reference - FastEdge platform overview (geoip-country-code header injection) - deploy skill reference (uploading WASM binary and setting environment variables) + +## Source Material + +### FILE: examples/http/wasi/geo_redirect/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example WASI-HTTP app demonstrating geo-based redirects. + +Reads the country code from the geoip-country-code request header +and redirects to a country-specific origin URL. Falls back to +BASE_ORIGIN when no country-specific mapping is configured. + +Required configuration: + - Environment variable: BASE_ORIGIN (fallback origin URL) + - Environment variable: (optional per-country origin URLs, e.g. US, DE, GB) +*/ + +use std::env; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let base_origin = match env::var("BASE_ORIGIN") { + Ok(origin) => origin, + Err(_) => { + return Ok(Response::builder() + .status(500) + .body(Body::from("BASE_ORIGIN is not set"))?); + } + }; + + let country_code = req + .headers() + .get("geoip-country-code") + .and_then(|v| v.to_str().ok()) + .unwrap_or("") + .to_string(); + + let redirect_origin = if !country_code.is_empty() { + env::var(&country_code).unwrap_or(base_origin) + } else { + base_origin + }; + + Ok(Response::builder() + .status(302) + .header("location", &redirect_origin) + .body(Body::empty())?) +} +``` + + +### FILE: examples/http/wasi/geo_redirect/Cargo.toml + +```toml +[workspace] + +[package] +name = "geo_redirect_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/geo_redirect/README.md + +``` +[← Back to examples](../../../README.md) + +# Geo Redirect (WASI) + +Redirects requests to country-specific origins based on the `geoip-country-code` request header. Falls back to `BASE_ORIGIN` when no country-specific mapping is configured. + +Demonstrates reading request headers, reading environment variables, and returning redirect responses. + +## Configuration + +| Env var | Required | Description | +|---|---|---| +| `BASE_ORIGIN` | Yes | Fallback redirect URL (e.g. `https://example.com`). Returns 500 if unset. | +| `` | No | Per-country redirect URL, keyed by 2-letter country code (e.g. `DE`, `US`, `GB`). Falls back to `BASE_ORIGIN` if not set. | + +## How it works + +``` +geoip-country-code: DE → env var DE is set → 302 to DE value +geoip-country-code: FR → env var FR not set → 302 to BASE_ORIGIN +(no header) → 302 to BASE_ORIGIN +BASE_ORIGIN not set → 500 +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/geo_redirect_wasi.wasm +``` + +## APIs used + +- `request.headers().get("geoip-country-code")` — read geo header injected by the FastEdge edge +- `std::env::var(country_code)` — dynamic env var lookup by country code +- `Response::builder().status(302).header("location", url)` — redirect response +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md index 4cc23df..1adeb05 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/headers-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-wasi-rust.md index f66ef74..a23be87 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/kv-store-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -250,10 +250,10 @@ The app accepts these query parameters: | Parameter | Required | Description | |-----------|----------|-------------| -| `store` | Yes | Name of the KV store to open | -| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | -| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | -| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | +| `store` | Yes | Name of the KV store to open | +| `action` | No | One of: `get`, `scan`, `zscan`, `zrange`, `bfExists` (default: `get`) | +| `key` | Varies | Key to access (required for `get`, `zrange`, `zscan`, `bfExists`) | +| `match` | Varies | Prefix match pattern, e.g. `foo*` (required for `scan`, `zscan`) | | `min` | Yes for `zrange` | Minimum score bound (float); no default — must be provided | | `max` | Yes for `zrange` | Maximum score bound (float); no default — must be provided | | `item` | Yes for `bfExists` | Item to check in Bloom Filter | @@ -350,3 +350,218 @@ target = "wasm32-wasip1" - fastedge-sdk-rust key_value module documentation - http-base skeleton reference - wstd HTTP server documentation + +## Source Material + +### FILE: examples/http/wasi/key_value/src/lib.rs + +```rust +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating KV Store operations via the WASI-HTTP interface. + +Supports all KV Store operations via query parameters: + ?store=&action=get&key= + ?store=&action=scan&match= + ?store=&action=zrange&key=&min=&max= + ?store=&action=zscan&key=&match= + ?store=&action=bfExists&key=&item= + +Defaults to action=get if not specified. +*/ + +use std::collections::HashMap; + +use anyhow::anyhow; +use fastedge::key_value::{Store, Error as StoreError}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(req: Request) -> anyhow::Result> { + let query = req.uri().query().ok_or(anyhow!("no query parameters"))?; + let params: HashMap<&str, &str> = querystring::querify(query).into_iter().collect(); + + let store_name = *params + .get("store") + .ok_or(anyhow!("missing param 'store'"))?; + + let action = params.get("action").copied().unwrap_or("get"); + + let store = match Store::open(store_name) { + Ok(s) => s, + Err(StoreError::AccessDenied) => { + return Ok(Response::builder() + .status(403) + .header("content-type", "application/json") + .body(Body::from(json!({"error": "access denied"}).to_string()))?); + } + Err(e) => { + return Ok(Response::builder() + .status(500) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("store open error: {e}")}).to_string()))?); + } + }; + + let body = match action { + "get" => handle_get(&store, ¶ms)?, + "scan" => handle_scan(&store, ¶ms)?, + "zrange" => handle_zrange(&store, ¶ms)?, + "zscan" => handle_zscan(&store, ¶ms)?, + "bfExists" => handle_bf_exists(&store, ¶ms)?, + _ => { + return Ok(Response::builder() + .status(400) + .header("content-type", "application/json") + .body(Body::from(json!({"error": format!("Invalid action '{action}'. Supported: get, scan, zrange, zscan, bfExists")}).to_string()))?); + } + }; + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(body))?) +} + +fn handle_get(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + match store.get(key) { + Ok(Some(value)) => { + let value_str = String::from_utf8_lossy(&value); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": value_str.as_ref() + }).to_string()) + } + Ok(None) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "get", + "key": key, + "response": null + }).to_string()), + Err(e) => Err(anyhow!("KV get error: {e}")), + } +} + +fn handle_scan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + match store.scan(pattern) { + Ok(keys) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "scan", + "match": pattern, + "response": keys + }).to_string()), + Err(e) => Err(anyhow!("KV scan error: {e}")), + } +} + +fn handle_zrange(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let min: f64 = params + .get("min") + .ok_or(anyhow!("missing param 'min'"))? + .parse() + .map_err(|_| anyhow!("invalid 'min': must be a number"))?; + let max: f64 = params + .get("max") + .ok_or(anyhow!("missing param 'max'"))? + .parse() + .map_err(|_| anyhow!("invalid 'max': must be a number"))?; + + match store.zrange_by_score(key, min, max) { + Ok(entries) => { + let entries_json: Vec = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zrange", + "key": key, + "min": min, + "max": max, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zrange error: {e}")), + } +} + +fn handle_zscan(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let pattern = *params + .get("match") + .ok_or(anyhow!("missing param 'match'"))?; + + match store.zscan(key, pattern) { + Ok(entries) => { + let entries_json: Vec = entries + .iter() + .map(|(value, score)| { + let value_str = String::from_utf8_lossy(value); + json!({"value": value_str.as_ref(), "score": score}) + }) + .collect(); + Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "zscan", + "key": key, + "match": pattern, + "response": entries_json + }).to_string()) + } + Err(e) => Err(anyhow!("KV zscan error: {e}")), + } +} + +fn handle_bf_exists(store: &Store, params: &HashMap<&str, &str>) -> anyhow::Result { + let key = *params.get("key").ok_or(anyhow!("missing param 'key'"))?; + let item = *params + .get("item") + .ok_or(anyhow!("missing param 'item'"))?; + + match store.bf_exists(key, item) { + Ok(exists) => Ok(json!({ + "store": params.get("store").unwrap_or(&""), + "action": "bfExists", + "key": key, + "item": item, + "response": exists + }).to_string()), + Err(e) => Err(anyhow!("KV bfExists error: {e}")), + } +} +``` + +### FILE: examples/http/wasi/key_value/Cargo.toml + +```toml +[workspace] + +[package] +name = "key_value_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +querystring = "1.1" +serde_json = "1" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md index 6c86644..c8849da 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/large-env-variable-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md index c1a13e7..aeaede1 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -127,3 +127,90 @@ Both errors propagate via `?` and result in a 500-class response from the FastEd - `outbound-modify-response` (WASI, Rust) — fetches upstream and reshapes the body into a new JSON response - `streaming` (WASI, Rust) — handler that generates its own streaming response without an upstream fetch - `outbound-fetch` (JavaScript) — mirror of this example in the FastEdge SDK JS + +## Source Material + +### FILE: examples/http/wasi/outbound_fetch/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Minimal outbound fetch example. + +Makes a GET request to an upstream HTTP origin and returns the upstream +response verbatim — status, headers, and body pass through unchanged. + +For a variant that reads and transforms the upstream body, see +`outbound_modify_response/`. For a streaming-response demo, see `streaming/`. + +Mirror of the FastEdge-sdk-js `outbound-fetch` example. +*/ + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + // Return the upstream response verbatim. The body is passed through + // without calling `.contents()`, so it streams to the client as upstream + // produces it. + let (parts, body) = upstream_resp.into_parts(); + let mut response = Response::new(body); + *response.status_mut() = parts.status; + *response.headers_mut() = parts.headers; + Ok(response) +} +``` + + +### FILE: examples/http/wasi/outbound_fetch/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_fetch" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/outbound_fetch/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Fetch (WASI) + +Fetch data from an outbound HTTP origin and return the response directly — status, headers, +and body pass through unchanged. + +The body is never buffered (no `.contents().await`), so upstream chunks stream to the client +as they arrive. + +## Related + +- [outbound_modify_response](../outbound_modify_response/) — same fetch, but reads the body + and reshapes it into a new JSON response. +- [streaming](../streaming/) — a handler that generates its own streaming response body. +- Mirror of `FastEdge-sdk-js/examples/outbound-fetch/`. +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md index 9d4a66e..c33ea71 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/outbound-modify-response-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -216,3 +216,82 @@ async fn main(_request: Request) -> anyhow::Result> { - outbound-fetch-wasi-rust (simpler variant — passes upstream response through unchanged) - http-base skeleton (base handler structure, `#[wstd::http_server]`, `Body`, `Request`, `Response`) - sdk-reference-rust (full `wstd` API surface) + +## Source Material + +### FILE: examples/http/wasi/outbound_modify_response/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Outbound fetch with response transformation. + +Fetches JSON from an upstream origin, reads and parses the body, reshapes it +into a new JSON object (first 5 users with pagination metadata), and returns +it with a fresh `content-type: application/json` header. + +This is the stepping-stone beyond `outbound_fetch/` which just passes the +upstream response through unchanged. + +Mirror of the FastEdge-sdk-js `outbound-modify-response` example. +*/ + +use anyhow::anyhow; +use serde_json::{Value, json}; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let upstream_req = Request::get("http://jsonplaceholder.typicode.com/users") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let upstream_resp = Client::new() + .send(upstream_req) + .await + .map_err(|e| anyhow!("outbound request failed: {e}"))?; + + let (_, mut body) = upstream_resp.into_parts(); + let body_bytes = body.contents().await?; + let users: Value = serde_json::from_slice(body_bytes)?; + + let sliced_users = match users.as_array() { + Some(arr) => Value::Array(arr.iter().take(5).cloned().collect()), + None => Value::Array(vec![]), + }; + + let result = json!({ + "users": sliced_users, + "total": 5, + "skip": 0, + "limit": 30, + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "application/json") + .body(Body::from(result.to_string()))?) +} +``` + +### FILE: examples/http/wasi/outbound_modify_response/Cargo.toml + +```toml +[workspace] + +[package] +name = "outbound_modify_response_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +serde_json = "1" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md index 7bbfb60..17c459e 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/secret-rollover-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md index fc98f25..cf34807 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/simple-fetch-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -131,6 +131,19 @@ use wstd::http::{Client, Request, Response}; | Missing `x-fetch-url` header | Falls back to `https://httpbin.org/get` | | Non-UTF-8 header value | `to_str().ok()` returns `None`; fallback default applied | +## Build + +```bash +cargo component build --release +# Output: target/wasm32-wasip2/release/simple_fetch.wasm +``` + +## Example Usage + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + ## Source Material ### FILE: examples/http/wasi/simple_fetch/src/lib.rs diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-wasi-rust.md index fe01ad1..1eecd03 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/static-assets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -32,6 +32,7 @@ include_str!("../assets/filename.ext") - Embeds a UTF-8 text file into the binary at compile time. - Path is relative to the source file containing the macro. - Returns `&'static str`. +- Build fails if the target file does not exist at compile time. ```rust include_bytes!("../assets/filename.ext") @@ -127,6 +128,13 @@ async fn main(req: Request) -> anyhow::Result> { } ``` +## Imports + +```rust +use wstd::http::body::Body; +use wstd::http::{Request, Response, StatusCode}; +``` + ## Cargo.toml ```toml @@ -165,6 +173,7 @@ All files in `assets/` must exist at compile time; the build will fail if `inclu - Asset paths in `include_str!` are relative to the `.rs` source file, not the crate root. - All assets are compiled into the WASM binary — large assets increase binary size proportionally. - No dynamic file loading is possible at runtime; adding an asset requires a rebuild. +- The WASM runtime has no filesystem; there is no alternative to compile-time embedding. ## See Also diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-wasi-rust.md index e011ba1..956f361 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/streaming-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -141,3 +141,67 @@ async fn main(_request: Request) -> anyhow::Result> { - FastEdge-sdk-rust HTTP examples (other HTTP feature blueprints) - wstd crate documentation (Runtime, Body, Timer APIs) - futures-lite crate documentation (stream combinators, unfold) + +## Source Material + +### FILE: examples/http/wasi/streaming/src/lib.rs + +```rust +/* + * Copyright 2025 G-Core Innovations SARL + */ +/* +Streaming response example. + +Generates a response body on the fly — five text chunks, one every 200ms — +using `Body::from_stream` backed by a `futures_lite::Stream`. The runtime +polls the stream as the body is sent, so chunks flow to the client as they +are produced instead of all at once at the end. + +Watch it stream with `curl -N https:///` (`-N` disables client-side +buffering). + +Mirror of the FastEdge-sdk-js `streaming` example. +*/ + +use futures_lite::stream; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; +use wstd::time::{Duration, Timer}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let chunk_stream = stream::unfold(0u32, |i| async move { + if i >= 5 { + return None; + } + Timer::after(Duration::from_millis(200)).wait().await; + Some((format!("chunk {i}\n"), i + 1)) + }); + + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from_stream(chunk_stream))?) +} +``` + + +### FILE: examples/http/wasi/streaming/Cargo.toml + +```toml +[workspace] + +[package] +name = "streaming_wasi" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" +futures-lite = "1" +``` diff --git a/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md b/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md index c23693c..00d44d9 100644 --- a/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md +++ b/plugins/gcore-fastedge/skills/scaffold/reference/http/variables-and-secrets-wasi-rust.md @@ -4,7 +4,7 @@ - id: fastedge-sdk-rust ref: main commit: 6347a7c2fda0d03e66f1214db5eec041c16801b7 - updated: 2026-08-17 + updated: 2026-08-20 --> --- @@ -135,3 +135,89 @@ cargo build --release - deploy skill reference (uploading the compiled `.wasm` binary) - manage skill reference (setting environment variables and secrets on an app) - FastEdge platform overview (secret storage model) + +## Source Material + +### FILE: examples/http/wasi/variables_and_secrets/src/lib.rs + +```rust +use fastedge::secret; +use std::env; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[wstd::http_server] +async fn main(_request: Request) -> anyhow::Result> { + let username = env::var("USERNAME").unwrap_or_default(); + let password = match secret::get("PASSWORD") { + Ok(Some(value)) => value, + _ => String::new(), + }; + + Ok(Response::builder() + .status(200) + .body(Body::from(format!( + "Username: {username}, Password: {password}" + )))?) +} +``` + + +### FILE: examples/http/wasi/variables_and_secrets/Cargo.toml + +```toml +[workspace] + +[package] +name = "variables_and_secrets" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = "0.4" +anyhow = "1" +``` + + +### FILE: examples/http/wasi/variables_and_secrets/README.md + +``` +[← Back to examples](../../../README.md) + +# Variables and Secrets (WASI) + +Demonstrates reading an environment variable (`USERNAME`) and a secret (`PASSWORD`), returning both in the response body. + +Environment variables are set via the FastEdge app configuration and accessed with `std::env::var`. Secrets are stored encrypted and accessed with `fastedge::secret::get` — they are never exposed in platform logs or configuration UIs. + +## Configuration + +| Key | Type | Required | Description | +|---|---|---|---| +| `USERNAME` | Environment variable | No | Username to include in response. Empty string if unset. | +| `PASSWORD` | Secret | No | Password to include in response. Empty string if unset or unavailable. | + +## What it returns + +``` +HTTP/1.1 200 OK + +Username: , Password: +``` + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/variables_and_secrets.wasm +``` + +## APIs used + +- `std::env::var("USERNAME").unwrap_or_default()` — read env var with fallback +- `fastedge::secret::get("PASSWORD")` — read secret by name; returns `Ok(Some(String))` on success +```