From 236d570604e79251736c4576aba44ec70baf1d04 Mon Sep 17 00:00:00 2001 From: "fastedge-plugin-sync[bot]" Date: Mon, 17 Aug 2026 13:27:02 +0000 Subject: [PATCH] auto: update reference docs from fastedge-sdk-rust (main) --- .../fastedge-docs/reference/cdn-apps-rust.md | 2 +- .../reference/cdn/examples-ab-testing-rust.md | 190 +++++++++- .../reference/cdn/examples-api-key-rust.md | 2 +- .../reference/cdn/examples-auth-jwt-rust.md | 2 +- .../reference/cdn/examples-body-rust.md | 2 +- .../cdn/examples-cache-control-rust.md | 2 +- .../cdn/examples-convert-image-rust.md | 299 +-------------- .../reference/cdn/examples-cors-rust.md | 2 +- .../cdn/examples-custom-error-pages-rust.md | 71 +++- .../reference/cdn/examples-custom-rust.md | 122 +++++- .../cdn/examples-env-secrets-rust.md | 2 +- .../cdn/examples-geo-redirect-rust.md | 2 +- .../reference/cdn/examples-geoblock-rust.md | 2 +- .../reference/cdn/examples-headers-rust.md | 355 +++++++++++++++++- .../reference/cdn/examples-http-call-rust.md | 65 +--- .../reference/cdn/examples-kv-store-rust.md | 2 +- .../cdn/examples-large-dictionary-rust.md | 2 +- .../reference/cdn/examples-log-time-rust.md | 2 +- .../reference/cdn/examples-md2html-rust.md | 4 +- .../reference/cdn/examples-properties-rust.md | 2 +- .../reference/host-services-rust.md | 2 +- .../http/examples-ab-testing-wasi-rust.md | 2 +- .../http/examples-api-wrapper-basic-rust.md | 2 +- .../http/examples-backend-basic-rust.md | 113 +----- ...xamples-bloom-filter-denylist-wasi-rust.md | 161 +++++++- .../http/examples-cache-basic-rust.md | 2 +- .../http/examples-cache-wasi-rust.md | 196 +++++++++- .../examples-diagnostic-logging-wasi-rust.md | 151 +++++++- .../http/examples-geo-redirect-wasi-rust.md | 116 +++++- .../http/examples-headers-wasi-rust.md | 2 +- .../http/examples-hello-world-basic-rust.md | 2 +- .../http/examples-hello-world-wasi-rust.md | 76 +++- .../http/examples-kv-store-wasi-rust.md | 276 +++++++++++++- .../examples-large-env-variable-wasi-rust.md | 2 +- .../examples-markdown-render-basic-rust.md | 13 +- .../examples-outbound-fetch-basic-rust.md | 119 +++++- .../http/examples-outbound-fetch-wasi-rust.md | 2 +- ...ples-outbound-modify-response-wasi-rust.md | 101 ++++- .../http/examples-print-basic-rust.md | 2 +- .../http/examples-s3upload-basic-rust.md | 3 +- .../http/examples-secret-basic-rust.md | 184 ++++++++- .../examples-secret-rollover-wasi-rust.md | 2 +- .../http/examples-simple-fetch-wasi-rust.md | 122 +++++- .../http/examples-smart-switch-basic-rust.md | 2 +- .../http/examples-static-assets-wasi-rust.md | 2 +- .../http/examples-streaming-wasi-rust.md | 104 ++++- ...xamples-variables-and-secrets-wasi-rust.md | 2 +- .../http/examples-watermark-basic-rust.md | 2 +- .../reference/quickstart-rust.md | 2 +- .../reference/sdk-reference-rust.md | 2 +- .../scaffold/reference/cdn/ab-testing-rust.md | 2 +- .../scaffold/reference/cdn/api-key-rust.md | 121 +++++- .../scaffold/reference/cdn/auth-jwt-rust.md | 141 +------ .../scaffold/reference/cdn/base-rust.md | 108 +----- .../scaffold/reference/cdn/body-rust.md | 124 +++++- .../reference/cdn/cache-control-rust.md | 143 +------ .../reference/cdn/convert-image-rust.md | 301 ++++++++++++++- .../scaffold/reference/cdn/cors-rust.md | 2 +- .../reference/cdn/custom-error-pages-rust.md | 206 +++++++++- .../scaffold/reference/cdn/custom-rust.md | 122 +++++- .../reference/cdn/env-secrets-rust.md | 103 ++++- .../reference/cdn/geo-redirect-rust.md | 15 +- .../scaffold/reference/cdn/geoblock-rust.md | 113 +++++- .../scaffold/reference/cdn/headers-rust.md | 20 +- .../scaffold/reference/cdn/http-call-rust.md | 2 +- .../scaffold/reference/cdn/kv-store-rust.md | 2 +- .../reference/cdn/large-dictionary-rust.md | 110 +----- .../scaffold/reference/cdn/log-time-rust.md | 2 +- .../scaffold/reference/cdn/md2html-rust.md | 2 +- .../scaffold/reference/cdn/properties-rust.md | 2 +- .../reference/http/ab-testing-wasi-rust.md | 213 +---------- .../scaffold/reference/http/base-rust.md | 46 ++- .../http/bloom-filter-denylist-wasi-rust.md | 116 +++++- .../reference/http/cache-wasi-rust.md | 200 +--------- .../http/diagnostic-logging-wasi-rust.md | 102 ++++- .../reference/http/geo-redirect-wasi-rust.md | 118 +----- .../reference/http/headers-wasi-rust.md | 4 +- .../reference/http/kv-store-wasi-rust.md | 2 +- .../http/large-env-variable-wasi-rust.md | 2 +- .../http/outbound-fetch-wasi-rust.md | 89 +---- .../outbound-modify-response-wasi-rust.md | 81 +--- .../http/secret-rollover-wasi-rust.md | 152 ++------ .../reference/http/simple-fetch-wasi-rust.md | 2 +- .../reference/http/static-assets-wasi-rust.md | 2 +- .../reference/http/streaming-wasi-rust.md | 66 +--- .../http/variables-and-secrets-wasi-rust.md | 88 +---- 86 files changed, 3813 insertions(+), 2010 deletions(-) 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 6d77329..277e59d 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-07-23 + updated: 2026-08-17 --> # FastEdge Rust SDK — CDN Apps (Proxy-Wasm) 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 60d90b9..0205b81 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-07-23 + updated: 2026-08-17 --> --- @@ -248,3 +248,191 @@ 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 d9b973e..1de9cd9 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-07-23 + updated: 2026-08-17 --> # API Key Validation — CDN (Rust) 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 f7d7f39..5d68f6c 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-07-23 + updated: 2026-08-17 --> --- 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 6fefb13..e927177 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-07-23 + updated: 2026-08-17 --> --- 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 1fb5df0..7b9f6ae 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-07-23 + updated: 2026-08-17 --> --- 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 f36b937..baedbd0 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-07-23 + updated: 2026-08-17 --> # CDN Example: Convert Image (Rust) @@ -202,300 +202,3 @@ 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 297a27c..6da1290 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-07-23 + updated: 2026-08-17 --> --- 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 1fe604d..90a1205 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-07-23 + updated: 2026-08-17 --> ## Custom Error Pages — CDN (Rust) @@ -13,6 +13,8 @@ Intercepts 4xx and 5xx HTTP error responses at the CDN edge and replaces their bodies with branded HTML pages rendered via Handlebars templates. Use this pattern when you need consistent, styled error pages served from the edge regardless of what the origin returns. +A build script (`build.rs`) runs at compile time to embed images and messages from the `public/` folder into the WASM binary — there is no filesystem at runtime. + --- ### API Patterns @@ -188,6 +190,29 @@ let (message, description) = message_map }); ``` +**Pattern 4 — Root context and HTTP context wiring:** + +```rust +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)) + } +} +``` + --- ### Dependencies (`Cargo.toml`) @@ -203,11 +228,40 @@ regex = "1.10" base64 = "0.22" ``` -- `proxy-wasm`: CDN lifecycle hooks and context traits -- `handlebars`: template rendering for error pages and message strings -- `serde_json`: JSON data construction for template variables -- `regex`: available for pattern matching (used in build script or message processing) -- `base64` (build dependency only): encodes images into the generated map at compile time +| Crate | Role | +|-------|------| +| `proxy-wasm` | CDN lifecycle hooks and context traits | +| `handlebars` | Template rendering for error pages and message strings | +| `serde_json` | JSON data construction for template variables | +| `regex` | Pattern matching (available for use in build script or message processing) | +| `base64` (build-dep only) | Encodes images into the generated map at compile time | + +Crate type must be `cdylib`: + +```toml +[lib] +crate-type = ["cdylib"] +``` + +--- + +### Project Layout + +``` +custom_error_pages/ +├── build.rs # Generates image_map.rs and message_map.rs into OUT_DIR +├── Cargo.toml +├── src/ +│ └── lib.rs # HttpContext implementation +├── templates/ +│ └── error_page.hbs # Handlebars HTML page template +└── public/ + ├── styles.css # Embedded at compile time via include_str! + ├── images/ + │ └── .jpg # Per-status images; also 4000.jpg / 5000.jpg for fallbacks + └── messages/ + └── .hbs # First line = title, second line = description +``` --- @@ -215,7 +269,7 @@ base64 = "0.22" 1. Add an image: `public/images/.jpg` 2. Add a message file: `public/messages/.hbs` (first line = title, second line = description) -3. Recompile and redeploy — the build script regenerates the embedded maps +3. Recompile and redeploy — the build script regenerates the embedded maps automatically --- @@ -230,10 +284,11 @@ base64 = "0.22" - **Handlebars rendering is two-stage.** Message and description strings may themselves contain `{{status}}` placeholders. Render them first with `json!({ "status": ... })`, then pass the rendered strings into the final page template. Registering and rendering in a single pass will not expand variables inside message/description values. - **`handlebars::Handlebars::render` panics on template registration failure if `.unwrap()` is used.** In production code, handle `register_template_string` and `render` errors explicitly unless the templates are known-valid at compile time. - **`get_property` returns `Option>` in both hooks.** The property may be absent even for valid responses. Always handle the `None` case before attempting to decode. +- **Generic fallback keys use sentinel values, not HTTP status ranges.** The image and message maps use `4000` (not `400`) for the generic 4xx fallback and `5000` (not `500`) for the generic 5xx fallback. These are not valid HTTP status codes — they are sentinel keys used only in the embedded maps. --- -### Related +### See Also - CDN apps reference — proxy-wasm lifecycle hooks (`on_http_response_headers`, `on_http_response_body`), `HttpContext` trait, `Action` enum, and `get_property` / `set_http_response_header` / `set_http_response_body` signatures - Host services reference — KV store, secrets, and dictionary APIs available in CDN apps 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 bbfd2a1..ec7dc83 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-07-23 + updated: 2026-08-17 --> --- @@ -250,3 +250,123 @@ 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 77ca37b..0d8d824 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-07-23 + updated: 2026-08-17 --> --- 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 2f876e9..fb251dd 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-07-23 + updated: 2026-08-17 --> --- 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 ef70e4b..8c66081 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-07-23 + updated: 2026-08-17 --> --- 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 b327ddc..97aea43 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-07-23 + updated: 2026-08-17 --> --- @@ -230,3 +230,356 @@ 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 b2bf6a4..3959d1b 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-07-23 + updated: 2026-08-17 --> # HTTP Call — CDN (Rust) @@ -244,27 +244,7 @@ on_http_call_response(token_id, num_headers, body_size, _) --- -## Gotchas - -- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. -- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. -- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. -- 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`. - ---- - -## See Also - -- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) -- CDN app scaffold blueprint -- FastEdge platform overview -- FastEdge error codes reference - -## Source Material - -### FILE: examples/cdn/http_call/src/lib.rs +## Complete Example ```rust use proxy_wasm::traits::*; @@ -305,7 +285,6 @@ impl Context for HttpHeaders { println!( "Received http call response with token id: {token_id}, num_headers: {num_headers}" ); - //If num_headers is 0, then the HTTP call failed. if num_headers != 0 { let headers = self.get_http_call_response_headers(); let headers_str = headers @@ -315,7 +294,7 @@ impl Context for HttpHeaders { .join(","); println!("Response headers: [{}]", headers_str); - self.state = 1; // Set state to 1 to indicate that the HTTP call response was received successfully. + self.state = 1; self.resume_http_request(); } else { @@ -376,32 +355,22 @@ fn to_status_code(status: Status) -> u32 { } ``` +--- -### FILE: examples/cdn/http_call/Cargo.toml - -```toml -[workspace] - -[package] -name = "http_call" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -log = "0.4" -proxy-wasm = "0.2" -``` - +## Gotchas -### FILE: examples/cdn/http_call/README.md +- `Action::Pause` must be returned from `on_http_request_headers` after dispatching — failing to pause allows the request to proceed before the async response arrives. +- State must be tracked in struct fields (e.g. `state: u32`) because `on_http_call_response` and `on_http_request_headers` execute in separate hook invocations on the same context instance. +- `timeout` uses `Duration::from_millis()`; passing zero may cause immediate failure depending on runtime behavior. +- 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`. -``` -[← Back to examples](../../README.md) +--- -# HTTP Call (CDN) +## See Also -Makes asynchronous HTTP calls to external services with timeout handling using the proxy-wasm ABI. -``` +- proxy-wasm Rust SDK reference (traits: Context, RootContext, HttpContext) +- CDN app scaffold blueprint +- FastEdge platform overview +- FastEdge error codes reference 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 c6d043a..4fe50fc 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-07-23 + updated: 2026-08-17 --> --- 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 e992be6..78af977 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-07-23 + updated: 2026-08-17 --> --- 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 c5d1d11..934c352 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-07-23 + updated: 2026-08-17 --> --- 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 eecbdf0..e88ed80 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-07-23 + updated: 2026-08-17 --> --- @@ -65,7 +65,7 @@ self.send_http_response(BAD_REQUEST, vec![], None); **Steps**: 1. Reads `Content-Type` response header via `self.get_http_response_header("Content-Type")`. -2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. +2. Checks if value starts with `"text/plain"` or `"text/markdown"`. Both trigger Markdown conversion. Detection uses `starts_with`, so parameters (e.g. `text/plain; charset=utf-8`) are also matched. 3. If matched: - Removes `Content-Length` by setting to `None` (required before body replacement to avoid length mismatch). - Sets `Transfer-Encoding` to `"Chunked"`. 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 40af407..575b2ab 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-07-23 + updated: 2026-08-17 --> --- 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 ac21c92..92a6ebf 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-07-23 + updated: 2026-08-17 --> # 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 1eb2c2d..1640164 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-07-23 + updated: 2026-08-17 --> --- 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 66f0876..27532f3 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-07-23 + updated: 2026-08-17 --> # 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 1dd6bda..d754a7e 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-07-23 + updated: 2026-08-17 --> --- @@ -153,114 +153,3 @@ 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 c836e97..21d90c6 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-07-23 + updated: 2026-08-17 --> --- @@ -150,3 +150,162 @@ 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 ab82a99..b723ff8 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-07-23 + updated: 2026-08-17 --> --- 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 28ff740..4193334 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-07-23 + updated: 2026-08-17 --> --- @@ -188,3 +188,197 @@ cargo build --release - HTTP outbound requests via wstd (sdk-reference-rust) - Environment variable configuration (platform-overview) - HTTP app deploy workflow (deploy skill) + +## 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 + +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/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 99f2107..fe37566 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-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,152 @@ Use `key=value` pairs separated by spaces (`logfmt`-ish format). Benefits: - host-services-rust (other host service integrations available to Rust WASI apps) - CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, different module path (see CDN apps reference) - examples-kv-store-wasi-rust (another Rust WASI HTTP example showing KV Store usage) + +## 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" +``` + + +### FILE: examples/http/wasi/diagnostic_logging/README.md + +``` +[← Back to examples](../../../README.md) + +# Diagnostic Logging (WASI) + +Pass-through proxy that writes a single `fastedge::utils::set_user_diag` tag per request +summarising the outcome. The tag appears in the FastEdge platform's per-request log viewer, +distinct from stdout, and is designed to be filtered/counted/aggregated by SREs looking at +per-request outcomes. + +## Configuration + +- `ORIGIN_URL` environment variable — the origin that requests are proxied to. + +## Outcomes + +Each request writes exactly one of: + +| Condition | Tag | +| --- | --- | +| `ORIGIN_URL` missing | `outcome=config_error reason=origin_missing` | +| Origin unreachable | `outcome=origin_unreachable method= path=

err=` | +| Request proxied | `outcome=proxied method= path=

status=` | + +## `set_user_diag` vs `println!` + +| | `println!` | `set_user_diag` | +| --- | --- | --- | +| Channel | stdout — general application logs | per-request structured tag in platform log viewer | +| Cardinality | many per request | **one per request** — multiple calls leave only the last or are concatenated (undefined) | +| Best for | verbose traces, debug details | a single filterable outcome label | +| Forbidden | — | secrets and PII (tags appear in platform logs) | + +## Convention + +Call `set_user_diag` **once**, on every branch, late enough in the handler to know the +outcome. The `logfmt`-ish format (`outcome= key=value key=value …`) is easy to slice in +log search tooling — keep keys short and stable so they make good filter terms. + +## Related + +CDN (proxy-wasm) variant: `fastedge::proxywasm::utils::set_user_diag` — same semantics, +different module path. See `docs/CDN_APPS.md`. +``` 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 2a9166c..41b44c6 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-07-23 + updated: 2026-08-17 --> --- @@ -145,3 +145,117 @@ cargo build --release - host-services-rust — `wstd` HTTP types: `Request`, `Response`, `Body` - examples-headers-wasi-rust — general header reading patterns - examples-env-vars-wasi-rust — environment variable access patterns + +## 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/fastedge-docs/reference/http/examples-headers-wasi-rust.md b/plugins/gcore-fastedge/skills/fastedge-docs/reference/http/examples-headers-wasi-rust.md index 715bd2c..fe47695 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-07-23 + updated: 2026-08-17 --> --- 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 4a78f63..68f0752 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-07-23 + updated: 2026-08-17 --> --- 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 8b6fef2..2f17c5e 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-07-23 + updated: 2026-08-17 --> --- @@ -133,3 +133,77 @@ 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 bc4dd90..3766914 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-07-23 + updated: 2026-08-17 --> --- @@ -378,3 +378,277 @@ 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 dee4d5c..e48ca3b 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-07-23 + updated: 2026-08-17 --> # 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 43e3c89..f98adf3 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-07-23 + updated: 2026-08-17 --> --- @@ -51,14 +51,14 @@ Only `GET` and `HEAD` are accepted. ## Core Flow -1. Read `BASE` env var; strip trailing `/`. +1. Read `BASE` env var; strip trailing `/` via `base.trim_end_matches('/')`. 2. Validate request path is non-empty and not `/`. 3. Construct outbound `GET` request: `BASE + path`, `User-Agent: fastedge`. 4. Call `fastedge::send_request` via internal `request()` helper. -5. Follow redirects up to `MAX_REDIRECTS = 5` hops. -6. Decode response body as UTF-8; failure → `500`. -7. Parse Markdown with `Parser::new_ext(md, Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. -8. Render HTML with `pulldown_cmark::html::push_html`. +5. Follow redirects up to `MAX_REDIRECTS = 5` hops via `request_inner(req, depth)`. +6. Decode response body as UTF-8 via `String::from_utf8(rsp.body().to_vec())`; failure → `500`. +7. Parse Markdown with `Parser::new_ext(md.as_str(), Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES)`. +8. Render HTML with `pulldown_cmark::html::push_html(&mut html, parser)`. 9. Wrap in `...`; optionally inject `HEAD` env var into ``. 10. Return `200 OK`, `Content-Type: text/html`. @@ -193,6 +193,7 @@ Build target: `wasm32-wasip1`. - `String::from_utf8(rsp.body().to_vec())` fails on binary responses (e.g. images) — returns `500`. Ensure `BASE` points to a text/Markdown origin. - Redirect following is implemented manually; `fastedge::send_request` does not auto-follow redirects. - `HEAD` env var content is injected as raw HTML — no escaping or validation. Only inject trusted content. +- `request_inner` only returns `Ok(rsp)` for `200 OK` responses; all other non-redirect status codes are returned as `Err(status)` and forwarded as empty-body responses. --- 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 142235f..f25c8e7 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-07-23 + updated: 2026-08-17 --> --- @@ -186,3 +186,120 @@ 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 334764f..051e70d 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-07-23 + updated: 2026-08-17 --> # 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 5666f87..6be9762 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-07-23 + updated: 2026-08-17 --> --- @@ -182,3 +182,102 @@ Ok(Response::builder() - outbound-modify-response example (JS) - wstd HTTP client documentation - serde_json crate documentation + +## 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" +``` + +### FILE: examples/http/wasi/outbound_modify_response/README.md + +``` +[← Back to examples](../../../README.md) + +# Outbound Modify Response (WASI) + +Fetch data from an outbound HTTP origin, transform the JSON response (slice to first 5 +users), and return it with a fresh `content-type: application/json` header. + +Demonstrates reading the upstream body with `body.contents().await`, parsing JSON with +`serde_json`, and composing a new response from scratch. + +## Related + +- [outbound_fetch](../outbound_fetch/) — the simpler variant that just passes the upstream + response through unchanged. +- Mirror of `FastEdge-sdk-js/examples/outbound-modify-response/`. +``` 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 0b6ff0c..31ed106 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-07-23 + updated: 2026-08-17 --> --- 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 ac1fdd7..2f2a35a 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-07-23 + updated: 2026-08-17 --> --- @@ -171,6 +171,7 @@ cargo build --release - `MAX_FILE_SIZE` is silently ignored (no error, no log) when set to a non-numeric string. - On S3 success, the response body is replaced entirely with the clean object URL — the original S3 response body is discarded. - The `UrlStyle::Path` style is used for `Bucket::new` — bucket name appears in the URL path, not the hostname. +- Query params (`?name=...`) are parsed manually using `split('&')` and `splitn(2, '=')` into a `HashMap`. --- 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 d97a52d..3d9715b 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-07-23 + updated: 2026-08-17 --> # Secret Access — Rust HTTP Example @@ -251,3 +251,185 @@ 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 ac3b308..483e53a 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-07-23 + updated: 2026-08-17 --> # Secret Rollover (WASI, Rust) 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 824cf69..3370f1b 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-07-23 + updated: 2026-08-17 --> --- @@ -147,3 +147,123 @@ 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 4be6846..8c84135 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-07-23 + updated: 2026-08-17 --> --- 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 a1fc6c3..4093ad9 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-07-23 + updated: 2026-08-17 --> --- 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 7584018..b8e56f8 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-07-23 + updated: 2026-08-17 --> --- @@ -143,3 +143,105 @@ 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 dc3161e..84f7561 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-07-23 + updated: 2026-08-17 --> --- 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 af5e2c2..4585b12 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-07-23 + updated: 2026-08-17 --> # 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 d3423fe..85ac605 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-07-23 + updated: 2026-08-17 --> # 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 bce5118..7b86940 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-07-23 + updated: 2026-08-17 --> # 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 b0f3438..ba5b29f 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-07-23 + updated: 2026-08-17 --> --- 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 4c21047..4e444bf 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-07-23 + updated: 2026-08-17 --> --- @@ -212,3 +212,122 @@ 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 f92ce4c..28c7eda 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-07-23 + updated: 2026-08-17 --> --- @@ -259,142 +259,3 @@ 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 7b47146..d4f417a 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-07-23 + updated: 2026-08-17 --> --- @@ -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-07-23 +updated: 2026-08-17 --- # Base Skeleton: CDN Rust @@ -165,29 +165,6 @@ lerna-debug.log* .history/* ``` -## Logging Convention - -FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: - -1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. -2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - -**Do not use:** -- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime -- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer -- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - -If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - -Example pattern for CDN Rust: -```rust -fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - log::info!("incoming request"); // visible in FastEdge logs - // eprintln!("incoming request"); // DO NOT use — dropped - Action::Continue -} -``` - ## Build Configuration ```bash @@ -209,76 +186,25 @@ cargo build --release --target wasm32-wasip1 - **RootContext**: implements `get_type() -> Option` returning `ContextType::HttpContext` and `create_http_context(_: u32) -> Option>` - **HttpContext**: all 4 hooks return `Action::Continue` in the base skeleton; `on_http_response_headers` adds `x-powered-by: FastEdge` response header via `self.add_http_response_header` -## 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; +## Logging Convention -impl Context for HelloWorld {} +FastEdge captures **stdout only**. CDN Rust apps have two stdout-safe options — pick one and stay consistent: -impl HttpContext for HelloWorld { - fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_headers"); - Action::Continue - } +1. **`log` crate macros via proxy-wasm** (used in the base skeleton above): `log::info!`, `log::warn!`, `log::error!`, `log::debug!`, `log::trace!`. The `proxy_wasm::main!` macro wires these through the proxy-wasm host ABI (`log_message` import), which the FastEdge runtime routes to stdout. Already configured by the base skeleton via `proxy_wasm::set_log_level(LogLevel::Trace)`. +2. **Direct `println!` / `print!`** — writes straight to stdout. Works, but unconventional for CDN filters; prefer the `log` crate macros for consistency with proxy-wasm idioms. - fn on_http_request_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_request_body"); - Action::Continue - } +**Do not use:** +- `eprintln!` / `eprint!` — write to stderr, silently dropped by the runtime +- `writeln!(std::io::stderr(), …)` or any direct `std::io::stderr()` writer +- `env_logger` with its default configuration — defaults to stderr; the `log` facade is already wired by proxy-wasm, so do not initialize a competing backend - 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 - } +If a log line does not appear when running the visual debugger against a fixture, it is on stderr and will be invisible in production too. - fn on_http_response_body(&mut self, _: usize, _: bool) -> Action { - info!("Hello from on_http_response_body"); - Action::Continue - } +Example pattern for CDN Rust: +```rust +fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action { + log::info!("incoming request"); // visible in FastEdge logs + // eprintln!("incoming request"); // DO NOT use — dropped + 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 725bf65..e8b9ba2 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-07-23 + updated: 2026-08-17 --> --- @@ -243,3 +243,125 @@ 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 25517cf..b49ed2a 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-07-23 + updated: 2026-08-17 --> --- @@ -219,144 +219,3 @@ 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 9ce892f..69d0e83 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-07-23 + updated: 2026-08-17 --> --- @@ -191,7 +191,7 @@ fn on_http_response_body(&mut self, body_size: usize, end_of_stream: bool) -> Ac ## Helper: `rsp_status` -Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. +Decodes the `response.status` property, which is returned as a 2-byte big-endian `u16`. Defined as an `impl ConvertImageContext` method. ```rust fn rsp_status(&mut self) -> Option { @@ -284,3 +284,300 @@ 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 2ae30f6..019bcb3 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-07-23 + updated: 2026-08-17 --> --- 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 1c410d6..d8d3907 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-07-23 + updated: 2026-08-17 --> --- @@ -310,3 +310,207 @@ 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 99d95cb..de7cb9f 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-07-23 + updated: 2026-08-17 --> --- @@ -260,3 +260,123 @@ 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 598249f..9336819 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-07-23 + updated: 2026-08-17 --> --- @@ -175,3 +175,104 @@ 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 1fac82b..dab63e7 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-07-23 + updated: 2026-08-17 --> --- @@ -46,7 +46,7 @@ All routing logic executes in `on_http_request_headers`. No body hooks are used. 5. Read `request.path` property; default to `"/"` if absent. 6. Preserve the `Host` header by reading `request.host` and calling `set_http_request_header`. 7. Construct the target URL as `format!("{}{}", origin, path)`. -8. Log the target URL at `Info` level. +8. Log the target URL at `Info` level via `println!`. 9. Write the URL to `request.url` via `set_property` and return `Action::Continue`. ### Country Detection @@ -125,6 +125,8 @@ crate-type = ["cdylib"] proxy-wasm = "0.2" ``` +Note: the workspace-level `[workspace]` key is present in the source Cargo.toml but omitted here as it is not relevant to the library package configuration. + ## Struct Layout | Struct | Traits | Role | @@ -135,6 +137,15 @@ proxy-wasm = "0.2" `GeoRedirectRoot::get_type` returns `Some(ContextType::HttpContext)`. `GeoRedirectRoot::create_http_context` returns `Some(Box::new(GeoRedirectContext))`. +## Entry Point + +```rust +proxy_wasm::main! {{ + proxy_wasm::set_log_level(LogLevel::Info); + proxy_wasm::set_root_context(|_| -> Box { Box::new(GeoRedirectRoot) }); +}} +``` + ## See Also - cdn-base skeleton reference 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 031afb2..189abf0 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-07-23 + updated: 2026-08-17 --> --- @@ -155,7 +155,7 @@ Configure these in the FastEdge dashboard under the app's environment variables: | `BLACKLIST_TW_START` | No | Unix timestamp (u64) — start of time window during which blocking applies | | `BLACKLIST_TW_END` | No | Unix timestamp (u64) — end of time window during which blocking applies | -If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. +If both `BLACKLIST_TW_START` and `BLACKLIST_TW_END` are set, the block only applies during that time window (`now >= tw_start && now <= tw_end`). If neither is set, the block is permanent for all listed countries. If only one of the two is set, it is treated as absent and the block is permanent. ## Error Conditions @@ -206,3 +206,112 @@ 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 6fc0857..f2b6798 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-07-23 + updated: 2026-08-17 --> --- @@ -246,14 +246,14 @@ let headers = self.get_http_request_headers() .collect::>(); // After add/set operations, expected new entries include: -// ("new-header-01", "") // removed → empty string -// ("new-header-bytes-01", "") // removed → empty string -// ("new-header-02", "new-value-02") // replaced -// ("new-header-bytes-02", "new-value-bytes-02") // replaced -// ("new-header-03", "value-03") // original add -// ("new-header-bytes-03", "value-bytes-03") // original add -// ("new-header-03", "value-03-a") // duplicate add preserved -// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved +// ("new-header-01", "") // removed → empty string +// ("new-header-bytes-01", "") // removed → empty string +// ("new-header-02", "new-value-02") // replaced +// ("new-header-bytes-02", "new-value-bytes-02") // replaced +// ("new-header-03", "value-03") // original add +// ("new-header-bytes-03", "value-bytes-03") // original add +// ("new-header-03", "value-03-a") // duplicate add preserved +// ("new-header-bytes-03", "value-bytes-03-a") // duplicate add preserved let diff = headers .difference(&original_headers) @@ -265,7 +265,7 @@ if !diff.is_empty() { } ``` -The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. +The same diff pattern applies to bytes variants using `get_http_request_headers_bytes()` and a `HashSet<(String, Bytes)>`. The response phase (`on_http_response_headers`) applies an identical add/set/diff sequence using response header methods. --- 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 f03ef41..c9efc2c 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-07-23 + updated: 2026-08-17 --> --- 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 451f32a..6a0769b 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-07-23 + updated: 2026-08-17 --> --- 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 925690d..a8334a3 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-07-23 + updated: 2026-08-17 --> --- @@ -153,111 +153,3 @@ 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 e1cea4c..a39c42e 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-07-23 + updated: 2026-08-17 --> --- 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 3f071b6..ef3b630 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-07-23 + updated: 2026-08-17 --> --- 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 a007b08..a76a8e3 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-07-23 + updated: 2026-08-17 --> --- 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 49f2ccf..9f1a6a0 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-07-23 + updated: 2026-08-17 --> --- @@ -217,214 +217,3 @@ 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 a8d1e14..f2b3419 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-07-23 + updated: 2026-08-17 --> --- @@ -14,7 +14,7 @@ languages: [rust] template_origin: http-base source_repo: fastedge-sdk-rust source_ref: 6347a7c2fda0d03e66f1214db5eec041c16801b7 -updated: 2026-07-23 +updated: 2026-08-17 --- @@ -174,3 +174,45 @@ cargo build --release --target wasm32-wasip2 - **Handler signature**: `async fn main(request: Request) -> anyhow::Result>` - **Crate type**: `cdylib` (required for WASM output) - **Workspace**: Single-project workspace pattern (`[workspace]` declared in Cargo.toml) + +## 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" + +``` 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 07dd858..ad66102 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-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,117 @@ 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 f08fffb..20e88de 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-07-23 + updated: 2026-08-17 --> --- @@ -273,201 +273,3 @@ 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 0710498..64f2d27 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-07-23 + updated: 2026-08-17 --> --- @@ -190,3 +190,103 @@ 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 d8d4004..85ad7d9 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-07-23 + updated: 2026-08-17 --> --- @@ -154,119 +154,3 @@ 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 8c5712a..4cc23df 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-07-23 + updated: 2026-08-17 --> --- @@ -164,7 +164,6 @@ async fn main(request: Request) -> anyhow::Result> { } ``` - ### FILE: examples/http/wasi/headers/Cargo.toml ```toml @@ -183,7 +182,6 @@ wstd = "0.6" anyhow = "1" ``` - ### FILE: examples/http/wasi/headers/README.md ``` 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 02ee4c4..f66ef74 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-07-23 + updated: 2026-08-17 --> --- 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 0b2a974..6c86644 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-07-23 + updated: 2026-08-17 --> --- 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 871a96b..c1a13e7 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-07-23 + updated: 2026-08-17 --> --- @@ -127,90 +127,3 @@ 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 fb8121f..9d4a66e 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-07-23 + updated: 2026-08-17 --> --- @@ -216,82 +216,3 @@ 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 c5cb2b8..7bbfb60 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-07-23 + updated: 2026-08-17 --> --- @@ -42,6 +42,15 @@ get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" Slot `0` serves as the baseline — always matched when no higher slot qualifies. +Usage as indices: +``` +get_effective_at("token-secret", 0) -> "original_password" +get_effective_at("token-secret", 3) -> "original_password" +get_effective_at("token-secret", 5) -> slot 5's value (if exists) +``` + +Usage as timestamps: a token's `iat` claim determines which password to validate against. `get_effective_at("token-secret", claims.iat)` returns the password that was effective when the token was issued. + ## API Reference ### `secret::get` @@ -158,6 +167,12 @@ let slot: u32 = request .as_secs() as u32 }); +let secret_name = request + .headers() + .get("x-secret-name") + .and_then(|v| v.to_str().ok()) + .unwrap_or("TOKEN_SECRET"); + // Current (latest) value let current = secret::get(secret_name) .map_err(|e| anyhow!("secret::get failed: {e}"))?; @@ -165,59 +180,19 @@ let current = secret::get(secret_name) // 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}"))?; -``` -## Build - -```sh -cargo build --release -# Output: target/wasm32-wasip2/release/secret_rollover.wasm +let result = json!({ + "secret_name": secret_name, + "slot": slot, + "current": current, + "effective_at_slot": effective, + "is_same": current == effective, +}); ``` -## See Also - -- fastedge::secret module reference (Rust SDK reference) -- http-base skeleton (base HTTP app structure) -- platform-overview (secret management configuration) -- best-practices (secret rotation strategies) - -## Source Material - -### FILE: examples/http/wasi/secret_rollover/src/lib.rs +## Full Source ```rust -/* - * Copyright 2025 G-Core Innovations SARL - */ -/* -Secret rollover example using slot-based secret retrieval. - -Demonstrates how to use `secret::get_effective_at()` with slots to support -secret rotation. Slots use a greatest matching rule: the slot with the highest -value that is <= the requested `effective_at` is returned. - -Example secret configuration: -{ - "secret": { - "name": "token-secret", - "secret_slots": [ - { "slot": 0, "value": "original_password" }, - { "slot": 1741790697, "value": "new_password" } - ] - } -} - -Usage as indices: - get_effective_at("token-secret", 0) -> "original_password" - get_effective_at("token-secret", 3) -> "original_password" - get_effective_at("token-secret", 5) -> slot 5's value (if exists) - -Usage as timestamps: - A token's `iat` claim determines which password to validate against. - get_effective_at("token-secret", claims.iat) returns the password - that was effective when the token was issued. -*/ - use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::anyhow; @@ -228,7 +203,6 @@ use wstd::http::{Request, Response}; #[wstd::http_server] async fn main(request: Request) -> anyhow::Result> { - // Read the slot from the x-slot header, defaulting to current timestamp let slot: u32 = request .headers() .get("x-slot") @@ -247,10 +221,8 @@ async fn main(request: Request) -> anyhow::Result> { .and_then(|v| v.to_str().ok()) .unwrap_or("TOKEN_SECRET"); - // Get the current secret value (latest slot) let current = secret::get(secret_name).map_err(|e| anyhow!("secret::get failed: {e}"))?; - // Get the secret effective at the requested slot let effective = secret::get_effective_at(secret_name, slot) .map_err(|e| anyhow!("secret::get_effective_at failed: {e}"))?; @@ -269,73 +241,6 @@ async fn main(request: Request) -> anyhow::Result> { } ``` - -### FILE: examples/http/wasi/secret_rollover/Cargo.toml - -```toml -[workspace] - -[package] -name = "secret_rollover" -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/secret_rollover/README.md - -``` -[← Back to examples](../../../README.md) - -# Secret Rollover (WASI) - -Demonstrates slot-based secret retrieval for secret rotation scenarios using `secret::get_effective_at()`. - -Compares the current secret value with the value effective at a given slot, returning both as JSON. This lets you validate that rotation is working correctly before removing the old slot. - -## Usage - -| Request header | Default | Description | -|---|---|---| -| `x-secret-name` | `TOKEN_SECRET` | Name of the secret to query | -| `x-slot` | current Unix timestamp | Slot value passed to `get_effective_at` | - -## How slots work - -Slots use a **greatest-match rule**: the slot with the highest value that is `<= effective_at` is returned. - -``` -Secret slots: { 0: "old-password", 1741790697: "new-password" } - -get_effective_at("TOKEN_SECRET", 0) → "old-password" -get_effective_at("TOKEN_SECRET", 100) → "old-password" -get_effective_at("TOKEN_SECRET", 1741790697) → "new-password" -get_effective_at("TOKEN_SECRET", 9999999999) → "new-password" -``` - -When used with token `iat` (issued-at) timestamps, `get_effective_at(name, claims.iat)` returns the password that was active when the token was issued — enabling zero-downtime rotation without invalidating existing tokens. - -## What it returns - -```json -{ - "secret_name": "TOKEN_SECRET", - "slot": 0, - "current": "new-password", - "effective_at_slot": "old-password", - "is_same": false -} -``` - ## Build ```sh @@ -343,8 +248,9 @@ cargo build --release # Output: target/wasm32-wasip2/release/secret_rollover.wasm ``` -## APIs used +## See Also -- `fastedge::secret::get(name)` — current (latest-slot) secret value; `Ok(Option)` -- `fastedge::secret::get_effective_at(name, slot)` — secret value at a given slot; `Ok(Option)` -``` +- fastedge::secret module reference (Rust SDK reference) +- http-base skeleton (base HTTP app structure) +- platform-overview (secret management configuration) +- best-practices (secret rotation strategies) 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 f9fc9d2..fc98f25 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-07-23 + updated: 2026-08-17 --> --- 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 e859b3e..fe01ad1 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-07-23 + updated: 2026-08-17 --> --- 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 765e332..e011ba1 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-07-23 + updated: 2026-08-17 --> --- @@ -141,67 +141,3 @@ 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 c06bc83..c23693c 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-07-23 + updated: 2026-08-17 --> --- @@ -135,89 +135,3 @@ 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 -```