From 09f5511876d80b7f2c3a34d7212c1a3b6dde4f48 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 11 Sep 2025 11:23:00 +0100 Subject: [PATCH 01/17] Initial work --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + include/fastly/esi.h | 36 ++++++++++++++++++++++++++++++++++++ src/esi.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 ++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 include/fastly/esi.h create mode 100644 src/esi.rs diff --git a/Cargo.lock b/Cargo.lock index 0e701a1..f3df208 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "esi" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aec2576e951a80a55f7cbb541149afa271f58a5e3d70d82d45c90e7bc939298" +dependencies = [ + "fastly", + "html-escape", + "log", + "quick-xml", + "regex", + "thiserror 2.0.12", +] + [[package]] name = "fastly" version = "0.11.5" @@ -283,6 +297,7 @@ version = "0.3.0" dependencies = [ "cxx", "cxx-build", + "esi", "fastly", "fastly-shared", "http", @@ -341,6 +356,15 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "1.3.1" @@ -582,6 +606,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -881,6 +914,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 91410b6..7b141f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ http = "1.3.1" log = "0.4.27" log-fastly = "0.11.5" thiserror = "2.0.12" +esi = "0.6.1" [build-dependencies] cxx-build = "1.0" diff --git a/include/fastly/esi.h b/include/fastly/esi.h new file mode 100644 index 0000000..1ff0b11 --- /dev/null +++ b/include/fastly/esi.h @@ -0,0 +1,36 @@ +#ifndef FASTLY_ESI_H +#define FASTLY_ESI_H + +#include + +namespace fastly::esi +{ + /// Used to configure optional behaviour within the ESI processor. + struct Configuration + { + public: + /// Create a new configuration object. + /// \param namespc The namespace to use for ESI tags. Defaults to "esi". + /// \param is_escaped_content Whether to escape content by default. Defaults to true. + Configuration(std::string namespc = "esi", bool is_escaped_content = true) + : namespace_(std::move(namespc)), is_escaped_content_(is_escaped_content) {} + + std::string_view get_namespace() const { return namespace_; } + bool is_escaped_content() const { return is_escaped_content_; } + + private: + std::string namespace_; + bool is_escaped_content_; + }; + + class Processor + { + public: + /// Create a new ESI processor with the given configuration. + explicit Processor(std::optional original_request_metadata = std::nullopt, + Configuration config = Configuration()) + { + } + } + +#endif \ No newline at end of file diff --git a/src/esi.rs b/src/esi.rs new file mode 100644 index 0000000..bea67a8 --- /dev/null +++ b/src/esi.rs @@ -0,0 +1,40 @@ +use std::pin::Pin; + +use cxx::CxxString; +use esi::Configuration; + +use crate::{ + http::{request::Request, response::Response}, + try_fe, +}; + +pub(crate) struct Processor(esi::Processor); + +impl Processor { + pub fn process_response( + self, + src_document: &mut Response, + client_response_metadata: *mut Box, + dispatch_fragment_request: Option<&dyn Fn(Request) -> Result>, + process_fragment_response: Option<&dyn Fn(&mut Request, Response) -> Result>, + ) -> Result<()> { + } +} + +pub fn m_static_esi_processor_new( + original_request_metadata: *mut Box, + namespace: &CxxString, + is_escaped_content: bool, +) -> Box { + let original_request_metadata = if original_request_metadata.is_null() { + None + } else { + Some(unsafe { original_request_metadata.read().0 }) + }; + Box::new(Processor(esi::Processor::new( + original_request_metadata, + Configuration::default() + .with_escaped(is_escaped_content) + .with_namespace(namespace.to_string()), + ))) +} diff --git a/src/lib.rs b/src/lib.rs index 2f6d456..a0cfca9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,13 @@ use http::{ use kv_store::*; use log::*; use secret_store::*; +use esi::*; mod backend; mod config_store; mod device_detection; mod error; +mod esi; mod geo; mod http; mod kv_store; @@ -1018,4 +1020,14 @@ mod ffi { mut err: Pin<&mut *mut KVStoreError>, ) -> bool; } + + #[namespace = "fastly::sys::esi"] + extern "Rust" { + type Processor; + pub unsafe fn m_static_esi_processor_new( + original_request_metadata: *mut Box, + namespace: &CxxString, + is_escaped_content: bool +) -> Box; + } } From 0ee3f2c9b11abb8acc469f83e977998529e84cdd Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 11 Sep 2025 11:32:36 +0100 Subject: [PATCH 02/17] More testing --- include/fastly/esi.h | 23 ++++++++++++++++++++++- src/lib.rs | 23 ++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/include/fastly/esi.h b/include/fastly/esi.h index 1ff0b11..ef449c8 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -2,6 +2,13 @@ #define FASTLY_ESI_H #include +#include +#include +#include +#include +#include +#include +#include namespace fastly::esi { @@ -31,6 +38,20 @@ namespace fastly::esi Configuration config = Configuration()) { } - } + }; + + class DispatchFragmentRequestFn + { + public: + DispatchFragmentRequestFn(std::function(Request &)> fn) + : fn_(std::move(fn)) {} + expected call(Request &req) const noexcept + { + return fn_(req); + } + private: + std::function(Request &)> fn_; + }; +} #endif \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a0cfca9..cd0c54a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use backend::*; use config_store::*; use device_detection::*; use error::*; +use esi::*; use geo::*; use http::{ body::*, header::*, purge::*, request::request::*, request::*, response::*, status_code::*, @@ -13,7 +14,6 @@ use http::{ use kv_store::*; use log::*; use secret_store::*; -use esi::*; mod backend; mod config_store; @@ -1021,13 +1021,26 @@ mod ffi { ) -> bool; } + #[namespace = "fastly::sys::esi"] + unsafe extern "C++" { + include!("fastly/esi.h"); + type DispatchFragmentRequestFn; + } + #[namespace = "fastly::sys::esi"] extern "Rust" { type Processor; + pub fn m_esi_processor_process_response( + processor: Box, + src_document: &mut Response, + client_response_metadata: *mut Box, + dispatch_fragment_request: DispatchFragmentRequestFn, + process_fragment_response: ProcessFragmentResponseFn, + ) -> Result<()>; pub unsafe fn m_static_esi_processor_new( - original_request_metadata: *mut Box, - namespace: &CxxString, - is_escaped_content: bool -) -> Box; + original_request_metadata: *mut Box, + namespace: &CxxString, + is_escaped_content: bool, + ) -> Box; } } From 7de06ddddc1312bf53028b581ba63db6b538e9ad Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 11 Sep 2025 13:28:26 +0100 Subject: [PATCH 03/17] Slowly getting there --- include/fastly/esi.h | 26 ++++++++------ src/cpp/esi.cpp | 37 +++++++++++++++++++ src/esi.rs | 86 +++++++++++++++++++++++++++++++++++++++----- src/lib.rs | 32 ++++++++++++++--- 4 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 src/cpp/esi.cpp diff --git a/include/fastly/esi.h b/include/fastly/esi.h index ef449c8..88ee0d4 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -34,24 +34,30 @@ namespace fastly::esi { public: /// Create a new ESI processor with the given configuration. - explicit Processor(std::optional original_request_metadata = std::nullopt, - Configuration config = Configuration()) - { - } + Processor(std::optional original_request_metadata = std::nullopt, + Configuration config = Configuration()); + + tl::expected process_response( + Response &src_document, + std::optional client_response_metadata = std::nullopt, + std::optional dispatch_fragment_request = std::nullopt, + std::function(Request &, Response &)> + process_fragment_response = nullptr) + + private : rust::Box processor_; }; + using PendingFragmentContent = std::variant; + class DispatchFragmentRequestFn { public: - DispatchFragmentRequestFn(std::function(Request &)> fn) + DispatchFragmentRequestFn(std::function(Request)> fn) : fn_(std::move(fn)) {} - expected call(Request &req) const noexcept - { - return fn_(req); - } + uint32_t call(Request req, PendingResponse *&out_pending, Response *&out_complete, ExecutionError *&out_error) const noexcept; private: - std::function(Request &)> fn_; + std::function(Request)> fn_; }; } #endif \ No newline at end of file diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp new file mode 100644 index 0000000..00ae037 --- /dev/null +++ b/src/cpp/esi.cpp @@ -0,0 +1,37 @@ +#include + +namespace fastly::esi +{ + + Processor::Processor(std::optional original_request_metadata = std::nullopt, + Configuration config = Configuration()) + { + processor_ = fastly::sys::esi::m_esi_processor_new(original_request_metadata.has_value() ? &original_request_metadata->req : nullptr, + static_cast(config.get_namespace()), + config.is_escaped_content()); + } + + uint32_t DispatchFragmentRequestFn::call(Request req, http::PendingResponse *&out_pending, http::Response *&out_complete, ExecutionError *&out_error) const noexcept; + { + auto res = fn_(std::move(req)); + if (!res) + { + out_error = new ExecutionError(res.error()); + return 3; // Error + } + if (std::holds_alternative(*res)) + { + out_pending = new PendingResponse(std::get(*res)); + return 0; // Pending response + } + else if (std::holds_alternative(*res)) + { + out_complete = new Response(std::get(*res)); + return 1; // Complete Response + } + else + { + return 2; // No content + } + } +} \ No newline at end of file diff --git a/src/esi.rs b/src/esi.rs index bea67a8..856ebbb 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -1,23 +1,91 @@ -use std::pin::Pin; +use std::{ + pin::{self, Pin}, + ptr, + str::ParseBoolError, +}; use cxx::CxxString; use esi::Configuration; use crate::{ + error::FastlyError, + ffi::{DispatchFragmentRequestFn, ProcessFragmentResponseFn}, http::{request::Request, response::Response}, try_fe, }; +pub(crate) struct ExecutionError(esi::ExecutionError); + pub(crate) struct Processor(esi::Processor); -impl Processor { - pub fn process_response( - self, - src_document: &mut Response, - client_response_metadata: *mut Box, - dispatch_fragment_request: Option<&dyn Fn(Request) -> Result>, - process_fragment_response: Option<&dyn Fn(&mut Request, Response) -> Result>, - ) -> Result<()> { +pub fn m_esi_processor_process_response( + processor: Box, + src_document: &mut Response, + client_response_metadata: *mut Box, + dispatch_fragment_request: *const DispatchFragmentRequestFn, + process_fragment_response: *const ProcessFragmentResponseFn, + mut err: Pin<&mut *mut ExecutionError>, +) -> bool { + let client_response_metadata = if client_response_metadata.is_null() { + None + } else { + Some(unsafe { client_response_metadata.read().0 }) + }; + let dispatch_fragment_request = if dispatch_fragment_request.is_null() { + None + } else { + let func = unsafe { dispatch_fragment_request.read() }; + let shim: Box< + dyn Fn(fastly::Request) -> Result, + > = + Box::new(move |req| { + let mut out_pending = ptr::null_mut(); + let mut out_completed = ptr::null_mut(); + let mut err = ptr::null_mut(); + let result = func.call( + Box::new(Request(req)), + &mut out_pending, + &mut out_completed, + &mut err, + ); + match result { + 0 => Ok(unsafe { + esi::PendingFragmentContent::PendingRequest(out_pending.read().0) + }), + 1 => Ok(unsafe { + esi::PendingFragmentContent::CompletedRequest(out_completed.read().0) + }), + 2 => Ok(esi::PendingFragmentContent::NoContent), + 3 => Err(unsafe { err.read().0 }), + _ => unreachable!(), + } + }); + Some(shim) + }; + let process_fragment_response = if process_fragment_response.is_null() { + None + } else { + let shim: &dyn Fn( + &mut fastly::Request, + fastly::Response, + ) -> Result = + &|req, resp| Err(esi::ExecutionError::UnexpectedEndOfDocument); + Some(shim) + }; + match processor.0.process_response( + &mut src_document.0, + client_response_metadata, + dispatch_fragment_request.as_deref(), + process_fragment_response, + ) { + Ok(_) => { + err.set(ptr::null_mut()); + true + } + Err(e) => { + err.set(Box::into_raw(Box::new(ExecutionError(e)))); + false + } } } diff --git a/src/lib.rs b/src/lib.rs index cd0c54a..d86de7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1021,22 +1021,46 @@ mod ffi { ) -> bool; } + #[namespace = "fastly::sys::esi"] + extern "Rust" { + type ExecutionError; + } + #[namespace = "fastly::sys::esi"] unsafe extern "C++" { include!("fastly/esi.h"); type DispatchFragmentRequestFn; + // The return value is: + // 0 on pending + // 1 on completed + // 2 on no content + // 3 on error + fn call( + &self, + req: Box, + out_pending: &mut *mut PendingRequest, + out_completed: &mut *mut Response, + err: &mut *mut ExecutionError, + ) -> u32; + } + + #[namespace = "fastly::sys::esi"] + unsafe extern "C++" { + include!("fastly/esi.h"); + type ProcessFragmentResponseFn; } #[namespace = "fastly::sys::esi"] extern "Rust" { type Processor; - pub fn m_esi_processor_process_response( + pub unsafe fn m_esi_processor_process_response( processor: Box, src_document: &mut Response, client_response_metadata: *mut Box, - dispatch_fragment_request: DispatchFragmentRequestFn, - process_fragment_response: ProcessFragmentResponseFn, - ) -> Result<()>; + dispatch_fragment_request: *const DispatchFragmentRequestFn, + process_fragment_response: *const ProcessFragmentResponseFn, + mut err: Pin<&mut *mut ExecutionError>, + ) -> bool; pub unsafe fn m_static_esi_processor_new( original_request_metadata: *mut Box, namespace: &CxxString, From 433f4e01b65b98df64a8a71871b3b4a703e6d574 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 11 Sep 2025 16:38:48 +0100 Subject: [PATCH 04/17] Getting there --- CMakeLists.txt | 3 + examples/esi.cpp | 65 + examples/fastly.toml | 8 +- include/fastly/callbacks.h | 0 .../fastly/detail/access_bridge_internals.h | 28 + include/fastly/esi.h | 56 +- include/fastly/http/request.h | 1432 +++++++++-------- include/fastly/http/response.h | 1050 ++++++------ src/cpp/esi.cpp | 91 +- src/esi.rs | 57 +- src/lib.rs | 44 +- 11 files changed, 1531 insertions(+), 1303 deletions(-) create mode 100644 examples/esi.cpp create mode 100644 include/fastly/callbacks.h create mode 100644 include/fastly/detail/access_bridge_internals.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f952fbc..3457aa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,9 @@ add_custom_command( target_include_directories(fastly-thin PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_BINARY_DIR}/include) +target_include_directories(fastly-sys PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + ${CMAKE_CURRENT_BINARY_DIR}/include) target_link_libraries(fastly-thin PUBLIC fastly-sys-orig fastly::sys) diff --git a/examples/esi.cpp b/examples/esi.cpp new file mode 100644 index 0000000..2475251 --- /dev/null +++ b/examples/esi.cpp @@ -0,0 +1,65 @@ +#include "fastly/sdk.h" + +const auto html = R"( + + + My Shopping Website + + +
+

My Shopping Website

+
+
+ + + + +
+ + +)"; + +auto to_string(fastly::http::Method method) +{ + switch (method) + { + case fastly::http::Method::GET: + return "GET"; + case fastly::http::Method::POST: + return "POST"; + case fastly::http::Method::PUT: + return "PUT"; + case fastly::http::Method::DELETE: + return "DELETE"; + default: + return "UNKNOWN"; + } +} + +int main() +{ + fastly::log::init_simple("logs"); + fastly::log::info("Starting ESI example"); + auto req{fastly::http::Request::from_client()}; + auto beresp = fastly::http::Response::from_body(html).with_content_type("text/html"); + fastly::esi::Processor processor(std::move(req)); + fastly::esi::DispatchFragmentRequestFn dispatch_fragment_request([](fastly::http::Request req) -> tl::expected + { + fastly::log::info("Sending request {} {}", to_string(req.get_method()), req.get_path()); + auto pending = std::move(req).with_ttl(120).send_async("mock-s3"); + if (pending) + { + return fastly::esi::PendingFragmentContent{std::move(*pending)}; + } + else + { + return tl::unexpected(fastly::esi::ExecutionError{}); + } }); + fastly::log::info("Processor created"); + fastly::esi::ProcessFragmentResponseFn process_fragment_response([](fastly::http::Request &req, fastly::http::Response resp) -> tl::expected + { + fastly::log::info("Received response for {} {}", to_string(req.get_method()), req.get_path()); + + return resp; }); + (void)processor.process_response(beresp, std::nullopt, dispatch_fragment_request, process_fragment_response); +} diff --git a/examples/fastly.toml b/examples/fastly.toml index fd8a642..721ee7b 100644 --- a/examples/fastly.toml +++ b/examples/fastly.toml @@ -10,4 +10,10 @@ service_id = "" [local_server] backends.fastly.url = "https://www.fastly.com" -backends.wikipedia.url = "https://en.wikipedia.org" \ No newline at end of file +backends.wikipedia.url = "https://en.wikipedia.org" + +[setup] +[setup.backends] +[setup.backends.mock-s3] +address = "mock-s3.edgecompute.app" +port = 443 diff --git a/include/fastly/callbacks.h b/include/fastly/callbacks.h new file mode 100644 index 0000000..e69de29 diff --git a/include/fastly/detail/access_bridge_internals.h b/include/fastly/detail/access_bridge_internals.h new file mode 100644 index 0000000..9aca68c --- /dev/null +++ b/include/fastly/detail/access_bridge_internals.h @@ -0,0 +1,28 @@ +#ifndef FASTLY_ACCESS_BRIDGE_INTERNALS_H +#define FASTLY_ACCESS_BRIDGE_INTERNALS_H + +#include + +namespace fastly::detail +{ + struct AccessBridgeInternals + { + template + static auto &get(T &obj) + { + return obj.inner(); + } + template + static auto &get(const T &obj) + { + return obj.inner(); + } + template + static auto from_raw(U *ptr) + { + return T(rust::Box::from_raw(ptr)); + } + }; +} + +#endif \ No newline at end of file diff --git a/include/fastly/esi.h b/include/fastly/esi.h index 88ee0d4..c7a8b92 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -30,34 +32,52 @@ namespace fastly::esi bool is_escaped_content_; }; - class Processor + class ExecutionError { - public: - /// Create a new ESI processor with the given configuration. - Processor(std::optional original_request_metadata = std::nullopt, - Configuration config = Configuration()); - - tl::expected process_response( - Response &src_document, - std::optional client_response_metadata = std::nullopt, - std::optional dispatch_fragment_request = std::nullopt, - std::function(Request &, Response &)> - process_fragment_response = nullptr) - - private : rust::Box processor_; }; - using PendingFragmentContent = std::variant; + using PendingFragmentContent = std::variant; class DispatchFragmentRequestFn { public: - DispatchFragmentRequestFn(std::function(Request)> fn) + DispatchFragmentRequestFn(std::function(Request)> fn) + : fn_(std::move(fn)) {} + + private: + friend detail::AccessBridgeInternals; + auto &inner() const { return fn_; } + std::function(Request)> fn_; + }; + + class ProcessFragmentResponseFn + { + public: + ProcessFragmentResponseFn(std::function(Request &, Response)> fn) : fn_(std::move(fn)) {} - uint32_t call(Request req, PendingResponse *&out_pending, Response *&out_complete, ExecutionError *&out_error) const noexcept; private: - std::function(Request)> fn_; + friend detail::AccessBridgeInternals; + auto &inner() const { return fn_; } + std::function(Request &, Response)> fn_; }; + + class Processor + { + public: + /// Create a new ESI processor with the given configuration. + Processor(std::optional original_request_metadata = std::nullopt, + Configuration config = Configuration()); + + tl::expected process_response( + Response &src_document, + std::optional client_response_metadata = std::nullopt, + std::optional dispatch_fragment_request = std::nullopt, + std::optional process_fragment_response = std::nullopt); + + private: + rust::Box processor_; + }; + } #endif \ No newline at end of file diff --git a/include/fastly/http/request.h b/include/fastly/http/request.h index 6aa3122..10ff15b 100644 --- a/include/fastly/http/request.h +++ b/include/fastly/http/request.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -18,733 +19,742 @@ #include #include -namespace fastly::backend { -class Backend; +namespace fastly::backend +{ + class Backend; } -namespace fastly::http { - -class Body; -class StreamingBody; -class Response; -class Request; - -namespace request { - -/// A handle to a pending asynchronous request returned by -/// `Request::send_async()` or -/// `Request::send_async_streaming()`. -/// -/// A handle can be evaluated using `PendingRequest::poll()`, -/// `PendingRequest::wait()`, or -/// `http::select`. It can also be discarded if the request was sent for effects -/// it might have, and the response is unimportant. -class PendingRequest { - - friend Request; - friend std::pair, std::vector> - select(std::vector &reqs); - - /// Try to get the result of a pending request without blocking. - /// - /// This method returns immediately with a `std::variant` containing either - /// the original `PendingRequest` if the response was not ready, or a - /// `Response` if the response was ready. If you want to block until a result - /// is ready, use `PendingRequest::wait()`. - std::variant> poll(); - - /// Block until the result of a pending request is ready. - /// - /// If you want check whether the result is ready without blocking, use - /// `PendingRequest::poll()`. - fastly::expected wait(); - - /// Cloned version of the original request that was sent, without the original - /// body. This is only a copy and cannot be used to modify anything, since the - /// request has already been sent. - Request cloned_sent_req(); - -private: - rust::Box req; - - PendingRequest(rust::Box r) - : req(std::move(r)) {}; -}; - -/// Given a collection of `PendingRequest`s, block until the result of one of -/// the requests is ready. -/// -/// Returns an `std::pair` of ``, where: -/// -/// - `result` is the result of the request that became ready. -/// -/// - `remaining` is a vector containing all of the requests that did not become -/// ready. The order of the requests in this vector is not guaranteed to match -/// the order of the requests in the argument collection. -/// -/// ### Panics -/// -/// Panics if the argument collection is empty, or contains too many requests. -std::pair, std::vector> -select(std::vector &reqs); - -} // namespace request - -/// An HTTP request, including body, headers, method, and URL. -/// -/// # Getting the client request -/// -/// Call `Request::from_client()` to get the client request being handled by -/// this execution of the Compute program. -/// -/// # Creation and conversion -/// -/// New requests can be created programmatically with the `Request()` -/// constructor. In addition, there are convenience constructors like -/// `Request::get()` which automatically select the appropriate method. -/// -/// # Sending backend requests -/// -/// Requests can be sent to a backend in blocking or asynchronous fashion using -/// `Request::send()`, `Request::send_async()`, or -/// `Request::send_async_streaming()`. -/// -/// # Builder-style methods -/// -/// `Request` can be used as a builder allowing requests to be constructed and -/// used through method chaining. Methods with the `with_` name prefix, such as -/// `Request::with_header()`, return a moved `Request` to allow chaining. The -/// builder style is typically most useful when constructing and using a request -/// in a single expression. -/// -/// For example: -/// -/// ```cpp -/// Request::get("https://example.com") -/// .with_header("my-header", "hello!") -/// .with_header("my-other-header", "Здравствуйте!") -/// .send("example_backend"); -/// ``` -/// -/// # Setter methods -/// -/// Setter methods, such as `Request::set_header()`, are prefixed by `set_`, and -/// can be used interchangeably with the builder-style methods, allowing you to -/// mix and match styles based on what is most convenient for your program. -/// Setter methods tend to work better than builder-style methods when -/// constructing a request involves conditional branches or loops. -/// -/// For example: -/// -/// ```cpp -/// auto req{Request::get("https://example.com").with_header("my-header", -/// "hello!")}; -/// if (needs_translation) { -/// req.set_header("my-other-header", "Здравствуйте!"); -/// } -/// req.send("example_backend"); -/// ``` -class Request { - friend request::PendingRequest; - friend Response; - -public: - /// Create a new request with the given method and URL, no headers, and an - /// empty body. - Request(Method method, std::string_view url); - - /// Create a new `GET` `Request` with the given URL, no headers, and an - /// empty body. - static Request get(std::string_view url); - - /// Create a new `HEAD` `Request` with the given URL, no headers, and an - /// empty body. - static Request head(std::string_view url); - - /// Create a new `POST` `Request` with the given URL, no headers, and an - /// empty body. - static Request post(std::string_view url); - - /// Create a new `PUT` `Request` with the given URL, no headers, and an - /// empty body. - static Request put(std::string_view url); - - /// Create a new `DELETE` `Request` with the given URL, no headers, and an - /// empty body. - static Request delete_(std::string_view url); - - /// Create a new `CONNECT` `Request` with the given URL, no headers, and an - /// empty body. - static Request connect(std::string_view url); - - /// Create a new `OPTIONS` `Request` with the given URL, no headers, and an - /// empty body. - static Request options(std::string_view url); - - /// Create a new `TRACE` `Request` with the given URL, no headers, and an - /// empty body. - static Request trace(std::string_view url); - - /// Create a new `PATCH` `Request` with the given URL, no headers, and an - /// empty body. - static Request patch(std::string_view url); - - /// Get the client request being handled by this execution of the Compute - /// program. - /// - /// # Panics - /// - /// This method panics if the client request has already been retrieved by - /// this method or by the low-level handle API. - static Request from_client(); - - /// Return `true` if this request is from the client of this execution of the - /// Compute program. - bool is_from_client(); - - /// Make a new request with the same method, url, headers, and version of this - /// request, but no body. - /// - /// If you also need to clone the request body, use - /// [`clone_with_body()`][`Self::clone_with_body()`] - /// - /// # Examples - /// - /// ```cpp - /// auto original = Request::post("https://example.com") - /// .with_header("hello", "world!") - /// .with_body("hello"); - /// auto new_req = original.clone_without_body(); - /// assert(original.get_method() == new.get_method()); - /// assert(original.get_url() == new.get_url()); - /// assert(original.get_header("hello") == new.get_header("hello")); - /// assert(original.has_body()); - /// assert(!new.has_body()); - /// ``` - Request clone_without_body(); - - /// Clone this request by reading in its body, and then writing the same body - /// to the original and the cloned request. - /// - /// This method requires mutable access to this request because reading from - /// and writing to the body can involve an HTTP connection. - Request clone_with_body(); - - /// Retrieve a reponse for the request, either from cache or by sending it to - /// the given backend server. Returns once the response headers have been - /// received, or an error occurs. - /// - /// # Examples - /// - /// Sending the client request to a backend without modification: - /// - /// ```cpp - /// auto backend_resp{Request::from_client().send("example_backend")}; - /// assert(backend_resp.get_status().is_success()); - /// ``` - /// - /// Sending a synthetic request: - /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com").send("example_backend")}; - /// assert(backend_resp.get_status().is_success()); - /// ``` - fastly::expected send(fastly::backend::Backend &backend); - fastly::expected send(std::string_view backend_name); - - /// Begin sending the request to the given backend server, and return a - /// `PendingRequest` that can yield the backend response or an error. - /// - /// This method returns as soon as the request begins sending to the backend, - /// and transmission of the request body and headers will continue in the - /// background. - /// - /// This method allows for sending more than one request at once and receiving - /// their responses in arbitrary orders. See `PendingRequest` for more details - /// on how to wait on, poll, or select between pending requests. - /// - /// This method is also useful for sending requests where the response is - /// unimportant, but the request may take longer than the Compute program is - /// able to run, as the request will continue sending even after the program - /// that initiated it exits. - /// - /// # Examples - /// - /// Sending a request to two backends and returning whichever response - /// finishes first: - /// - /// ```cpp - /// auto backend_resp_1{Request::get("https://example.com/") - /// .send_async("example_backend_1")}; - /// auto backend_resp_2{Request::get("https://example.com/") - /// .send_async("example_backend_2")}; - /// auto [selected_resp, _others] = - /// fastly::http::request::select({backend_resp_1, backend_resp_2}); - /// selected_resp.send_to_client(); - /// ``` - /// - /// Sending a long-running request and ignoring its result so that the program - /// can exit before - /// it completes: +namespace fastly::http +{ + + class Body; + class StreamingBody; + class Response; + class Request; + + namespace request + { + + /// A handle to a pending asynchronous request returned by + /// `Request::send_async()` or + /// `Request::send_async_streaming()`. + /// + /// A handle can be evaluated using `PendingRequest::poll()`, + /// `PendingRequest::wait()`, or + /// `http::select`. It can also be discarded if the request was sent for effects + /// it might have, and the response is unimportant. + class PendingRequest + { + friend detail::AccessBridgeInternals; + friend Request; + friend std::pair, std::vector> + select(std::vector &reqs); + + /// Try to get the result of a pending request without blocking. + /// + /// This method returns immediately with a `std::variant` containing either + /// the original `PendingRequest` if the response was not ready, or a + /// `Response` if the response was ready. If you want to block until a result + /// is ready, use `PendingRequest::wait()`. + std::variant> poll(); + + /// Block until the result of a pending request is ready. + /// + /// If you want check whether the result is ready without blocking, use + /// `PendingRequest::poll()`. + fastly::expected wait(); + + /// Cloned version of the original request that was sent, without the original + /// body. This is only a copy and cannot be used to modify anything, since the + /// request has already been sent. + Request cloned_sent_req(); + + private: + auto &inner() { return req; } + rust::Box req; + + PendingRequest(rust::Box r) + : req(std::move(r)) {}; + }; + + /// Given a collection of `PendingRequest`s, block until the result of one of + /// the requests is ready. + /// + /// Returns an `std::pair` of ``, where: + /// + /// - `result` is the result of the request that became ready. + /// + /// - `remaining` is a vector containing all of the requests that did not become + /// ready. The order of the requests in this vector is not guaranteed to match + /// the order of the requests in the argument collection. + /// + /// ### Panics + /// + /// Panics if the argument collection is empty, or contains too many requests. + std::pair, std::vector> + select(std::vector &reqs); + + } // namespace request + + /// An HTTP request, including body, headers, method, and URL. + /// + /// # Getting the client request + /// + /// Call `Request::from_client()` to get the client request being handled by + /// this execution of the Compute program. + /// + /// # Creation and conversion + /// + /// New requests can be created programmatically with the `Request()` + /// constructor. In addition, there are convenience constructors like + /// `Request::get()` which automatically select the appropriate method. + /// + /// # Sending backend requests + /// + /// Requests can be sent to a backend in blocking or asynchronous fashion using + /// `Request::send()`, `Request::send_async()`, or + /// `Request::send_async_streaming()`. + /// + /// # Builder-style methods + /// + /// `Request` can be used as a builder allowing requests to be constructed and + /// used through method chaining. Methods with the `with_` name prefix, such as + /// `Request::with_header()`, return a moved `Request` to allow chaining. The + /// builder style is typically most useful when constructing and using a request + /// in a single expression. + /// + /// For example: /// /// ```cpp - /// Request::post("https://example.com") - /// .with_body(some_large_file) - /// .send_async("example_backend"); + /// Request::get("https://example.com") + /// .with_header("my-header", "hello!") + /// .with_header("my-other-header", "Здравствуйте!") + /// .send("example_backend"); /// ``` - fastly::expected - send_async(fastly::backend::Backend &backend); - fastly::expected - send_async(std::string_view backend_name); - - /// Begin sending the request to the given backend server, and return a - /// `PendingRequest` that - /// can yield the backend response or an error along with a `StreamingBody` - /// that can accept - /// further data to send. - /// - /// The backend connection is only closed once `StreamingBody::finish()` is - /// called. The - /// `PendingRequest` will not yield a `Response` until the - /// `StreamingBody` is finished. - /// - /// This method is most useful for programs that do some sort of processing or - /// inspection of a - /// potentially-large client request body. Streaming allows the program to - /// operate on small - /// parts of the body rather than having to read it all into memory at once. /// - /// This method returns as soon as the request begins sending to the backend, - /// and transmission - /// of the request body and headers will continue in the background. + /// # Setter methods /// - /// # Examples + /// Setter methods, such as `Request::set_header()`, are prefixed by `set_`, and + /// can be used interchangeably with the builder-style methods, allowing you to + /// mix and match styles based on what is most convenient for your program. + /// Setter methods tend to work better than builder-style methods when + /// constructing a request involves conditional branches or loops. /// - /// Count the number of lines in a UTF-8 client request body while sending it - /// to the backend: + /// For example: /// /// ```cpp - /// auto req{Request::from_client()}; - /// // Take the body so we can iterate through its lines later - /// auto req_body{req.take_body()}; - /// // Start sending the client request to the client with a now-empty body - /// auto [backend_body, pending_req] = req - /// .send_async_streaming("example_backend"); - /// - /// size_t num_lines{0}; - /// std::string buf; - /// while (std::getline(req_body, buf)) { - /// num_lines++; - /// // Write the line to the streaming backend body - /// backend_body << buf << "\n" << std::flush; + /// auto req{Request::get("https://example.com").with_header("my-header", + /// "hello!")}; + /// if (needs_translation) { + /// req.set_header("my-other-header", "Здравствуйте!"); /// } - /// // Finish the streaming body to allow the backend connection to close - /// backend_body.finish(); - /// - /// std::cout - /// << "client request body contained " - /// << num_lines - /// << " lines" - /// << std::endl; - /// ``` - fastly::expected> - send_async_streaming(fastly::backend::Backend &backend); - fastly::expected> - send_async_streaming(std::string_view backend_name); - - /// Builder-style equivalent of `Request::set_body()`. - Request with_body(Body body) &&; - - /// Returns `true` if this request has a body. - bool has_body(); - - /// Take and return the body from this request. - /// - /// After calling this method, this request will no longer have a body. - Body take_body(); - - /// Set the given value as the request's body. - void set_body(Body body); - - /// Append another [`Body`] to the body of this request without reading or - /// writing any body contents. - /// - /// If this request does not have a body, the appended body is set as the - /// request's body. - /// - /// This method should be used when combining bodies that have not - /// necessarily been read yet, such as the body of the client. To append - /// contents that are already in memory as strings or bytes, you should - /// instead use - /// [`get_body_mut()`][`Self::get_body_mut()`] to write the contents to the - /// end of the body. - /// - /// # Examples - /// - /// ```cpp - /// auto req{Request::post("https://example.com").with_body("hello! client - /// says: ")}; req.append_body(Request::from_client().into_body()); /// req.send("example_backend"); /// ``` - void append_body(Body &body); - - /// Consume the request and return its body as a byte vector. - std::vector into_body_bytes(); - - /// Consume the request and return its body as a string. - std::string into_body_string(); - - /// Consume the request and return its body as a `Body` instance. - Body into_body(); - - /// Builder-style equivalent of - /// `Request::set_body_text_plain()`. - fastly::expected with_body_text_plain(std::string_view body) &&; - - /// Set the given string as the request's body with content type - /// `text/plain; charset=UTF-8`. - fastly::expected set_body_text_plain(std::string_view body); - - /// Builder-style equivalent of - /// `Request::set_body_text_html()`. - fastly::expected with_body_text_html(std::string_view body) &&; - - /// Set the given string as the request's body with content type `text/html; - /// charset=UTF-8`. - fastly::expected set_body_text_html(std::string_view body); - - /// Take and return the body from this request as a string. - /// - /// After calling this method, this request will no longer have a body. - std::string take_body_string(); - - /// Builder-style equivalent of - /// `Request::set_body_octet_stream()`. - Request with_body_octet_stream(std::vector body) &&; - - /// Set the given bytes as the request's body with content type - /// `application/octet-stream`. - void set_body_octet_stream(std::vector body); - - /// Take and return the body from this request as a vector of bytes. - /// - /// After calling this method, this request will no longer have a body. - std::vector take_body_bytes(); - - // ChunksIter read_body_chunks(size_t chunk_size); - - /// Get the MIME type described by the request's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header, or `std::nullopt` if that header is absent or contains an - /// invalid MIME type. - std::optional get_content_type(); - - /// Builder-style equivalent of - /// `Request::set_content_type()`. - Request with_content_type(std::string_view mime) &&; - - /// Set the MIME type described by the request's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header. - /// - /// Any existing `Content-Type` header values will be overwritten. - void set_content_type(std::string_view mime); - - /// Get the value of the request's - /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) - /// header, if it exists. - std::optional get_content_length(); - - /// Returns whether the given header name is present in the request. - fastly::expected contains_header(std::string_view name); - - /// Builder-style equivalent of `Request::append_header()`. - fastly::expected with_header(std::string_view name, - std::string_view value) &&; - - /// Builder-style equivalent of `Request::set_header()`. - fastly::expected with_set_header(std::string_view name, - std::string_view value) &&; - - /// Get the value of a header as a string, or `std::nullopt` if the header - /// is not present. - /// - /// If there are multiple values for the header, only one is returned, which - /// may be any of the values. See - /// `Request::get_header_all()` - /// all of the values. - fastly::expected> - get_header(std::string_view name); - - /// Get an iterator of all the values of a header. - fastly::expected get_header_all(std::string_view name); - fastly::expected get_headers(); - fastly::expected get_header_names(); - - /// Set a request header to the given value, discarding any previous values - /// for the given header name. - fastly::expected set_header(std::string_view name, - std::string_view value); - - /// Add a request header with given value. - /// - /// Unlike `Request::set_header()`, this does not discard existing values - /// for the same header name. - fastly::expected append_header(std::string_view name, - std::string_view value); - - /// Remove all request headers of the given name, and return one of the - /// removed header values if any were present. - fastly::expected> - remove_header(std::string_view name); - - /// Builder-style equivalent of `Request::set_method()`. - Request with_method(Method method) &&; - - /// Get the request method. - Method get_method(); - - /// Set the request method. - void set_method(Method method); - - /// Builder-style equivalent of `Request::set_url()`. - fastly::expected with_url(std::string_view url) &&; - - /// Get the request URL as a string. - std::string get_url(); - - /// Set the request URL. - fastly::expected set_url(std::string_view url); - - /// Get the path component of the request URL. - /// - /// # Examples - /// - /// ```cpp - /// auto req{Request::get("https://example.com/hello#world")}; - /// assert(req.get_path() == "/hello"); - /// ``` - std::string get_path(); - - /// Builder-style equivalent of `Request::set_path()`. - fastly::expected with_path(std::string_view path) &&; - - /// Set the path component of the request URL. - /// # Examples - /// - /// ```cpp - /// auto req{Request::get("https://example.com/")}; - /// req.set_path("/hello"); - /// assert!(req.get_url(), "https://example.com/hello"); - /// ``` - fastly::expected set_path(std::string_view path); - - /// Get the query component of the request URL, if it exists, as a - /// percent-encoded ASCII string. - std::optional get_query_string(); - - /// Get the value of a query parameter in the request's URL. - /// - /// This assumes that the query string is a `&` separated list of - /// `parameter=value` pairs. The value of the first occurrence of - /// `parameter` is returned. No URL decoding is performed. - std::optional get_query_parameter(std::string_view param); - - /// Builder-style equivalent of `Request::set_query()`. - fastly::expected with_query_string(std::string_view query) &&; - - /// Set the query string of the request URL query component to the given - /// string, performing percent-encoding if necessary. - /// - /// # Examples - /// - /// ```no_run - /// auto req{Request::get("https://example.com/foo")}; - /// req.set_query_string("hello=🌐!&bar=baz"); - /// assert(req.get_url(), - /// "https://example.com/foo?hello=%F0%9F%8C%90!&bar=baz"); - /// ``` - fastly::expected set_query_string(std::string_view query); - - /// Remove the query component from the request URL, if one exists. - void remove_query(); - - /// Builder-style equivalent of `Request::set_version()`. - Request with_version(Version version) &&; - - /// Get the HTTP version of this request. - Version get_version(); - - /// Set the HTTP version of this request. - void set_version(Version version); - - /// Builder-style equivalent of `Request::set_pass()`. - Request with_pass(bool pass) &&; - - /// Set whether this request should be cached if sent to a backend. - /// - /// By default this is `false`, which means the backend will only be reached - /// if a cached response is not available. Set this to `true` to send the - /// request directly to the backend without caching. - /// - /// # Overrides - /// - /// Setting this to `true` overrides any other custom caching behaviors for - /// this request, such as `Request::set_ttl()` or - /// `Request::set_surrogate_key()`. - void set_pass(bool pass); - - /// Builder-style equivalent of `Request::set_ttl()`. - Request with_ttl(uint32_t ttl) &&; - - /// Override the caching behavior of this request to use the given Time to - /// Live (TTL), in seconds. - /// - /// # Overrides - /// - /// This overrides the behavior specified in the response headers, and sets - /// the `Request::set_pass()` behavior to `false`. - void set_ttl(uint32_t ttl); - - /// Builder-style equivalent of - /// `Request::set_stale_while_revalidate()`. - Request with_stale_while_revalidate(uint32_t swr) &&; - - /// Override the caching behavior of this request to use the given - /// `stale-while-revalidate` time, in seconds. - /// - /// # Overrides - /// - /// This overrides the behavior specified in the response headers, and sets - /// the `Request::set_pass()` behavior to `false`. - void set_stale_while_revalidate(uint32_t swr); - - /// Builder-style equivalent of `Request::set_pci()`. - Request with_pci(bool pci) &&; - - /// Override the caching behavior of this request to enable or disable - /// PCI/HIPAA-compliant non-volatile caching. - /// - /// By default, this is `false`, which means the request may not be - /// PCI/HIPAA-compliant. Set it to `true` to enable compliant caching. - /// - /// See the [Fastly PCI-Compliant Caching and Delivery - /// documentation](https://docs.fastly.com/products/pci-compliant-caching-and-delivery) - /// for details. - /// - /// # Overrides - /// - /// This sets the `Request::set_pass()` behavior to `false`. - void set_pci(bool pci); - - /// Builder-style equivalent of - /// `Request::set_surrogate_key()`. - fastly::expected with_surrogate_key(std::string_view sk) &&; - - /// Override the caching behavior of this request to include the given - /// surrogate key(s), provided as a header value. - /// - /// The header value can contain more than one surrogate key, separated by - /// spaces. - /// - /// Surrogate keys must contain only printable ASCII characters (those - /// between `0x21` and `0x7E`, inclusive). Any invalid keys will be ignored. - /// - /// See the [Fastly surrogate keys - /// guide](https://docs.fastly.com/en/guides/purging-api-cache-with-surrogate-keys) - /// for details. - /// - /// # Overrides - /// - /// This sets the `Request::set_pass()` behavior to `false`, and - /// extends (but does not replace) any `Surrogate-Key` response headers from - /// the backend. - fastly::expected set_surrogate_key(std::string_view sk); - - std::optional get_client_ip_addr(); - std::optional get_server_ip_addr(); - - std::optional get_original_header_names(); - std::optional get_original_header_count(); - - /// Returns whether the request was tagged as contributing to a DDoS attack - /// - /// Returns `std::nullopt` if this is not the client request. - std::optional get_client_ddos_detected(); - - // std::optional> get_tls_client_hello(); - // std::optional> get_tls_ja3_md5(); - // std::optional get_tls_ja4(); - // std::optional get_tls_raw_client_certificate(); - // std::optional> - // get_tls_raw_client_certificate_bytes(); - // // TODO(@zkat): needs additional type - // // std::optional - // get_tls_client_cert_verify_result(); std::optional - // get_tls_cipher_openssl_name(); std::optional> - // get_tls_cipher_openssl_name_bytes(); std::optional> - // get_tls_protocol_bytes(); - - /// Set whether a `gzip`-encoded response to this request will be - /// automatically decompressed. - /// - /// Enabling this will set the `Accept-Encoding` header before the request - /// is sent, regardless of the original value in the request, to ensure that - /// any values originally sent by a browser or other client get replaced - /// with `gzip`, so that the backend will not try sending unsupported - /// compression algorithms. - /// - /// If the response to this request is `gzip`-encoded, it will be presented - /// in decompressed form, and the `Content-Encoding` and `Content-Length` - /// headers will be removed. - void set_auto_decompress_gzip(bool gzip); - - /// Builder-style equivalent of - /// `Request::set_auto_decompress_gzip()`. - Request with_auto_decompress_gzip(bool gzip) &&; - - // TODO(@zkat): needs enum - // void set_framing_headers_mode(FramingHeadersMode mode); - // Request *set_framing_headers_mode(FramingHeadersMode mode); - - /// Returns whether or not the client request had a `Fastly-Key` header - /// which is valid for purging content for the service. - /// - /// This function ignores the current value of any `Fastly-Key` header for - /// this request. - bool fastly_key_is_valid(); - - // void handoff_websocket(fastly::backend::Backend backend); - // void handoff_fanout(fastly::backend::Backend backend); - // Request *on_behalf_of(std::string_view service); - - /// Set the cache key to be used when attempting to satisfy this request - /// from a cached response. - void set_cache_key(std::string_view key); - - /// Set the cache key to be used when attempting to satisfy this request - /// from a cached response. - void set_cache_key(std::vector key); - - /// Builder-style equivalent of `Request::set_cache_key()`. - Request with_cache_key(std::string_view key) &&; - - /// Builder-style equivalent of `Request::set_cache_key()`. - Request with_cache_key(std::vector key) &&; - - /// Gets whether the request is potentially cacheable. - bool is_cacheable(); - -private: - Request(rust::Box r) : req(std::move(r)) {}; - rust::Box req; -}; + class Request + { + friend request::PendingRequest; + friend Response; + friend detail::AccessBridgeInternals; + + public: + /// Create a new request with the given method and URL, no headers, and an + /// empty body. + Request(Method method, std::string_view url); + + /// Create a new `GET` `Request` with the given URL, no headers, and an + /// empty body. + static Request get(std::string_view url); + + /// Create a new `HEAD` `Request` with the given URL, no headers, and an + /// empty body. + static Request head(std::string_view url); + + /// Create a new `POST` `Request` with the given URL, no headers, and an + /// empty body. + static Request post(std::string_view url); + + /// Create a new `PUT` `Request` with the given URL, no headers, and an + /// empty body. + static Request put(std::string_view url); + + /// Create a new `DELETE` `Request` with the given URL, no headers, and an + /// empty body. + static Request delete_(std::string_view url); + + /// Create a new `CONNECT` `Request` with the given URL, no headers, and an + /// empty body. + static Request connect(std::string_view url); + + /// Create a new `OPTIONS` `Request` with the given URL, no headers, and an + /// empty body. + static Request options(std::string_view url); + + /// Create a new `TRACE` `Request` with the given URL, no headers, and an + /// empty body. + static Request trace(std::string_view url); + + /// Create a new `PATCH` `Request` with the given URL, no headers, and an + /// empty body. + static Request patch(std::string_view url); + + /// Get the client request being handled by this execution of the Compute + /// program. + /// + /// # Panics + /// + /// This method panics if the client request has already been retrieved by + /// this method or by the low-level handle API. + static Request from_client(); + + /// Return `true` if this request is from the client of this execution of the + /// Compute program. + bool is_from_client(); + + /// Make a new request with the same method, url, headers, and version of this + /// request, but no body. + /// + /// If you also need to clone the request body, use + /// [`clone_with_body()`][`Self::clone_with_body()`] + /// + /// # Examples + /// + /// ```cpp + /// auto original = Request::post("https://example.com") + /// .with_header("hello", "world!") + /// .with_body("hello"); + /// auto new_req = original.clone_without_body(); + /// assert(original.get_method() == new.get_method()); + /// assert(original.get_url() == new.get_url()); + /// assert(original.get_header("hello") == new.get_header("hello")); + /// assert(original.has_body()); + /// assert(!new.has_body()); + /// ``` + Request clone_without_body(); + + /// Clone this request by reading in its body, and then writing the same body + /// to the original and the cloned request. + /// + /// This method requires mutable access to this request because reading from + /// and writing to the body can involve an HTTP connection. + Request clone_with_body(); + + /// Retrieve a reponse for the request, either from cache or by sending it to + /// the given backend server. Returns once the response headers have been + /// received, or an error occurs. + /// + /// # Examples + /// + /// Sending the client request to a backend without modification: + /// + /// ```cpp + /// auto backend_resp{Request::from_client().send("example_backend")}; + /// assert(backend_resp.get_status().is_success()); + /// ``` + /// + /// Sending a synthetic request: + /// + /// ```cpp + /// auto + /// backend_resp{Request::get("https://example.com").send("example_backend")}; + /// assert(backend_resp.get_status().is_success()); + /// ``` + fastly::expected send(fastly::backend::Backend &backend); + fastly::expected send(std::string_view backend_name); + + /// Begin sending the request to the given backend server, and return a + /// `PendingRequest` that can yield the backend response or an error. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission of the request body and headers will continue in the + /// background. + /// + /// This method allows for sending more than one request at once and receiving + /// their responses in arbitrary orders. See `PendingRequest` for more details + /// on how to wait on, poll, or select between pending requests. + /// + /// This method is also useful for sending requests where the response is + /// unimportant, but the request may take longer than the Compute program is + /// able to run, as the request will continue sending even after the program + /// that initiated it exits. + /// + /// # Examples + /// + /// Sending a request to two backends and returning whichever response + /// finishes first: + /// + /// ```cpp + /// auto backend_resp_1{Request::get("https://example.com/") + /// .send_async("example_backend_1")}; + /// auto backend_resp_2{Request::get("https://example.com/") + /// .send_async("example_backend_2")}; + /// auto [selected_resp, _others] = + /// fastly::http::request::select({backend_resp_1, backend_resp_2}); + /// selected_resp.send_to_client(); + /// ``` + /// + /// Sending a long-running request and ignoring its result so that the program + /// can exit before + /// it completes: + /// + /// ```cpp + /// Request::post("https://example.com") + /// .with_body(some_large_file) + /// .send_async("example_backend"); + /// ``` + fastly::expected + send_async(fastly::backend::Backend &backend); + fastly::expected + send_async(std::string_view backend_name); + + /// Begin sending the request to the given backend server, and return a + /// `PendingRequest` that + /// can yield the backend response or an error along with a `StreamingBody` + /// that can accept + /// further data to send. + /// + /// The backend connection is only closed once `StreamingBody::finish()` is + /// called. The + /// `PendingRequest` will not yield a `Response` until the + /// `StreamingBody` is finished. + /// + /// This method is most useful for programs that do some sort of processing or + /// inspection of a + /// potentially-large client request body. Streaming allows the program to + /// operate on small + /// parts of the body rather than having to read it all into memory at once. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission + /// of the request body and headers will continue in the background. + /// + /// # Examples + /// + /// Count the number of lines in a UTF-8 client request body while sending it + /// to the backend: + /// + /// ```cpp + /// auto req{Request::from_client()}; + /// // Take the body so we can iterate through its lines later + /// auto req_body{req.take_body()}; + /// // Start sending the client request to the client with a now-empty body + /// auto [backend_body, pending_req] = req + /// .send_async_streaming("example_backend"); + /// + /// size_t num_lines{0}; + /// std::string buf; + /// while (std::getline(req_body, buf)) { + /// num_lines++; + /// // Write the line to the streaming backend body + /// backend_body << buf << "\n" << std::flush; + /// } + /// // Finish the streaming body to allow the backend connection to close + /// backend_body.finish(); + /// + /// std::cout + /// << "client request body contained " + /// << num_lines + /// << " lines" + /// << std::endl; + /// ``` + fastly::expected> + send_async_streaming(fastly::backend::Backend &backend); + fastly::expected> + send_async_streaming(std::string_view backend_name); + + /// Builder-style equivalent of `Request::set_body()`. + Request with_body(Body body) &&; + + /// Returns `true` if this request has a body. + bool has_body(); + + /// Take and return the body from this request. + /// + /// After calling this method, this request will no longer have a body. + Body take_body(); + + /// Set the given value as the request's body. + void set_body(Body body); + + /// Append another [`Body`] to the body of this request without reading or + /// writing any body contents. + /// + /// If this request does not have a body, the appended body is set as the + /// request's body. + /// + /// This method should be used when combining bodies that have not + /// necessarily been read yet, such as the body of the client. To append + /// contents that are already in memory as strings or bytes, you should + /// instead use + /// [`get_body_mut()`][`Self::get_body_mut()`] to write the contents to the + /// end of the body. + /// + /// # Examples + /// + /// ```cpp + /// auto req{Request::post("https://example.com").with_body("hello! client + /// says: ")}; req.append_body(Request::from_client().into_body()); + /// req.send("example_backend"); + /// ``` + void append_body(Body &body); + + /// Consume the request and return its body as a byte vector. + std::vector into_body_bytes(); + + /// Consume the request and return its body as a string. + std::string into_body_string(); + + /// Consume the request and return its body as a `Body` instance. + Body into_body(); + + /// Builder-style equivalent of + /// `Request::set_body_text_plain()`. + fastly::expected with_body_text_plain(std::string_view body) &&; + + /// Set the given string as the request's body with content type + /// `text/plain; charset=UTF-8`. + fastly::expected set_body_text_plain(std::string_view body); + + /// Builder-style equivalent of + /// `Request::set_body_text_html()`. + fastly::expected with_body_text_html(std::string_view body) &&; + + /// Set the given string as the request's body with content type `text/html; + /// charset=UTF-8`. + fastly::expected set_body_text_html(std::string_view body); + + /// Take and return the body from this request as a string. + /// + /// After calling this method, this request will no longer have a body. + std::string take_body_string(); + + /// Builder-style equivalent of + /// `Request::set_body_octet_stream()`. + Request with_body_octet_stream(std::vector body) &&; + + /// Set the given bytes as the request's body with content type + /// `application/octet-stream`. + void set_body_octet_stream(std::vector body); + + /// Take and return the body from this request as a vector of bytes. + /// + /// After calling this method, this request will no longer have a body. + std::vector take_body_bytes(); + + // ChunksIter read_body_chunks(size_t chunk_size); + + /// Get the MIME type described by the request's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header, or `std::nullopt` if that header is absent or contains an + /// invalid MIME type. + std::optional get_content_type(); + + /// Builder-style equivalent of + /// `Request::set_content_type()`. + Request with_content_type(std::string_view mime) &&; + + /// Set the MIME type described by the request's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header. + /// + /// Any existing `Content-Type` header values will be overwritten. + void set_content_type(std::string_view mime); + + /// Get the value of the request's + /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) + /// header, if it exists. + std::optional get_content_length(); + + /// Returns whether the given header name is present in the request. + fastly::expected contains_header(std::string_view name); + + /// Builder-style equivalent of `Request::append_header()`. + fastly::expected with_header(std::string_view name, + std::string_view value) &&; + + /// Builder-style equivalent of `Request::set_header()`. + fastly::expected with_set_header(std::string_view name, + std::string_view value) &&; + + /// Get the value of a header as a string, or `std::nullopt` if the header + /// is not present. + /// + /// If there are multiple values for the header, only one is returned, which + /// may be any of the values. See + /// `Request::get_header_all()` + /// all of the values. + fastly::expected> + get_header(std::string_view name); + + /// Get an iterator of all the values of a header. + fastly::expected get_header_all(std::string_view name); + fastly::expected get_headers(); + fastly::expected get_header_names(); + + /// Set a request header to the given value, discarding any previous values + /// for the given header name. + fastly::expected set_header(std::string_view name, + std::string_view value); + + /// Add a request header with given value. + /// + /// Unlike `Request::set_header()`, this does not discard existing values + /// for the same header name. + fastly::expected append_header(std::string_view name, + std::string_view value); + + /// Remove all request headers of the given name, and return one of the + /// removed header values if any were present. + fastly::expected> + remove_header(std::string_view name); + + /// Builder-style equivalent of `Request::set_method()`. + Request with_method(Method method) &&; + + /// Get the request method. + Method get_method(); + + /// Set the request method. + void set_method(Method method); + + /// Builder-style equivalent of `Request::set_url()`. + fastly::expected with_url(std::string_view url) &&; + + /// Get the request URL as a string. + std::string get_url(); + + /// Set the request URL. + fastly::expected set_url(std::string_view url); + + /// Get the path component of the request URL. + /// + /// # Examples + /// + /// ```cpp + /// auto req{Request::get("https://example.com/hello#world")}; + /// assert(req.get_path() == "/hello"); + /// ``` + std::string get_path(); + + /// Builder-style equivalent of `Request::set_path()`. + fastly::expected with_path(std::string_view path) &&; + + /// Set the path component of the request URL. + /// # Examples + /// + /// ```cpp + /// auto req{Request::get("https://example.com/")}; + /// req.set_path("/hello"); + /// assert!(req.get_url(), "https://example.com/hello"); + /// ``` + fastly::expected set_path(std::string_view path); + + /// Get the query component of the request URL, if it exists, as a + /// percent-encoded ASCII string. + std::optional get_query_string(); + + /// Get the value of a query parameter in the request's URL. + /// + /// This assumes that the query string is a `&` separated list of + /// `parameter=value` pairs. The value of the first occurrence of + /// `parameter` is returned. No URL decoding is performed. + std::optional get_query_parameter(std::string_view param); + + /// Builder-style equivalent of `Request::set_query()`. + fastly::expected with_query_string(std::string_view query) &&; + + /// Set the query string of the request URL query component to the given + /// string, performing percent-encoding if necessary. + /// + /// # Examples + /// + /// ```no_run + /// auto req{Request::get("https://example.com/foo")}; + /// req.set_query_string("hello=🌐!&bar=baz"); + /// assert(req.get_url(), + /// "https://example.com/foo?hello=%F0%9F%8C%90!&bar=baz"); + /// ``` + fastly::expected set_query_string(std::string_view query); + + /// Remove the query component from the request URL, if one exists. + void remove_query(); + + /// Builder-style equivalent of `Request::set_version()`. + Request with_version(Version version) &&; + + /// Get the HTTP version of this request. + Version get_version(); + + /// Set the HTTP version of this request. + void set_version(Version version); + + /// Builder-style equivalent of `Request::set_pass()`. + Request with_pass(bool pass) &&; + + /// Set whether this request should be cached if sent to a backend. + /// + /// By default this is `false`, which means the backend will only be reached + /// if a cached response is not available. Set this to `true` to send the + /// request directly to the backend without caching. + /// + /// # Overrides + /// + /// Setting this to `true` overrides any other custom caching behaviors for + /// this request, such as `Request::set_ttl()` or + /// `Request::set_surrogate_key()`. + void set_pass(bool pass); + + /// Builder-style equivalent of `Request::set_ttl()`. + Request with_ttl(uint32_t ttl) &&; + + /// Override the caching behavior of this request to use the given Time to + /// Live (TTL), in seconds. + /// + /// # Overrides + /// + /// This overrides the behavior specified in the response headers, and sets + /// the `Request::set_pass()` behavior to `false`. + void set_ttl(uint32_t ttl); + + /// Builder-style equivalent of + /// `Request::set_stale_while_revalidate()`. + Request with_stale_while_revalidate(uint32_t swr) &&; + + /// Override the caching behavior of this request to use the given + /// `stale-while-revalidate` time, in seconds. + /// + /// # Overrides + /// + /// This overrides the behavior specified in the response headers, and sets + /// the `Request::set_pass()` behavior to `false`. + void set_stale_while_revalidate(uint32_t swr); + + /// Builder-style equivalent of `Request::set_pci()`. + Request with_pci(bool pci) &&; + + /// Override the caching behavior of this request to enable or disable + /// PCI/HIPAA-compliant non-volatile caching. + /// + /// By default, this is `false`, which means the request may not be + /// PCI/HIPAA-compliant. Set it to `true` to enable compliant caching. + /// + /// See the [Fastly PCI-Compliant Caching and Delivery + /// documentation](https://docs.fastly.com/products/pci-compliant-caching-and-delivery) + /// for details. + /// + /// # Overrides + /// + /// This sets the `Request::set_pass()` behavior to `false`. + void set_pci(bool pci); + + /// Builder-style equivalent of + /// `Request::set_surrogate_key()`. + fastly::expected with_surrogate_key(std::string_view sk) &&; + + /// Override the caching behavior of this request to include the given + /// surrogate key(s), provided as a header value. + /// + /// The header value can contain more than one surrogate key, separated by + /// spaces. + /// + /// Surrogate keys must contain only printable ASCII characters (those + /// between `0x21` and `0x7E`, inclusive). Any invalid keys will be ignored. + /// + /// See the [Fastly surrogate keys + /// guide](https://docs.fastly.com/en/guides/purging-api-cache-with-surrogate-keys) + /// for details. + /// + /// # Overrides + /// + /// This sets the `Request::set_pass()` behavior to `false`, and + /// extends (but does not replace) any `Surrogate-Key` response headers from + /// the backend. + fastly::expected set_surrogate_key(std::string_view sk); + + std::optional get_client_ip_addr(); + std::optional get_server_ip_addr(); + + std::optional get_original_header_names(); + std::optional get_original_header_count(); + + /// Returns whether the request was tagged as contributing to a DDoS attack + /// + /// Returns `std::nullopt` if this is not the client request. + std::optional get_client_ddos_detected(); + + // std::optional> get_tls_client_hello(); + // std::optional> get_tls_ja3_md5(); + // std::optional get_tls_ja4(); + // std::optional get_tls_raw_client_certificate(); + // std::optional> + // get_tls_raw_client_certificate_bytes(); + // // TODO(@zkat): needs additional type + // // std::optional + // get_tls_client_cert_verify_result(); std::optional + // get_tls_cipher_openssl_name(); std::optional> + // get_tls_cipher_openssl_name_bytes(); std::optional> + // get_tls_protocol_bytes(); + + /// Set whether a `gzip`-encoded response to this request will be + /// automatically decompressed. + /// + /// Enabling this will set the `Accept-Encoding` header before the request + /// is sent, regardless of the original value in the request, to ensure that + /// any values originally sent by a browser or other client get replaced + /// with `gzip`, so that the backend will not try sending unsupported + /// compression algorithms. + /// + /// If the response to this request is `gzip`-encoded, it will be presented + /// in decompressed form, and the `Content-Encoding` and `Content-Length` + /// headers will be removed. + void set_auto_decompress_gzip(bool gzip); + + /// Builder-style equivalent of + /// `Request::set_auto_decompress_gzip()`. + Request with_auto_decompress_gzip(bool gzip) &&; + + // TODO(@zkat): needs enum + // void set_framing_headers_mode(FramingHeadersMode mode); + // Request *set_framing_headers_mode(FramingHeadersMode mode); + + /// Returns whether or not the client request had a `Fastly-Key` header + /// which is valid for purging content for the service. + /// + /// This function ignores the current value of any `Fastly-Key` header for + /// this request. + bool fastly_key_is_valid(); + + // void handoff_websocket(fastly::backend::Backend backend); + // void handoff_fanout(fastly::backend::Backend backend); + // Request *on_behalf_of(std::string_view service); + + /// Set the cache key to be used when attempting to satisfy this request + /// from a cached response. + void set_cache_key(std::string_view key); + + /// Set the cache key to be used when attempting to satisfy this request + /// from a cached response. + void set_cache_key(std::vector key); + + /// Builder-style equivalent of `Request::set_cache_key()`. + Request with_cache_key(std::string_view key) &&; + + /// Builder-style equivalent of `Request::set_cache_key()`. + Request with_cache_key(std::vector key) &&; + + /// Gets whether the request is potentially cacheable. + bool is_cacheable(); + + private: + auto &inner() { return req; } + Request(rust::Box r) : req(std::move(r)) {}; + rust::Box req; + }; } // namespace fastly::http -namespace fastly { -using fastly::http::Request; +namespace fastly +{ + using fastly::http::Request; } #endif diff --git a/include/fastly/http/response.h b/include/fastly/http/response.h index 9342597..ef270fa 100644 --- a/include/fastly/http/response.h +++ b/include/fastly/http/response.h @@ -9,560 +9,568 @@ #include #include #include +#include #include #include #include #include #include -namespace fastly::backend { -class Backend; +namespace fastly::backend +{ + class Backend; } -namespace fastly::http { - -class Body; -class StreamingBody; -class Response; -class Request; -namespace request { -class PendingRequest; -std::pair, std::vector> -select(std::vector &reqs); -} // namespace request - -/// An HTTP response, including body, headers, and status code. -/// -/// # Sending to the client -/// -/// Each execution of a Compute program may send a single response back to the -/// client: -/// -/// - `Response::send_to_client()` -/// - `Response::stream_to_client()` -/// -/// If no response is explicitly sent by the program, a default `200 OK` -/// response is sent. -/// -/// # Creation and conversion -/// -/// Responses can be created programmatically: -/// -/// - `Response::new()` -/// - `Response::from_body()` -/// - `Response::from_status()` -/// -/// Responses are also returned from backend requests: -/// -/// - `Request::send()` -/// - `Request::send_async()` -/// - `Request::send_async_streaming()` -/// -/// # Builder-style methods -/// -/// `Response` can be used as a -/// [builder](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html), -/// allowing responses to be constructed and used through method chaining. -/// Methods with the `with_` name prefix, such as `Response::with_header()`, -/// return `std::move(*this)` to allow chaining. The builder style is typically -/// most useful when constructing and using a response in a single expression. -/// For example: -/// -/// ```cpp -/// Response() -/// .with_header("my-header", "hello!") -/// .with_header("my-other-header", "Здравствуйте!") -/// .send_to_client(); -/// ``` -/// -/// # Setter methods -/// -/// Setter methods, such as `Response::set_header()`, are prefixed by `set_`, -/// and can be used interchangeably with the builder-style methods, allowing you -/// to mix and match styles based on what is most convenient for your program. -/// Setter methods tend to work better than builder-style methods when -/// constructing a value involves conditional branches or loops. For example: -/// -/// ```cpp -/// auto resp{Response().with_header("my-header", "hello!")}; -/// if (needs_translation) { -/// resp.set_header("my-other-header", "Здравствуйте!"); -/// } -/// resp.send_to_client(); -/// ``` -class Response { - friend Request; - friend request::PendingRequest; - friend std::pair, - std::vector> - request::select(std::vector &reqs); - -public: - /// Create a new `Response`. - /// - /// The new response is created with status code `200 OK`, no headers, and an - /// empty body. - Response(); - // TODO(@zkat): Make this a "friend"? - /// Return whether the response is from a backend request. - bool is_from_backend(); - - /// Make a new response with the same headers, status, and version of this - /// response, but no body. - /// - /// If you also need to clone the response body, use - /// `Response::clone_with_body()`. - Response clone_without_body(); - - /// Clone this response by reading in its body, and then writing the same body - /// to the original and the cloned response. - /// - /// This method requires mutable access to this response because reading from - /// and writing to the body can involve an HTTP connection. - Response clone_with_body(); - - /// Create a new `Response` with the given value as the body. - static Response from_body(Body body); - - /// Create a new response with the given status code. - static Response from_status(StatusCode status); - - /// Create a 303 See Other response with the given value as the `Location` - /// header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::see_other("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::SEE_OTHER); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response see_other(std::string_view destination); - - /// Create a 308 Permanent Redirect response with the given value as the - /// `Location` header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::redirect("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::PERMANENT_REDIRECT); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response redirect(std::string_view destination); - - /// Create a 307 Temporary Redirect response with the given value as the - /// `Location` header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::temporary_redirect("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::TEMPORARY_REDIRECT); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response temporary_redirect(std::string_view destination); - - /// Builder-style equivalent of `Response::set_body()`. - Response with_body(Body body) &&; - - /// Returns `true` if this response has a body. - bool has_body(); - - /// Set the given value as the response's body. - void set_body(Body body); - - /// Take and return the body from this response. - /// - /// After calling this method, this response will no longer have a body. - Body take_body(); - - /// Append another `Body` to the body of this response without reading or - /// writing any body contents. - /// - /// If this response does not have a body, the appended body is set as the - /// response's body. - /// - /// This method should be used when combining bodies that have not necessarily - /// been read yet, such as a body returned from a backend response. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::from_body("hello! backend says: ")}; - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// resp.append_body(backend_resp.into_body()); - /// resp.send_to_client(); - /// ``` - void append_body(Body body); +namespace fastly::http +{ - /// Consume the response and return its body as a byte vector. - std::vector into_body_bytes(); + class Body; + class StreamingBody; + class Response; + class Request; + namespace request + { + class PendingRequest; + std::pair, std::vector> + select(std::vector &reqs); + } // namespace request - /// Consume the response and return its body as a string. - std::string into_body_string(); - - /// Consume the response and return its body. - Body into_body(); - - /// Builder-style equivalent of - /// `Response::set_body_text_plain()`. - fastly::expected with_body_text_plain(std::string_view body) &&; - - /// Set the given string as the response's body with content type `text/plain; - /// charset=UTF-8`. - fastly::expected set_body_text_plain(std::string_view body); - - /// Builder-style equivalent of - /// `Response::set_body_text_html()`. - fastly::expected with_body_text_html(std::string_view body) &&; - - /// Set the given string as the response's body with content type `text/html; - /// charset=UTF-8`. - fastly::expected set_body_text_html(std::string_view body); - - /// Take and return the body from this response as a string. - /// - /// After calling this method, this response will no longer have a body. - std::string take_body_string(); - - /// Builder-style equivalent of - /// `Response::set_body_octet_stream()`. - Response with_body_octet_stream(std::vector body) &&; - - /// Set the given bytes as the response's body with content type - /// `application/octet-stream`. - void set_body_octet_stream(std::vector body); - - /// Take and return the body from this response as a vector of bytes. - /// - /// After calling this method, this response will no longer have a body. - std::vector take_body_bytes(); - - /// Get the MIME type described by the response's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header, or `std::nullopt` if that header is absent or contains an invalid - /// MIME type. - std::optional get_content_type(); - - /// Builder-style equivalent of - /// `Response::set_content_type()`. - Response with_content_type(std::string_view mime) &&; - - /// Set the MIME type described by the response's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header. - /// - /// Any existing `Content-Type` header values will be overwritten. - void set_content_type(std::string_view mime); - - /// Get the value of the response's - /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) - /// header, if it exists. - std::optional get_content_length(); - - /// Returns whether the given header name is present in the response. - fastly::expected contains_header(std::string_view name); - - /// Builder-style equivalent of `Response::append_header()`. - fastly::expected with_header(std::string_view name, - std::string_view value) &&; - - /// Builder-style equivalent of `Response::set_header()`. - fastly::expected with_set_header(std::string_view name, - std::string_view value) &&; - - /// Get the value of a header, or `std::nullopt` if the header is - /// not present. + /// An HTTP response, including body, headers, and status code. /// - /// If there are multiple values for the header, only one is returned, which - /// may be any of the values. See - /// `Response::get_header_all()` - /// all of the values. - fastly::expected> - get_header(std::string_view name); - - /// Get an iterator of all the values of a header. - fastly::expected get_header_all(std::string_view name); - fastly::expected get_headers(); - fastly::expected get_header_names(); - - /// Set a response header to the given value, discarding any previous values - /// for the given header name. - fastly::expected set_header(std::string_view name, - std::string_view value); - - /// Add a request header with given value. + /// # Sending to the client /// - /// Unlike `Response::set_header()`, this does not discard existing values for - /// the same header name. - fastly::expected append_header(std::string_view name, - std::string_view value); - - /// Remove all request headers of the given name, and return one of the - /// removed header values if any were present. - fastly::expected> - remove_header(std::string_view name); - - /// Builder-style equivalent of `Response::set_status()`. - Response with_status(StatusCode status) &&; - - /// Set the HTTP status code of the response. + /// Each execution of a Compute program may send a single response back to the + /// client: /// - /// # Examples + /// - `Response::send_to_client()` + /// - `Response::stream_to_client()` /// - /// Using the constants from `StatusCode`: + /// If no response is explicitly sent by the program, a default `200 OK` + /// response is sent. /// - /// ```cpp - /// auto resp{fastly::Response::from_body("not found!")}; - /// resp.set_status(fastly::http::StatusCode::NOT_FOUND); - /// resp.send_to_client(); - /// ``` + /// # Creation and conversion /// - /// Using a `uint16_t`: + /// Responses can be created programmatically: /// - /// ```cpp - /// auto resp{fastly::Response::from_body("not found!")}; - /// resp.set_status(404); - /// resp.send_to_client(); - /// ``` - void set_status(StatusCode status); - - /// Builder-style equivalent of `Response::set_version()`. - Response with_version(Version version) &&; - - /// Get the HTTP version of this response. - Version get_version(); - - /// Set the HTTP version of this response. - void set_version(Version version); - - // TODO(@zkat): needs enum - // void set_framing_headers_mode(FramingHeadersMode mode); - // Response set_framing_headers_mode(FramingHeadersMode mode); - - /// Get the name of the `Backend` this response came from, or `std::nullopt` - /// if the response is synthetic. + /// - `Response::new()` + /// - `Response::from_body()` + /// - `Response::from_status()` /// - /// # Examples + /// Responses are also returned from backend requests: /// - /// From a backend response: + /// - `Request::send()` + /// - `Request::send_async()` + /// - `Request::send_async_streaming()` /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com/").send("example_backend")}; - /// assert(backend_resp.get_backend_name(), - /// std::optional("example_backend")); - /// ``` + /// # Builder-style methods /// - /// From a synthetic response: + /// `Response` can be used as a + /// [builder](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html), + /// allowing responses to be constructed and used through method chaining. + /// Methods with the `with_` name prefix, such as `Response::with_header()`, + /// return `std::move(*this)` to allow chaining. The builder style is typically + /// most useful when constructing and using a response in a single expression. + /// For example: /// /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend_name() == std::nullopt); + /// Response() + /// .with_header("my-header", "hello!") + /// .with_header("my-other-header", "Здравствуйте!") + /// .send_to_client(); /// ``` - std::optional get_backend_name(); - - /// Get the backend this response came from, or `std::nullopt` if the response - /// is synthetic. /// - /// # Examples + /// # Setter methods /// - /// From a backend response: + /// Setter methods, such as `Response::set_header()`, are prefixed by `set_`, + /// and can be used interchangeably with the builder-style methods, allowing you + /// to mix and match styles based on what is most convenient for your program. + /// Setter methods tend to work better than builder-style methods when + /// constructing a value involves conditional branches or loops. For example: /// /// ```cpp - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// assert( - /// backend_resp.get_backend() == - /// std::optional(Backend::from_name("example_backend")) - /// ); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend() == std::nullopt); - /// ``` - std::optional get_backend(); - - /// Get the address of the backend this response came from, or `std::nullopt` - /// when the response is synthetic or cached. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto backend_resp{Request::get("https://example.com/") - /// .with_pass(true) - /// .send("example_backend")}; - /// assert( - /// backend_resp.get_backend_addr() == - /// std::optional("127.0.0.1:443")); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend_addr() == std::nullopt); - /// ``` - std::optional get_backend_addr(); - - /// Take and return the request this response came from, or `std::nullopt` if - /// the response is synthetic. - /// - /// Note that the returned request will only have the headers and metadata of - /// the original request, as the body is consumed when sending the request. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto backend_resp{Request::post("https://example.com/") - /// .with_body("hello") - /// .send("example_backend")}; - /// auto backend_req{backend_resp.take_backend_request().value()}; - /// assert(backend_req.get_url() == "https://example.com/"); - /// assert(!backend_req.has_body()); - /// backend_req.with_body("goodbye").send("example_backend"); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.take_backend_request() == std::nullopt); - /// ``` - std::optional take_backend_request(); - - /// Begin sending the response to the client. - /// - /// This method returns as soon as the response header begins sending to the - /// client, and transmission of the response will continue in the background. - /// - /// Once this method is called, nothing else may be added to the response - /// body. To stream additional data to a response body after it begins to - /// send, use `Response::stream_to_client()`. - /// - /// # Panics - /// - /// This method panics if another response has already been sent to the client - /// by this method, by `Response::stream_to_client()`. - /// - /// # Examples - /// - /// Sending a backend response without modification: - /// - /// ```cpp - /// Request::get("https://example.com/").send("example_backend").send_to_client(); - /// ``` - /// - /// Removing a header from a backend response before sending to the client: - /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com/").send("example_backend")}; - /// backend_resp.remove_header("bad-header"); - /// backend_resp.send_to_client(); - /// ``` - /// - /// Sending a synthetic response: - /// - /// ```cpp - /// Response::from_body("hello, world!").send_to_client(); - /// ``` - void send_to_client(); - - /// Begin sending the response to the client, and return a `StreamingBody` - /// that can accept further data to send. - /// - /// The client connection must be closed when finished writing the response by - /// calling `StreamingBody::finish()`. - /// - /// This method is most useful for programs that do some sort of processing or - /// inspection of a potentially-large backend response body. Streaming allows - /// the program to operate on small parts of the body rather than having to - /// read it all into memory at once. - /// - /// This method returns as soon as the response header begins sending to the - /// client, and transmission of the response will continue in the background. - /// - /// # Panics - /// - /// This method panics if another response has already been sent to the client - /// by this method, by `Response::send_to_client()`. - /// - /// # Examples - /// - /// Count the number of lines in a UTF-8 backend response body while sending - /// it to the client: - /// - /// ```cpp - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// - /// // Take the body so we can iterate through its lines later - /// auto backend_resp_body{backend_resp.take_body()}; - /// - /// // Start sending the backend response to the client with a now-empty body - /// auto client_body{backend_resp.stream_to_client()}; - /// - /// size_t num_lines{0}; - /// std::string line; - /// while (getline(backend_resp_body, line)) { - /// num_lines++; - /// client_body << line; + /// auto resp{Response().with_header("my-header", "hello!")}; + /// if (needs_translation) { + /// resp.set_header("my-other-header", "Здравствуйте!"); /// } - /// // Finish the streaming body to close the client connection. - /// client_body.finish(); - /// - /// std::cout - /// << "backend response body contained " - /// << num_lines - /// << " lines" - /// << std::endl; + /// resp.send_to_client(); /// ``` - StreamingBody stream_to_client(); - - /// Get the Time to Live (TTL) in the cache for this response, if it is - /// cached. - /// - /// The TTL provides the duration of "freshness" for the cached response - /// after it is inserted into the cache. If the response is stale, - /// the TTL is 0 (i.e. this returns `std::optional(0)`. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_ttl(); - - /// The current age of the response, if it is cached. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_age(); - - /// The time for which the response can safely be used despite being - /// considered stale, if it is cached. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_stale_while_revalidate(); - -private: - Response(rust::Box response) - : res(std::move(response)) {}; - rust::Box res; -}; + class Response + { + friend detail::AccessBridgeInternals; + friend Request; + friend request::PendingRequest; + friend std::pair, + std::vector> + request::select(std::vector &reqs); + + public: + /// Create a new `Response`. + /// + /// The new response is created with status code `200 OK`, no headers, and an + /// empty body. + Response(); + // TODO(@zkat): Make this a "friend"? + /// Return whether the response is from a backend request. + bool is_from_backend(); + + /// Make a new response with the same headers, status, and version of this + /// response, but no body. + /// + /// If you also need to clone the response body, use + /// `Response::clone_with_body()`. + Response clone_without_body(); + + /// Clone this response by reading in its body, and then writing the same body + /// to the original and the cloned response. + /// + /// This method requires mutable access to this response because reading from + /// and writing to the body can involve an HTTP connection. + Response clone_with_body(); + + /// Create a new `Response` with the given value as the body. + static Response from_body(Body body); + + /// Create a new response with the given status code. + static Response from_status(StatusCode status); + + /// Create a 303 See Other response with the given value as the `Location` + /// header. + /// + /// # Examples + /// + /// ```cpp + /// auto resp{Response::see_other("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::SEE_OTHER); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response see_other(std::string_view destination); + + /// Create a 308 Permanent Redirect response with the given value as the + /// `Location` header. + /// + /// # Examples + /// + /// ```cpp + /// auto resp{Response::redirect("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::PERMANENT_REDIRECT); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response redirect(std::string_view destination); + + /// Create a 307 Temporary Redirect response with the given value as the + /// `Location` header. + /// + /// # Examples + /// + /// ```cpp + /// auto resp{Response::temporary_redirect("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::TEMPORARY_REDIRECT); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response temporary_redirect(std::string_view destination); + + /// Builder-style equivalent of `Response::set_body()`. + Response with_body(Body body) &&; + + /// Returns `true` if this response has a body. + bool has_body(); + + /// Set the given value as the response's body. + void set_body(Body body); + + /// Take and return the body from this response. + /// + /// After calling this method, this response will no longer have a body. + Body take_body(); + + /// Append another `Body` to the body of this response without reading or + /// writing any body contents. + /// + /// If this response does not have a body, the appended body is set as the + /// response's body. + /// + /// This method should be used when combining bodies that have not necessarily + /// been read yet, such as a body returned from a backend response. + /// + /// # Examples + /// + /// ```cpp + /// auto resp{Response::from_body("hello! backend says: ")}; + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// resp.append_body(backend_resp.into_body()); + /// resp.send_to_client(); + /// ``` + void append_body(Body body); + + /// Consume the response and return its body as a byte vector. + std::vector into_body_bytes(); + + /// Consume the response and return its body as a string. + std::string into_body_string(); + + /// Consume the response and return its body. + Body into_body(); + + /// Builder-style equivalent of + /// `Response::set_body_text_plain()`. + fastly::expected with_body_text_plain(std::string_view body) &&; + + /// Set the given string as the response's body with content type `text/plain; + /// charset=UTF-8`. + fastly::expected set_body_text_plain(std::string_view body); + + /// Builder-style equivalent of + /// `Response::set_body_text_html()`. + fastly::expected with_body_text_html(std::string_view body) &&; + + /// Set the given string as the response's body with content type `text/html; + /// charset=UTF-8`. + fastly::expected set_body_text_html(std::string_view body); + + /// Take and return the body from this response as a string. + /// + /// After calling this method, this response will no longer have a body. + std::string take_body_string(); + + /// Builder-style equivalent of + /// `Response::set_body_octet_stream()`. + Response with_body_octet_stream(std::vector body) &&; + + /// Set the given bytes as the response's body with content type + /// `application/octet-stream`. + void set_body_octet_stream(std::vector body); + + /// Take and return the body from this response as a vector of bytes. + /// + /// After calling this method, this response will no longer have a body. + std::vector take_body_bytes(); + + /// Get the MIME type described by the response's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header, or `std::nullopt` if that header is absent or contains an invalid + /// MIME type. + std::optional get_content_type(); + + /// Builder-style equivalent of + /// `Response::set_content_type()`. + Response with_content_type(std::string_view mime) &&; + + /// Set the MIME type described by the response's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header. + /// + /// Any existing `Content-Type` header values will be overwritten. + void set_content_type(std::string_view mime); + + /// Get the value of the response's + /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) + /// header, if it exists. + std::optional get_content_length(); + + /// Returns whether the given header name is present in the response. + fastly::expected contains_header(std::string_view name); + + /// Builder-style equivalent of `Response::append_header()`. + fastly::expected with_header(std::string_view name, + std::string_view value) &&; + + /// Builder-style equivalent of `Response::set_header()`. + fastly::expected with_set_header(std::string_view name, + std::string_view value) &&; + + /// Get the value of a header, or `std::nullopt` if the header is + /// not present. + /// + /// If there are multiple values for the header, only one is returned, which + /// may be any of the values. See + /// `Response::get_header_all()` + /// all of the values. + fastly::expected> + get_header(std::string_view name); + + /// Get an iterator of all the values of a header. + fastly::expected get_header_all(std::string_view name); + fastly::expected get_headers(); + fastly::expected get_header_names(); + + /// Set a response header to the given value, discarding any previous values + /// for the given header name. + fastly::expected set_header(std::string_view name, + std::string_view value); + + /// Add a request header with given value. + /// + /// Unlike `Response::set_header()`, this does not discard existing values for + /// the same header name. + fastly::expected append_header(std::string_view name, + std::string_view value); + + /// Remove all request headers of the given name, and return one of the + /// removed header values if any were present. + fastly::expected> + remove_header(std::string_view name); + + /// Builder-style equivalent of `Response::set_status()`. + Response with_status(StatusCode status) &&; + + /// Set the HTTP status code of the response. + /// + /// # Examples + /// + /// Using the constants from `StatusCode`: + /// + /// ```cpp + /// auto resp{fastly::Response::from_body("not found!")}; + /// resp.set_status(fastly::http::StatusCode::NOT_FOUND); + /// resp.send_to_client(); + /// ``` + /// + /// Using a `uint16_t`: + /// + /// ```cpp + /// auto resp{fastly::Response::from_body("not found!")}; + /// resp.set_status(404); + /// resp.send_to_client(); + /// ``` + void set_status(StatusCode status); + + /// Builder-style equivalent of `Response::set_version()`. + Response with_version(Version version) &&; + + /// Get the HTTP version of this response. + Version get_version(); + + /// Set the HTTP version of this response. + void set_version(Version version); + + // TODO(@zkat): needs enum + // void set_framing_headers_mode(FramingHeadersMode mode); + // Response set_framing_headers_mode(FramingHeadersMode mode); + + /// Get the name of the `Backend` this response came from, or `std::nullopt` + /// if the response is synthetic. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto + /// backend_resp{Request::get("https://example.com/").send("example_backend")}; + /// assert(backend_resp.get_backend_name(), + /// std::optional("example_backend")); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend_name() == std::nullopt); + /// ``` + std::optional get_backend_name(); + + /// Get the backend this response came from, or `std::nullopt` if the response + /// is synthetic. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// assert( + /// backend_resp.get_backend() == + /// std::optional(Backend::from_name("example_backend")) + /// ); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend() == std::nullopt); + /// ``` + std::optional get_backend(); + + /// Get the address of the backend this response came from, or `std::nullopt` + /// when the response is synthetic or cached. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{Request::get("https://example.com/") + /// .with_pass(true) + /// .send("example_backend")}; + /// assert( + /// backend_resp.get_backend_addr() == + /// std::optional("127.0.0.1:443")); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend_addr() == std::nullopt); + /// ``` + std::optional get_backend_addr(); + + /// Take and return the request this response came from, or `std::nullopt` if + /// the response is synthetic. + /// + /// Note that the returned request will only have the headers and metadata of + /// the original request, as the body is consumed when sending the request. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{Request::post("https://example.com/") + /// .with_body("hello") + /// .send("example_backend")}; + /// auto backend_req{backend_resp.take_backend_request().value()}; + /// assert(backend_req.get_url() == "https://example.com/"); + /// assert(!backend_req.has_body()); + /// backend_req.with_body("goodbye").send("example_backend"); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.take_backend_request() == std::nullopt); + /// ``` + std::optional take_backend_request(); + + /// Begin sending the response to the client. + /// + /// This method returns as soon as the response header begins sending to the + /// client, and transmission of the response will continue in the background. + /// + /// Once this method is called, nothing else may be added to the response + /// body. To stream additional data to a response body after it begins to + /// send, use `Response::stream_to_client()`. + /// + /// # Panics + /// + /// This method panics if another response has already been sent to the client + /// by this method, by `Response::stream_to_client()`. + /// + /// # Examples + /// + /// Sending a backend response without modification: + /// + /// ```cpp + /// Request::get("https://example.com/").send("example_backend").send_to_client(); + /// ``` + /// + /// Removing a header from a backend response before sending to the client: + /// + /// ```cpp + /// auto + /// backend_resp{Request::get("https://example.com/").send("example_backend")}; + /// backend_resp.remove_header("bad-header"); + /// backend_resp.send_to_client(); + /// ``` + /// + /// Sending a synthetic response: + /// + /// ```cpp + /// Response::from_body("hello, world!").send_to_client(); + /// ``` + void send_to_client(); + + /// Begin sending the response to the client, and return a `StreamingBody` + /// that can accept further data to send. + /// + /// The client connection must be closed when finished writing the response by + /// calling `StreamingBody::finish()`. + /// + /// This method is most useful for programs that do some sort of processing or + /// inspection of a potentially-large backend response body. Streaming allows + /// the program to operate on small parts of the body rather than having to + /// read it all into memory at once. + /// + /// This method returns as soon as the response header begins sending to the + /// client, and transmission of the response will continue in the background. + /// + /// # Panics + /// + /// This method panics if another response has already been sent to the client + /// by this method, by `Response::send_to_client()`. + /// + /// # Examples + /// + /// Count the number of lines in a UTF-8 backend response body while sending + /// it to the client: + /// + /// ```cpp + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// + /// // Take the body so we can iterate through its lines later + /// auto backend_resp_body{backend_resp.take_body()}; + /// + /// // Start sending the backend response to the client with a now-empty body + /// auto client_body{backend_resp.stream_to_client()}; + /// + /// size_t num_lines{0}; + /// std::string line; + /// while (getline(backend_resp_body, line)) { + /// num_lines++; + /// client_body << line; + /// } + /// // Finish the streaming body to close the client connection. + /// client_body.finish(); + /// + /// std::cout + /// << "backend response body contained " + /// << num_lines + /// << " lines" + /// << std::endl; + /// ``` + StreamingBody stream_to_client(); + + /// Get the Time to Live (TTL) in the cache for this response, if it is + /// cached. + /// + /// The TTL provides the duration of "freshness" for the cached response + /// after it is inserted into the cache. If the response is stale, + /// the TTL is 0 (i.e. this returns `std::optional(0)`. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_ttl(); + + /// The current age of the response, if it is cached. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_age(); + + /// The time for which the response can safely be used despite being + /// considered stale, if it is cached. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_stale_while_revalidate(); + + private: + auto &inner() { return res; } + Response(rust::Box response) + : res(std::move(response)) {}; + rust::Box res; + }; } // namespace fastly::http -namespace fastly { -using fastly::http::Response; +namespace fastly +{ + using fastly::http::Response; } #endif diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp index 00ae037..35c4c3f 100644 --- a/src/cpp/esi.cpp +++ b/src/cpp/esi.cpp @@ -1,37 +1,92 @@ #include +#include +#include namespace fastly::esi { - - Processor::Processor(std::optional original_request_metadata = std::nullopt, - Configuration config = Configuration()) + extern "C" uint32_t fastly$esi$manualbridge$DispatchFragmentRequestFn$call(const DispatchFragmentRequestFn &fn, fastly::sys::http::Request *raw_req, fastly::sys::http::request::PendingRequest *&out_pending, fastly::sys::http::Response *&out_complete, fastly::sys::esi::ExecutionError *&) { - processor_ = fastly::sys::esi::m_esi_processor_new(original_request_metadata.has_value() ? &original_request_metadata->req : nullptr, - static_cast(config.get_namespace()), - config.is_escaped_content()); + auto req = detail::AccessBridgeInternals::from_raw(raw_req); + auto res = detail::AccessBridgeInternals::get(fn)(std::move(req)); + if (!res) + { + // TODO + return 0; // Error + } + if (std::holds_alternative(*res)) + { + out_pending = detail::AccessBridgeInternals::get(std::get(*res)).into_raw(); + return 1; // Pending response + } + else if (std::holds_alternative(*res)) + { + out_complete = detail::AccessBridgeInternals::get(std::get(*res)).into_raw(); + return 2; // Complete Response + } + else + { + return 3; // No content + } } - uint32_t DispatchFragmentRequestFn::call(Request req, http::PendingResponse *&out_pending, http::Response *&out_complete, ExecutionError *&out_error) const noexcept; + extern "C" bool fastly$esi$manualbridge$ProcessFragmentResponseFn$call(const ProcessFragmentResponseFn &fn, fastly::sys::http::Request *raw_req, fastly::sys::http::Response *raw_resp, fastly::sys::http::Response *&out_resp, fastly::sys::esi::ExecutionError *&) { - auto res = fn_(std::move(req)); - if (!res) + // DANGER: This call is very unsafe. We are *not* taking ownership of the + // request as it's supposed to be passed as a borrow. However, the existing + // Request type in C++ always takes ownership of the inner pointer, so we + // store the the request in a box temporarily to avoid having to create + // an entirely new wrapper type just for this. As such, we *must* release + // the box without dropping it after the call to the user function, otherwise + // we will double-free the inner pointer. + auto req = detail::AccessBridgeInternals::from_raw(raw_req); + + auto resp = detail::AccessBridgeInternals::from_raw(raw_resp); + auto res = detail::AccessBridgeInternals::get(fn)(req, std::move(resp)); + + // DANGER: Here we release the box without dropping it, to avoid double-freeing + detail::AccessBridgeInternals::get(req).into_raw(); + + if (res) { - out_error = new ExecutionError(res.error()); - return 3; // Error + out_resp = detail::AccessBridgeInternals::get(*res).into_raw(); + return true; } - if (std::holds_alternative(*res)) + else { - out_pending = new PendingResponse(std::get(*res)); - return 0; // Pending response + // TODO + return false; } - else if (std::holds_alternative(*res)) + } + + Processor::Processor(std::optional original_request_metadata, + Configuration config) + : processor_(fastly::sys::esi::m_static_esi_processor_new(original_request_metadata.has_value() ? &detail::AccessBridgeInternals::get(*original_request_metadata) : nullptr, + static_cast(config.get_namespace()), + config.is_escaped_content())) + { + } + + tl::expected Processor::process_response( + Response &src_document, + std::optional client_response_metadata, + std::optional dispatch_fragment_request, + std::optional process_fragment_response) + { + fastly::sys::esi::ExecutionError *err = nullptr; + bool success = fastly::sys::esi::m_esi_processor_process_response( + std::move(processor_), + *detail::AccessBridgeInternals::get(src_document), + client_response_metadata.has_value() ? &detail::AccessBridgeInternals::get(*client_response_metadata) : nullptr, + dispatch_fragment_request.has_value() ? reinterpret_cast(&*dispatch_fragment_request) : nullptr, + process_fragment_response.has_value() ? reinterpret_cast(&*process_fragment_response) : nullptr, + err); + if (success) { - out_complete = new Response(std::get(*res)); - return 1; // Complete Response + return {}; } else { - return 2; // No content + return tl::unexpected(ExecutionError()); } } } \ No newline at end of file diff --git a/src/esi.rs b/src/esi.rs index 856ebbb..09f206d 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -9,7 +9,6 @@ use esi::Configuration; use crate::{ error::FastlyError, - ffi::{DispatchFragmentRequestFn, ProcessFragmentResponseFn}, http::{request::Request, response::Response}, try_fe, }; @@ -18,6 +17,9 @@ pub(crate) struct ExecutionError(esi::ExecutionError); pub(crate) struct Processor(esi::Processor); +pub(crate) struct DispatchFragmentRequestFn; +pub(crate) struct ProcessFragmentResponseFn; + pub fn m_esi_processor_process_response( processor: Box, src_document: &mut Response, @@ -42,21 +44,24 @@ pub fn m_esi_processor_process_response( let mut out_pending = ptr::null_mut(); let mut out_completed = ptr::null_mut(); let mut err = ptr::null_mut(); - let result = func.call( - Box::new(Request(req)), - &mut out_pending, - &mut out_completed, - &mut err, - ); + let result = unsafe { + crate::manual_ffi::fastly_esi_manualbridge_DispatchFragmentRequestFn_call( + &func, + Box::into_raw(Box::new(Request(req))), + &mut out_pending, + &mut out_completed, + &mut err, + ) + }; match result { - 0 => Ok(unsafe { + 1 => Ok(unsafe { esi::PendingFragmentContent::PendingRequest(out_pending.read().0) }), - 1 => Ok(unsafe { + 2 => Ok(unsafe { esi::PendingFragmentContent::CompletedRequest(out_completed.read().0) }), - 2 => Ok(esi::PendingFragmentContent::NoContent), - 3 => Err(unsafe { err.read().0 }), + 3 => Ok(esi::PendingFragmentContent::NoContent), + 0 => Err(unsafe { err.read().0 }), _ => unreachable!(), } }); @@ -65,18 +70,36 @@ pub fn m_esi_processor_process_response( let process_fragment_response = if process_fragment_response.is_null() { None } else { - let shim: &dyn Fn( - &mut fastly::Request, - fastly::Response, - ) -> Result = - &|req, resp| Err(esi::ExecutionError::UnexpectedEndOfDocument); + let func = unsafe { process_fragment_response.read() }; + let shim: Box< + dyn Fn( + &mut fastly::Request, + fastly::Response, + ) -> Result, + > = Box::new(move |req, resp| { + let mut out_resp = ptr::null_mut(); + let mut err = ptr::null_mut(); + let result = unsafe { + crate::manual_ffi::fastly_esi_manualbridge_ProcessFragmentResponseFn_call( + &func, + Box::into_raw(Box::new(Request(req.clone_with_body()))), //TODO this leaks currently + Box::into_raw(Box::new(Response(resp))), + &mut out_resp, + &mut err, + ) + }; + match result { + true => Ok(unsafe { out_resp.read().0 }), + false => Err(unsafe { err.read().0 }), + } + }); Some(shim) }; match processor.0.process_response( &mut src_document.0, client_response_metadata, dispatch_fragment_request.as_deref(), - process_fragment_response, + process_fragment_response.as_deref(), ) { Ok(_) => { err.set(ptr::null_mut()); diff --git a/src/lib.rs b/src/lib.rs index d86de7d..e41d58d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1027,26 +1027,12 @@ mod ffi { } #[namespace = "fastly::sys::esi"] - unsafe extern "C++" { - include!("fastly/esi.h"); + extern "Rust" { type DispatchFragmentRequestFn; - // The return value is: - // 0 on pending - // 1 on completed - // 2 on no content - // 3 on error - fn call( - &self, - req: Box, - out_pending: &mut *mut PendingRequest, - out_completed: &mut *mut Response, - err: &mut *mut ExecutionError, - ) -> u32; } #[namespace = "fastly::sys::esi"] - unsafe extern "C++" { - include!("fastly/esi.h"); + extern "Rust" { type ProcessFragmentResponseFn; } @@ -1063,8 +1049,32 @@ mod ffi { ) -> bool; pub unsafe fn m_static_esi_processor_new( original_request_metadata: *mut Box, - namespace: &CxxString, + namespc: &CxxString, is_escaped_content: bool, ) -> Box; } } + +mod manual_ffi { + use crate::esi::DispatchFragmentRequestFn; + + unsafe extern "C" { + #[link_name = "fastly$esi$manualbridge$DispatchFragmentRequestFn$call"] + pub(crate) fn fastly_esi_manualbridge_DispatchFragmentRequestFn_call( + func: *const DispatchFragmentRequestFn, + req: *mut crate::Request, + out_pending: &mut *mut crate::PendingRequest, + out_complete: &mut *mut crate::Response, + out_error: &mut *mut crate::ExecutionError, + ) -> u32; + + #[link_name = "fastly$esi$manualbridge$ProcessFragmentResponseFn$call"] + pub(crate) fn fastly_esi_manualbridge_ProcessFragmentResponseFn_call( + func: *const crate::esi::ProcessFragmentResponseFn, + request: *mut crate::Request, + response: *mut crate::Response, + out_response: &mut *mut crate::Response, + out_error: &mut *mut crate::ExecutionError, + ) -> bool; + } +} From 8db8cb1fceda77227fde2e521b8d4bce28e185a4 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 16:46:04 +0100 Subject: [PATCH 05/17] Working ESI --- examples/esi.cpp | 84 ++++------- examples/fastly.toml | 12 +- include/fastly/detail/rust_bridge_tags.h | 22 +++ include/fastly/esi.h | 130 ++++++++-------- src/cpp/esi.cpp | 183 +++++++++++++---------- src/error.rs | 3 + src/esi.rs | 173 +++++++++++---------- src/lib.rs | 51 ++++--- 8 files changed, 344 insertions(+), 314 deletions(-) create mode 100644 include/fastly/detail/rust_bridge_tags.h diff --git a/examples/esi.cpp b/examples/esi.cpp index 2475251..b8a32cc 100644 --- a/examples/esi.cpp +++ b/examples/esi.cpp @@ -1,65 +1,31 @@ #include "fastly/sdk.h" -const auto html = R"( - - - My Shopping Website - - -
-

My Shopping Website

-
-
- - - - -
- - -)"; +int main() { + fastly::log::init_simple("logs", fastly::log::LogLevelFilter::Debug); + auto req{fastly::http::Request::from_client()}; + auto bereq = fastly::http::Request(fastly::http::Method::GET, + "https://esi-cpp-demo.edgecompute.app/") + .with_auto_decompress_gzip(true); + auto beresp = bereq.clone_without_body().send("esi-cpp-demo").value(); + auto names = req.get_header_names(); -auto to_string(fastly::http::Method method) -{ - switch (method) - { - case fastly::http::Method::GET: - return "GET"; - case fastly::http::Method::POST: - return "POST"; - case fastly::http::Method::PUT: - return "PUT"; - case fastly::http::Method::DELETE: - return "DELETE"; - default: - return "UNKNOWN"; + // Pass in the request made to the backend as a template for fragment + // requests: this ensures that the fragment requests also ask for gzipped + // content and automatically gunzip the response content. + fastly::esi::Processor processor(std::move(bereq)); + auto dispatch_fragment_request = [](fastly::http::Request req) + -> std::optional { + auto pending = req.send_async("esi-cpp-demo"); + if (pending) { + return fastly::esi::PendingFragmentContent{std::move(*pending)}; + } else { + return std::nullopt; } -} - -int main() -{ - fastly::log::init_simple("logs"); - fastly::log::info("Starting ESI example"); - auto req{fastly::http::Request::from_client()}; - auto beresp = fastly::http::Response::from_body(html).with_content_type("text/html"); - fastly::esi::Processor processor(std::move(req)); - fastly::esi::DispatchFragmentRequestFn dispatch_fragment_request([](fastly::http::Request req) -> tl::expected - { - fastly::log::info("Sending request {} {}", to_string(req.get_method()), req.get_path()); - auto pending = std::move(req).with_ttl(120).send_async("mock-s3"); - if (pending) - { - return fastly::esi::PendingFragmentContent{std::move(*pending)}; - } - else - { - return tl::unexpected(fastly::esi::ExecutionError{}); - } }); - fastly::log::info("Processor created"); - fastly::esi::ProcessFragmentResponseFn process_fragment_response([](fastly::http::Request &req, fastly::http::Response resp) -> tl::expected - { - fastly::log::info("Received response for {} {}", to_string(req.get_method()), req.get_path()); + }; - return resp; }); - (void)processor.process_response(beresp, std::nullopt, dispatch_fragment_request, process_fragment_response); + auto result = processor.process_response( + beresp, std::nullopt, dispatch_fragment_request, std::nullopt); + if (!result) { + fastly::log::error("Failed to process response"); + } } diff --git a/examples/fastly.toml b/examples/fastly.toml index 721ee7b..679e5d2 100644 --- a/examples/fastly.toml +++ b/examples/fastly.toml @@ -9,11 +9,7 @@ name = "cpp-example" service_id = "" [local_server] -backends.fastly.url = "https://www.fastly.com" -backends.wikipedia.url = "https://en.wikipedia.org" - -[setup] -[setup.backends] -[setup.backends.mock-s3] -address = "mock-s3.edgecompute.app" -port = 443 +[local_server.backends] +fastly.url = "https://www.fastly.com" +wikipedia.url = "https://en.wikipedia.org" +esi-cpp-demo.url = "https://esi-cpp-demo.edgecompute.app" diff --git a/include/fastly/detail/rust_bridge_tags.h b/include/fastly/detail/rust_bridge_tags.h new file mode 100644 index 0000000..5c764d3 --- /dev/null +++ b/include/fastly/detail/rust_bridge_tags.h @@ -0,0 +1,22 @@ +// Some types (primarily callback types) must be defined in C++, but depend upon +// types defined in Rust. To break this circular dependency, we define empty +// "tag" structs here in C++ that can be referenced both from C++ and Rust, +// allowing Rust to pass pointers to these types back to C++ without needing to +// know their full definition. +// +// C++ types that implement the tags should inherit from the tag structs and the +// bindings should cast to/from the tag types as necessary. + +#ifndef FASTLY_DETAIL_RUST_BRIDGE_TAGS_H +#define FASTLY_DETAIL_RUST_BRIDGE_TAGS_H + +namespace fastly::detail::rust_bridge_tags { +namespace esi { +// esi.h:DispatchFragmentRequestFn +struct DispatchFragmentRequestFnTag {}; +// esi.h:ProcessFragmentResponseFn +struct ProcessFragmentResponseFnTag {}; +} // namespace esi +} // namespace fastly::detail::rust_bridge_tags + +#endif \ No newline at end of file diff --git a/include/fastly/esi.h b/include/fastly/esi.h index c7a8b92..7ddfb64 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -1,83 +1,87 @@ #ifndef FASTLY_ESI_H #define FASTLY_ESI_H -#include -#include -#include -#include -#include +#include +#include #include +#include #include #include -#include #include +#include +#include +#include +#include +#include -namespace fastly::esi -{ - /// Used to configure optional behaviour within the ESI processor. - struct Configuration - { - public: - /// Create a new configuration object. - /// \param namespc The namespace to use for ESI tags. Defaults to "esi". - /// \param is_escaped_content Whether to escape content by default. Defaults to true. - Configuration(std::string namespc = "esi", bool is_escaped_content = true) - : namespace_(std::move(namespc)), is_escaped_content_(is_escaped_content) {} - - std::string_view get_namespace() const { return namespace_; } - bool is_escaped_content() const { return is_escaped_content_; } +namespace fastly::esi { +/// Used to configure optional behaviour within the ESI processor. +struct Configuration { +public: + /// Create a new configuration object. + /// \param namespc The namespace to use for ESI tags. Defaults to "esi". + /// \param is_escaped_content Whether to escape content by default. Defaults + /// to true. + Configuration(std::string namespc = "esi", bool is_escaped_content = true) + : namespace_(std::move(namespc)), + is_escaped_content_(is_escaped_content) {} - private: - std::string namespace_; - bool is_escaped_content_; - }; + std::string_view get_namespace() const { return namespace_; } + bool is_escaped_content() const { return is_escaped_content_; } - class ExecutionError - { - }; +private: + std::string namespace_; + bool is_escaped_content_; +}; - using PendingFragmentContent = std::variant; +using PendingFragmentContent = + std::variant; - class DispatchFragmentRequestFn - { - public: - DispatchFragmentRequestFn(std::function(Request)> fn) - : fn_(std::move(fn)) {} +class DispatchFragmentRequestFn + : public detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag { +public: + using function_type = + std::function(Request)>; + template F> + DispatchFragmentRequestFn(F &&fn) : fn_(std::forward(fn)) {} - private: - friend detail::AccessBridgeInternals; - auto &inner() const { return fn_; } - std::function(Request)> fn_; - }; +private: + friend detail::AccessBridgeInternals; + auto &inner() const { return fn_; } + function_type fn_; +}; - class ProcessFragmentResponseFn - { - public: - ProcessFragmentResponseFn(std::function(Request &, Response)> fn) - : fn_(std::move(fn)) {} +class ProcessFragmentResponseFn + : public detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag { +public: + using function_type = + std::function(Request &, Response)>; + template F> + ProcessFragmentResponseFn(F &&fn) : fn_(std::forward(fn)) {} - private: - friend detail::AccessBridgeInternals; - auto &inner() const { return fn_; } - std::function(Request &, Response)> fn_; - }; +private: + friend detail::AccessBridgeInternals; + auto &inner() const { return fn_; } + function_type fn_; +}; - class Processor - { - public: - /// Create a new ESI processor with the given configuration. - Processor(std::optional original_request_metadata = std::nullopt, - Configuration config = Configuration()); +class Processor { +public: + /// Create a new ESI processor with the given configuration. + Processor(std::optional original_request_metadata = std::nullopt, + Configuration config = Configuration()); - tl::expected process_response( - Response &src_document, - std::optional client_response_metadata = std::nullopt, - std::optional dispatch_fragment_request = std::nullopt, - std::optional process_fragment_response = std::nullopt); + tl::expected process_response( + Response &src_document, + std::optional client_response_metadata = std::nullopt, + std::optional dispatch_fragment_request = + std::nullopt, + std::optional process_fragment_response = + std::nullopt); - private: - rust::Box processor_; - }; +private: + rust::Box processor_; +}; -} +} // namespace fastly::esi #endif \ No newline at end of file diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp index 35c4c3f..ae144ac 100644 --- a/src/cpp/esi.cpp +++ b/src/cpp/esi.cpp @@ -1,92 +1,109 @@ -#include #include +#include +#include +#include +#include #include -namespace fastly::esi -{ - extern "C" uint32_t fastly$esi$manualbridge$DispatchFragmentRequestFn$call(const DispatchFragmentRequestFn &fn, fastly::sys::http::Request *raw_req, fastly::sys::http::request::PendingRequest *&out_pending, fastly::sys::http::Response *&out_complete, fastly::sys::esi::ExecutionError *&) - { - auto req = detail::AccessBridgeInternals::from_raw(raw_req); - auto res = detail::AccessBridgeInternals::get(fn)(std::move(req)); - if (!res) - { - // TODO - return 0; // Error - } - if (std::holds_alternative(*res)) - { - out_pending = detail::AccessBridgeInternals::get(std::get(*res)).into_raw(); - return 1; // Pending response - } - else if (std::holds_alternative(*res)) - { - out_complete = detail::AccessBridgeInternals::get(std::get(*res)).into_raw(); - return 2; // Complete Response - } - else - { - return 3; // No content - } - } +namespace fastly::esi { +// These functions are called by Rust to invoke the C++ callbacks. +extern "C" uint32_t fastly$esi$manualbridge$DispatchFragmentRequestFn$call( + const detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag &fn_tag, + fastly::sys::http::Request *raw_req, + fastly::sys::http::request::PendingRequest *&out_pending, + fastly::sys::http::Response *&out_complete) { + auto req = detail::AccessBridgeInternals::from_raw(raw_req); + + // The real callback type is cast to this tag type before being passed in to + // Rust to break circular dependencies. It's safe to cast it back here, as if + // some other type was passed in, something has gone horribly wrong. + auto fn = static_cast(fn_tag); + + auto res = detail::AccessBridgeInternals::get(fn)(std::move(req)); + if (!res) { + return 0; // Error + } + if (std::holds_alternative(*res)) { + out_pending = detail::AccessBridgeInternals::get( + std::get(*res)) + .into_raw(); + return 1; // Pending response + } else if (std::holds_alternative(*res)) { + out_complete = + detail::AccessBridgeInternals::get(std::get(*res)) + .into_raw(); + return 2; // Complete Response + } else { + return 3; // No content + } +} + +extern "C" bool fastly$esi$manualbridge$ProcessFragmentResponseFn$call( + const detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag &fn_tag, + fastly::sys::http::Request *raw_req, fastly::sys::http::Response *raw_resp, + fastly::sys::http::Response *&out_resp) { + // Ideally we wouldn't do this and would instead pass a reference, but + // that would require a separate wrapper type for Request references. + auto req = detail::AccessBridgeInternals::from_raw(raw_req); + + // The real callback type is cast to this tag type before being passed in to + // Rust to break circular dependencies. It's safe to cast it back here, as if + // some other type was passed in, something has gone horribly wrong. + auto fn = static_cast(fn_tag); - extern "C" bool fastly$esi$manualbridge$ProcessFragmentResponseFn$call(const ProcessFragmentResponseFn &fn, fastly::sys::http::Request *raw_req, fastly::sys::http::Response *raw_resp, fastly::sys::http::Response *&out_resp, fastly::sys::esi::ExecutionError *&) - { - // DANGER: This call is very unsafe. We are *not* taking ownership of the - // request as it's supposed to be passed as a borrow. However, the existing - // Request type in C++ always takes ownership of the inner pointer, so we - // store the the request in a box temporarily to avoid having to create - // an entirely new wrapper type just for this. As such, we *must* release - // the box without dropping it after the call to the user function, otherwise - // we will double-free the inner pointer. - auto req = detail::AccessBridgeInternals::from_raw(raw_req); + auto resp = detail::AccessBridgeInternals::from_raw(raw_resp); + auto res = detail::AccessBridgeInternals::get(fn)(req, std::move(resp)); - auto resp = detail::AccessBridgeInternals::from_raw(raw_resp); - auto res = detail::AccessBridgeInternals::get(fn)(req, std::move(resp)); + if (res) { + out_resp = detail::AccessBridgeInternals::get(*res).into_raw(); + return true; + } else { + return false; + } +} - // DANGER: Here we release the box without dropping it, to avoid double-freeing - detail::AccessBridgeInternals::get(req).into_raw(); +Processor::Processor(std::optional original_request_metadata, + Configuration config) + : processor_(fastly::sys::esi::m_static_esi_processor_new( + // The Rust side will take ownership + original_request_metadata.has_value() + ? detail::AccessBridgeInternals::get(*original_request_metadata) + .into_raw() + : nullptr, + static_cast(config.get_namespace()), + config.is_escaped_content())) {} - if (res) - { - out_resp = detail::AccessBridgeInternals::get(*res).into_raw(); - return true; - } - else - { - // TODO - return false; - } - } +tl::expected Processor::process_response( + Response &src_document, std::optional client_response_metadata, + std::optional dispatch_fragment_request, + std::optional process_fragment_response) { + fastly::sys::error::FastlyError *err; + // The Rust side will take ownership + auto raw_metadata = + client_response_metadata.has_value() + ? detail::AccessBridgeInternals::get(*client_response_metadata) + .into_raw() + : nullptr; - Processor::Processor(std::optional original_request_metadata, - Configuration config) - : processor_(fastly::sys::esi::m_static_esi_processor_new(original_request_metadata.has_value() ? &detail::AccessBridgeInternals::get(*original_request_metadata) : nullptr, - static_cast(config.get_namespace()), - config.is_escaped_content())) - { - } + // We convert the callbacks to their tag types here, or pass null if not + // present. They will be converted back to their real types when the C++ + // callback bindings are invoked from Rust. + detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag + *dispatch_fragment_tag = + dispatch_fragment_request.has_value() ? &*dispatch_fragment_request + : nullptr; + detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag + *process_fragment_tag = + process_fragment_response.has_value() ? &*process_fragment_response + : nullptr; - tl::expected Processor::process_response( - Response &src_document, - std::optional client_response_metadata, - std::optional dispatch_fragment_request, - std::optional process_fragment_response) - { - fastly::sys::esi::ExecutionError *err = nullptr; - bool success = fastly::sys::esi::m_esi_processor_process_response( - std::move(processor_), - *detail::AccessBridgeInternals::get(src_document), - client_response_metadata.has_value() ? &detail::AccessBridgeInternals::get(*client_response_metadata) : nullptr, - dispatch_fragment_request.has_value() ? reinterpret_cast(&*dispatch_fragment_request) : nullptr, - process_fragment_response.has_value() ? reinterpret_cast(&*process_fragment_response) : nullptr, - err); - if (success) - { - return {}; - } - else - { - return tl::unexpected(ExecutionError()); - } - } -} \ No newline at end of file + bool success = fastly::sys::esi::m_esi_processor_process_response( + std::move(processor_), *detail::AccessBridgeInternals::get(src_document), + raw_metadata, dispatch_fragment_tag, process_fragment_tag, err); + if (success) { + return {}; + } else { + return tl::unexpected(error::FastlyError(err)); + } +} +} // namespace fastly::esi \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index c15b4c5..c6fc257 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,8 @@ pub enum FastlyError { SecretStoreLookupError(#[from] fastly::secret_store::LookupError), #[error(transparent)] LogError(#[from] fastly::log::LogError), + #[error(transparent)] + ESIError(#[from] esi::ExecutionError), // Make sure to add any new variants to the `FastlyErrorCode` enum in `lib.rs` _and_ to the match below! } @@ -111,6 +113,7 @@ impl FastlyError { FastlyError::SecretStoreOpenError(_) => FastlyErrorCode::SecretStoreOpenError, FastlyError::SecretStoreLookupError(_) => FastlyErrorCode::SecretStoreLookupError, FastlyError::LogError(_) => FastlyErrorCode::LogError, + FastlyError::ESIError(_) => FastlyErrorCode::ESIError, } } } diff --git a/src/esi.rs b/src/esi.rs index 09f206d..f1919f3 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -9,121 +9,138 @@ use esi::Configuration; use crate::{ error::FastlyError, + ffi::{DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag}, http::{request::Request, response::Response}, try_fe, }; -pub(crate) struct ExecutionError(esi::ExecutionError); - pub(crate) struct Processor(esi::Processor); -pub(crate) struct DispatchFragmentRequestFn; -pub(crate) struct ProcessFragmentResponseFn; +fn shim_dispatch_fragment_request_fn( + func: *const DispatchFragmentRequestFnTag, +) -> Option Result>> +{ + if func.is_null() { + return None; + } + let shim = Box::new(move |req| { + let mut out_pending = ptr::null_mut(); + let mut out_completed = ptr::null_mut(); + let result = unsafe { + crate::manual_ffi::fastly_esi_manualbridge_DispatchFragmentRequestFn_call( + func, + Box::into_raw(Box::new(Request(req))), + &mut out_pending, + &mut out_completed, + ) + }; + match result { + 1 => Ok(unsafe { esi::PendingFragmentContent::PendingRequest(out_pending.read().0) }), + 2 => { + Ok( + unsafe { + esi::PendingFragmentContent::CompletedRequest(out_completed.read().0) + }, + ) + } + 3 => Ok(esi::PendingFragmentContent::NoContent), + 0 => Err(esi::ExecutionError::FunctionError( + "dispatch_fragment_request".into(), + )), + _ => unreachable!(), + } + }); + Some(shim) +} +fn shim_process_fragment_response_fn( + func: *const ProcessFragmentResponseFnTag, +) -> Option< + Box< + dyn Fn( + &mut fastly::Request, + fastly::Response, + ) -> Result, + >, +> { + if func.is_null() { + return None; + } + let shim = Box::new(move |req: &mut fastly::Request, resp| { + let mut out_resp = ptr::null_mut(); + let result = unsafe { + crate::manual_ffi::fastly_esi_manualbridge_ProcessFragmentResponseFn_call( + func, + // Ideally we wouldn't do this and would instead pass a reference, but + // that would require a separate wrapper type for Request references. + Box::into_raw(Box::new(Request(req.clone_with_body()))), + Box::into_raw(Box::new(Response(resp))), + &mut out_resp, + ) + }; + match result { + true => Ok(unsafe { out_resp.read().0 }), + false => Err(esi::ExecutionError::FunctionError( + "process_fragment_response".into(), + )), + } + }); + Some(shim) +} pub fn m_esi_processor_process_response( processor: Box, src_document: &mut Response, - client_response_metadata: *mut Box, - dispatch_fragment_request: *const DispatchFragmentRequestFn, - process_fragment_response: *const ProcessFragmentResponseFn, - mut err: Pin<&mut *mut ExecutionError>, + client_response_metadata: *mut Response, + dispatch_fragment_request: *const DispatchFragmentRequestFnTag, + process_fragment_response: *const ProcessFragmentResponseFnTag, + mut err: Pin<&mut *mut FastlyError>, ) -> bool { let client_response_metadata = if client_response_metadata.is_null() { None } else { - Some(unsafe { client_response_metadata.read().0 }) - }; - let dispatch_fragment_request = if dispatch_fragment_request.is_null() { - None - } else { - let func = unsafe { dispatch_fragment_request.read() }; - let shim: Box< - dyn Fn(fastly::Request) -> Result, - > = - Box::new(move |req| { - let mut out_pending = ptr::null_mut(); - let mut out_completed = ptr::null_mut(); - let mut err = ptr::null_mut(); - let result = unsafe { - crate::manual_ffi::fastly_esi_manualbridge_DispatchFragmentRequestFn_call( - &func, - Box::into_raw(Box::new(Request(req))), - &mut out_pending, - &mut out_completed, - &mut err, - ) - }; - match result { - 1 => Ok(unsafe { - esi::PendingFragmentContent::PendingRequest(out_pending.read().0) - }), - 2 => Ok(unsafe { - esi::PendingFragmentContent::CompletedRequest(out_completed.read().0) - }), - 3 => Ok(esi::PendingFragmentContent::NoContent), - 0 => Err(unsafe { err.read().0 }), - _ => unreachable!(), - } - }); - Some(shim) - }; - let process_fragment_response = if process_fragment_response.is_null() { - None - } else { - let func = unsafe { process_fragment_response.read() }; - let shim: Box< - dyn Fn( - &mut fastly::Request, - fastly::Response, - ) -> Result, - > = Box::new(move |req, resp| { - let mut out_resp = ptr::null_mut(); - let mut err = ptr::null_mut(); - let result = unsafe { - crate::manual_ffi::fastly_esi_manualbridge_ProcessFragmentResponseFn_call( - &func, - Box::into_raw(Box::new(Request(req.clone_with_body()))), //TODO this leaks currently - Box::into_raw(Box::new(Response(resp))), - &mut out_resp, - &mut err, - ) - }; - match result { - true => Ok(unsafe { out_resp.read().0 }), - false => Err(unsafe { err.read().0 }), - } - }); - Some(shim) + // Make sure to take ownership, as this pointer is modelling an Optional> + Some(unsafe { Box::from_raw(client_response_metadata) }) }; match processor.0.process_response( &mut src_document.0, - client_response_metadata, - dispatch_fragment_request.as_deref(), - process_fragment_response.as_deref(), + client_response_metadata.map(|r| r.0), + shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), + shim_process_fragment_response_fn(process_fragment_response).as_deref(), ) { Ok(_) => { err.set(ptr::null_mut()); true } Err(e) => { - err.set(Box::into_raw(Box::new(ExecutionError(e)))); + err.set(Box::into_raw(Box::new(FastlyError::ESIError(e)))); false } } } pub fn m_static_esi_processor_new( - original_request_metadata: *mut Box, + original_request_metadata: *mut Request, namespace: &CxxString, is_escaped_content: bool, ) -> Box { + println!( + "Original request metadata ptr: {:p}", + original_request_metadata + ); let original_request_metadata = if original_request_metadata.is_null() { None } else { - Some(unsafe { original_request_metadata.read().0 }) + // Make sure to take ownership, as this pointer is modelling an Optional> + Some(unsafe { Box::from_raw(original_request_metadata) }) }; + println!( + "Original request metadata: {:?}", + original_request_metadata + .as_ref() + .map_or("None".into(), |r| r.0.get_url().to_string()) + ); Box::new(Processor(esi::Processor::new( - original_request_metadata, + original_request_metadata.map(|r| r.0), Configuration::default() .with_escaped(is_escaped_content) .with_namespace(namespace.to_string()), diff --git a/src/lib.rs b/src/lib.rs index e41d58d..6d4466b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ mod ffi { SecretStoreOpenError, SecretStoreLookupError, LogError, + ESIError, } #[namespace = "fastly::sys::http"] @@ -1021,19 +1022,13 @@ mod ffi { ) -> bool; } - #[namespace = "fastly::sys::esi"] - extern "Rust" { - type ExecutionError; - } - - #[namespace = "fastly::sys::esi"] - extern "Rust" { - type DispatchFragmentRequestFn; - } - - #[namespace = "fastly::sys::esi"] - extern "Rust" { - type ProcessFragmentResponseFn; + // These tag types are empty types used to communicate callbacks from C++ to Rust. + // They will be cast back to the real callback types on the C++ side. + #[namespace = "fastly::detail::rust_bridge_tags::esi"] + unsafe extern "C++" { + include!("fastly/detail/rust_bridge_tags.h"); + type DispatchFragmentRequestFnTag; + type ProcessFragmentResponseFnTag; } #[namespace = "fastly::sys::esi"] @@ -1042,39 +1037,49 @@ mod ffi { pub unsafe fn m_esi_processor_process_response( processor: Box, src_document: &mut Response, - client_response_metadata: *mut Box, - dispatch_fragment_request: *const DispatchFragmentRequestFn, - process_fragment_response: *const ProcessFragmentResponseFn, - mut err: Pin<&mut *mut ExecutionError>, + // SAFETY: this parameter models an Option>, but CXX does not + // support this type directly. Care must be taken to take ownership of the pointer + // and free it if it is non-null. + client_response_metadata: *mut Response, + dispatch_fragment_request: *const DispatchFragmentRequestFnTag, + process_fragment_response: *const ProcessFragmentResponseFnTag, + mut err: Pin<&mut *mut FastlyError>, ) -> bool; pub unsafe fn m_static_esi_processor_new( - original_request_metadata: *mut Box, + // SAFETY: this parameter models an Option>, but CXX does not + // support this type directly. Care must be taken to take ownership of the pointer + // and free it if it is non-null. + original_request_metadata: *mut Request, namespc: &CxxString, is_escaped_content: bool, ) -> Box; } } +// Some types (notably callback functions) are not supported by CXX at all, so we +// define manual FFI bindings for them here. mod manual_ffi { - use crate::esi::DispatchFragmentRequestFn; + use crate::ffi::{DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag}; + // We never rely on the layout of Rust types passed to these functions, + // so we can ignore the improper_ctypes warning. + #[allow(improper_ctypes)] unsafe extern "C" { + // The link names here must exactly match the names of the extern "C" functions defined on the C++ side #[link_name = "fastly$esi$manualbridge$DispatchFragmentRequestFn$call"] pub(crate) fn fastly_esi_manualbridge_DispatchFragmentRequestFn_call( - func: *const DispatchFragmentRequestFn, + func: *const DispatchFragmentRequestFnTag, req: *mut crate::Request, out_pending: &mut *mut crate::PendingRequest, out_complete: &mut *mut crate::Response, - out_error: &mut *mut crate::ExecutionError, ) -> u32; #[link_name = "fastly$esi$manualbridge$ProcessFragmentResponseFn$call"] pub(crate) fn fastly_esi_manualbridge_ProcessFragmentResponseFn_call( - func: *const crate::esi::ProcessFragmentResponseFn, + func: *const ProcessFragmentResponseFnTag, request: *mut crate::Request, response: *mut crate::Response, out_response: &mut *mut crate::Response, - out_error: &mut *mut crate::ExecutionError, ) -> bool; } } From 4d0486e6d4697917cf5208a9273dd84bc14c602a Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 16:48:03 +0100 Subject: [PATCH 06/17] Comments --- .../fastly/detail/access_bridge_internals.h | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/include/fastly/detail/access_bridge_internals.h b/include/fastly/detail/access_bridge_internals.h index 9aca68c..1a6da47 100644 --- a/include/fastly/detail/access_bridge_internals.h +++ b/include/fastly/detail/access_bridge_internals.h @@ -3,26 +3,18 @@ #include -namespace fastly::detail -{ - struct AccessBridgeInternals - { - template - static auto &get(T &obj) - { - return obj.inner(); - } - template - static auto &get(const T &obj) - { - return obj.inner(); - } - template - static auto from_raw(U *ptr) - { - return T(rust::Box::from_raw(ptr)); - } - }; -} +namespace fastly::detail { +// This type can be used to access the inner `rust::Box` of +// various wrapper types in the C++ SDK. +// It can also be used to construct wrapper types from raw pointers. +// This is intended for internal use only. +struct AccessBridgeInternals { + template static auto &get(T &obj) { return obj.inner(); } + template static auto &get(const T &obj) { return obj.inner(); } + template static auto from_raw(U *ptr) { + return T(rust::Box::from_raw(ptr)); + } +}; +} // namespace fastly::detail #endif \ No newline at end of file From a6ca1f390de70754c244ad11d70389c2e1509916 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 16:48:20 +0100 Subject: [PATCH 07/17] Remove empty file --- include/fastly/callbacks.h | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 include/fastly/callbacks.h diff --git a/include/fastly/callbacks.h b/include/fastly/callbacks.h deleted file mode 100644 index e69de29..0000000 From 5233bd7629dedc0b35772faa1e03bf586c1742cf Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 16:48:55 +0100 Subject: [PATCH 08/17] Formatting --- include/fastly/http/request.h | 1436 ++++++++++++++++----------------- 1 file changed, 715 insertions(+), 721 deletions(-) diff --git a/include/fastly/http/request.h b/include/fastly/http/request.h index 10ff15b..8a21583 100644 --- a/include/fastly/http/request.h +++ b/include/fastly/http/request.h @@ -3,13 +3,13 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include #include @@ -19,742 +19,736 @@ #include #include -namespace fastly::backend -{ - class Backend; +namespace fastly::backend { +class Backend; } -namespace fastly::http -{ - - class Body; - class StreamingBody; - class Response; - class Request; - - namespace request - { - - /// A handle to a pending asynchronous request returned by - /// `Request::send_async()` or - /// `Request::send_async_streaming()`. - /// - /// A handle can be evaluated using `PendingRequest::poll()`, - /// `PendingRequest::wait()`, or - /// `http::select`. It can also be discarded if the request was sent for effects - /// it might have, and the response is unimportant. - class PendingRequest - { - friend detail::AccessBridgeInternals; - friend Request; - friend std::pair, std::vector> - select(std::vector &reqs); - - /// Try to get the result of a pending request without blocking. - /// - /// This method returns immediately with a `std::variant` containing either - /// the original `PendingRequest` if the response was not ready, or a - /// `Response` if the response was ready. If you want to block until a result - /// is ready, use `PendingRequest::wait()`. - std::variant> poll(); - - /// Block until the result of a pending request is ready. - /// - /// If you want check whether the result is ready without blocking, use - /// `PendingRequest::poll()`. - fastly::expected wait(); - - /// Cloned version of the original request that was sent, without the original - /// body. This is only a copy and cannot be used to modify anything, since the - /// request has already been sent. - Request cloned_sent_req(); - - private: - auto &inner() { return req; } - rust::Box req; - - PendingRequest(rust::Box r) - : req(std::move(r)) {}; - }; - - /// Given a collection of `PendingRequest`s, block until the result of one of - /// the requests is ready. - /// - /// Returns an `std::pair` of ``, where: - /// - /// - `result` is the result of the request that became ready. - /// - /// - `remaining` is a vector containing all of the requests that did not become - /// ready. The order of the requests in this vector is not guaranteed to match - /// the order of the requests in the argument collection. - /// - /// ### Panics - /// - /// Panics if the argument collection is empty, or contains too many requests. - std::pair, std::vector> - select(std::vector &reqs); - - } // namespace request - - /// An HTTP request, including body, headers, method, and URL. - /// - /// # Getting the client request - /// - /// Call `Request::from_client()` to get the client request being handled by - /// this execution of the Compute program. - /// - /// # Creation and conversion - /// - /// New requests can be created programmatically with the `Request()` - /// constructor. In addition, there are convenience constructors like - /// `Request::get()` which automatically select the appropriate method. - /// - /// # Sending backend requests - /// - /// Requests can be sent to a backend in blocking or asynchronous fashion using - /// `Request::send()`, `Request::send_async()`, or - /// `Request::send_async_streaming()`. - /// - /// # Builder-style methods - /// - /// `Request` can be used as a builder allowing requests to be constructed and - /// used through method chaining. Methods with the `with_` name prefix, such as - /// `Request::with_header()`, return a moved `Request` to allow chaining. The - /// builder style is typically most useful when constructing and using a request - /// in a single expression. - /// - /// For example: +namespace fastly::http { + +class Body; +class StreamingBody; +class Response; +class Request; + +namespace request { + +/// A handle to a pending asynchronous request returned by +/// `Request::send_async()` or +/// `Request::send_async_streaming()`. +/// +/// A handle can be evaluated using `PendingRequest::poll()`, +/// `PendingRequest::wait()`, or +/// `http::select`. It can also be discarded if the request was sent for effects +/// it might have, and the response is unimportant. +class PendingRequest { + friend detail::AccessBridgeInternals; + friend Request; + friend std::pair, std::vector> + select(std::vector &reqs); + + /// Try to get the result of a pending request without blocking. + /// + /// This method returns immediately with a `std::variant` containing either + /// the original `PendingRequest` if the response was not ready, or a + /// `Response` if the response was ready. If you want to block until a result + /// is ready, use `PendingRequest::wait()`. + std::variant> poll(); + + /// Block until the result of a pending request is ready. + /// + /// If you want check whether the result is ready without blocking, use + /// `PendingRequest::poll()`. + fastly::expected wait(); + + /// Cloned version of the original request that was sent, without the original + /// body. This is only a copy and cannot be used to modify anything, since the + /// request has already been sent. + Request cloned_sent_req(); + +private: + auto &inner() { return req; } + rust::Box req; + + PendingRequest(rust::Box r) + : req(std::move(r)) {}; +}; + +/// Given a collection of `PendingRequest`s, block until the result of one of +/// the requests is ready. +/// +/// Returns an `std::pair` of ``, where: +/// +/// - `result` is the result of the request that became ready. +/// +/// - `remaining` is a vector containing all of the requests that did not become +/// ready. The order of the requests in this vector is not guaranteed to match +/// the order of the requests in the argument collection. +/// +/// ### Panics +/// +/// Panics if the argument collection is empty, or contains too many requests. +std::pair, std::vector> +select(std::vector &reqs); + +} // namespace request + +/// An HTTP request, including body, headers, method, and URL. +/// +/// # Getting the client request +/// +/// Call `Request::from_client()` to get the client request being handled by +/// this execution of the Compute program. +/// +/// # Creation and conversion +/// +/// New requests can be created programmatically with the `Request()` +/// constructor. In addition, there are convenience constructors like +/// `Request::get()` which automatically select the appropriate method. +/// +/// # Sending backend requests +/// +/// Requests can be sent to a backend in blocking or asynchronous fashion using +/// `Request::send()`, `Request::send_async()`, or +/// `Request::send_async_streaming()`. +/// +/// # Builder-style methods +/// +/// `Request` can be used as a builder allowing requests to be constructed and +/// used through method chaining. Methods with the `with_` name prefix, such as +/// `Request::with_header()`, return a moved `Request` to allow chaining. The +/// builder style is typically most useful when constructing and using a request +/// in a single expression. +/// +/// For example: +/// +/// ```cpp +/// Request::get("https://example.com") +/// .with_header("my-header", "hello!") +/// .with_header("my-other-header", "Здравствуйте!") +/// .send("example_backend"); +/// ``` +/// +/// # Setter methods +/// +/// Setter methods, such as `Request::set_header()`, are prefixed by `set_`, and +/// can be used interchangeably with the builder-style methods, allowing you to +/// mix and match styles based on what is most convenient for your program. +/// Setter methods tend to work better than builder-style methods when +/// constructing a request involves conditional branches or loops. +/// +/// For example: +/// +/// ```cpp +/// auto req{Request::get("https://example.com").with_header("my-header", +/// "hello!")}; +/// if (needs_translation) { +/// req.set_header("my-other-header", "Здравствуйте!"); +/// } +/// req.send("example_backend"); +/// ``` +class Request { + friend request::PendingRequest; + friend Response; + friend detail::AccessBridgeInternals; + +public: + /// Create a new request with the given method and URL, no headers, and an + /// empty body. + Request(Method method, std::string_view url); + + /// Create a new `GET` `Request` with the given URL, no headers, and an + /// empty body. + static Request get(std::string_view url); + + /// Create a new `HEAD` `Request` with the given URL, no headers, and an + /// empty body. + static Request head(std::string_view url); + + /// Create a new `POST` `Request` with the given URL, no headers, and an + /// empty body. + static Request post(std::string_view url); + + /// Create a new `PUT` `Request` with the given URL, no headers, and an + /// empty body. + static Request put(std::string_view url); + + /// Create a new `DELETE` `Request` with the given URL, no headers, and an + /// empty body. + static Request delete_(std::string_view url); + + /// Create a new `CONNECT` `Request` with the given URL, no headers, and an + /// empty body. + static Request connect(std::string_view url); + + /// Create a new `OPTIONS` `Request` with the given URL, no headers, and an + /// empty body. + static Request options(std::string_view url); + + /// Create a new `TRACE` `Request` with the given URL, no headers, and an + /// empty body. + static Request trace(std::string_view url); + + /// Create a new `PATCH` `Request` with the given URL, no headers, and an + /// empty body. + static Request patch(std::string_view url); + + /// Get the client request being handled by this execution of the Compute + /// program. + /// + /// # Panics + /// + /// This method panics if the client request has already been retrieved by + /// this method or by the low-level handle API. + static Request from_client(); + + /// Return `true` if this request is from the client of this execution of the + /// Compute program. + bool is_from_client(); + + /// Make a new request with the same method, url, headers, and version of this + /// request, but no body. + /// + /// If you also need to clone the request body, use + /// [`clone_with_body()`][`Self::clone_with_body()`] + /// + /// # Examples + /// + /// ```cpp + /// auto original = Request::post("https://example.com") + /// .with_header("hello", "world!") + /// .with_body("hello"); + /// auto new_req = original.clone_without_body(); + /// assert(original.get_method() == new.get_method()); + /// assert(original.get_url() == new.get_url()); + /// assert(original.get_header("hello") == new.get_header("hello")); + /// assert(original.has_body()); + /// assert(!new.has_body()); + /// ``` + Request clone_without_body(); + + /// Clone this request by reading in its body, and then writing the same body + /// to the original and the cloned request. + /// + /// This method requires mutable access to this request because reading from + /// and writing to the body can involve an HTTP connection. + Request clone_with_body(); + + /// Retrieve a reponse for the request, either from cache or by sending it to + /// the given backend server. Returns once the response headers have been + /// received, or an error occurs. + /// + /// # Examples + /// + /// Sending the client request to a backend without modification: + /// + /// ```cpp + /// auto backend_resp{Request::from_client().send("example_backend")}; + /// assert(backend_resp.get_status().is_success()); + /// ``` + /// + /// Sending a synthetic request: + /// + /// ```cpp + /// auto + /// backend_resp{Request::get("https://example.com").send("example_backend")}; + /// assert(backend_resp.get_status().is_success()); + /// ``` + fastly::expected send(fastly::backend::Backend &backend); + fastly::expected send(std::string_view backend_name); + + /// Begin sending the request to the given backend server, and return a + /// `PendingRequest` that can yield the backend response or an error. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission of the request body and headers will continue in the + /// background. + /// + /// This method allows for sending more than one request at once and receiving + /// their responses in arbitrary orders. See `PendingRequest` for more details + /// on how to wait on, poll, or select between pending requests. + /// + /// This method is also useful for sending requests where the response is + /// unimportant, but the request may take longer than the Compute program is + /// able to run, as the request will continue sending even after the program + /// that initiated it exits. + /// + /// # Examples + /// + /// Sending a request to two backends and returning whichever response + /// finishes first: /// /// ```cpp - /// Request::get("https://example.com") - /// .with_header("my-header", "hello!") - /// .with_header("my-other-header", "Здравствуйте!") - /// .send("example_backend"); + /// auto backend_resp_1{Request::get("https://example.com/") + /// .send_async("example_backend_1")}; + /// auto backend_resp_2{Request::get("https://example.com/") + /// .send_async("example_backend_2")}; + /// auto [selected_resp, _others] = + /// fastly::http::request::select({backend_resp_1, backend_resp_2}); + /// selected_resp.send_to_client(); /// ``` /// - /// # Setter methods + /// Sending a long-running request and ignoring its result so that the program + /// can exit before + /// it completes: + /// + /// ```cpp + /// Request::post("https://example.com") + /// .with_body(some_large_file) + /// .send_async("example_backend"); + /// ``` + fastly::expected + send_async(fastly::backend::Backend &backend); + fastly::expected + send_async(std::string_view backend_name); + + /// Begin sending the request to the given backend server, and return a + /// `PendingRequest` that + /// can yield the backend response or an error along with a `StreamingBody` + /// that can accept + /// further data to send. + /// + /// The backend connection is only closed once `StreamingBody::finish()` is + /// called. The + /// `PendingRequest` will not yield a `Response` until the + /// `StreamingBody` is finished. + /// + /// This method is most useful for programs that do some sort of processing or + /// inspection of a + /// potentially-large client request body. Streaming allows the program to + /// operate on small + /// parts of the body rather than having to read it all into memory at once. + /// + /// This method returns as soon as the request begins sending to the backend, + /// and transmission + /// of the request body and headers will continue in the background. /// - /// Setter methods, such as `Request::set_header()`, are prefixed by `set_`, and - /// can be used interchangeably with the builder-style methods, allowing you to - /// mix and match styles based on what is most convenient for your program. - /// Setter methods tend to work better than builder-style methods when - /// constructing a request involves conditional branches or loops. + /// # Examples /// - /// For example: + /// Count the number of lines in a UTF-8 client request body while sending it + /// to the backend: /// /// ```cpp - /// auto req{Request::get("https://example.com").with_header("my-header", - /// "hello!")}; - /// if (needs_translation) { - /// req.set_header("my-other-header", "Здравствуйте!"); + /// auto req{Request::from_client()}; + /// // Take the body so we can iterate through its lines later + /// auto req_body{req.take_body()}; + /// // Start sending the client request to the client with a now-empty body + /// auto [backend_body, pending_req] = req + /// .send_async_streaming("example_backend"); + /// + /// size_t num_lines{0}; + /// std::string buf; + /// while (std::getline(req_body, buf)) { + /// num_lines++; + /// // Write the line to the streaming backend body + /// backend_body << buf << "\n" << std::flush; /// } + /// // Finish the streaming body to allow the backend connection to close + /// backend_body.finish(); + /// + /// std::cout + /// << "client request body contained " + /// << num_lines + /// << " lines" + /// << std::endl; + /// ``` + fastly::expected> + send_async_streaming(fastly::backend::Backend &backend); + fastly::expected> + send_async_streaming(std::string_view backend_name); + + /// Builder-style equivalent of `Request::set_body()`. + Request with_body(Body body) &&; + + /// Returns `true` if this request has a body. + bool has_body(); + + /// Take and return the body from this request. + /// + /// After calling this method, this request will no longer have a body. + Body take_body(); + + /// Set the given value as the request's body. + void set_body(Body body); + + /// Append another [`Body`] to the body of this request without reading or + /// writing any body contents. + /// + /// If this request does not have a body, the appended body is set as the + /// request's body. + /// + /// This method should be used when combining bodies that have not + /// necessarily been read yet, such as the body of the client. To append + /// contents that are already in memory as strings or bytes, you should + /// instead use + /// [`get_body_mut()`][`Self::get_body_mut()`] to write the contents to the + /// end of the body. + /// + /// # Examples + /// + /// ```cpp + /// auto req{Request::post("https://example.com").with_body("hello! client + /// says: ")}; req.append_body(Request::from_client().into_body()); /// req.send("example_backend"); /// ``` - class Request - { - friend request::PendingRequest; - friend Response; - friend detail::AccessBridgeInternals; - - public: - /// Create a new request with the given method and URL, no headers, and an - /// empty body. - Request(Method method, std::string_view url); - - /// Create a new `GET` `Request` with the given URL, no headers, and an - /// empty body. - static Request get(std::string_view url); - - /// Create a new `HEAD` `Request` with the given URL, no headers, and an - /// empty body. - static Request head(std::string_view url); - - /// Create a new `POST` `Request` with the given URL, no headers, and an - /// empty body. - static Request post(std::string_view url); - - /// Create a new `PUT` `Request` with the given URL, no headers, and an - /// empty body. - static Request put(std::string_view url); - - /// Create a new `DELETE` `Request` with the given URL, no headers, and an - /// empty body. - static Request delete_(std::string_view url); - - /// Create a new `CONNECT` `Request` with the given URL, no headers, and an - /// empty body. - static Request connect(std::string_view url); - - /// Create a new `OPTIONS` `Request` with the given URL, no headers, and an - /// empty body. - static Request options(std::string_view url); - - /// Create a new `TRACE` `Request` with the given URL, no headers, and an - /// empty body. - static Request trace(std::string_view url); - - /// Create a new `PATCH` `Request` with the given URL, no headers, and an - /// empty body. - static Request patch(std::string_view url); - - /// Get the client request being handled by this execution of the Compute - /// program. - /// - /// # Panics - /// - /// This method panics if the client request has already been retrieved by - /// this method or by the low-level handle API. - static Request from_client(); - - /// Return `true` if this request is from the client of this execution of the - /// Compute program. - bool is_from_client(); - - /// Make a new request with the same method, url, headers, and version of this - /// request, but no body. - /// - /// If you also need to clone the request body, use - /// [`clone_with_body()`][`Self::clone_with_body()`] - /// - /// # Examples - /// - /// ```cpp - /// auto original = Request::post("https://example.com") - /// .with_header("hello", "world!") - /// .with_body("hello"); - /// auto new_req = original.clone_without_body(); - /// assert(original.get_method() == new.get_method()); - /// assert(original.get_url() == new.get_url()); - /// assert(original.get_header("hello") == new.get_header("hello")); - /// assert(original.has_body()); - /// assert(!new.has_body()); - /// ``` - Request clone_without_body(); - - /// Clone this request by reading in its body, and then writing the same body - /// to the original and the cloned request. - /// - /// This method requires mutable access to this request because reading from - /// and writing to the body can involve an HTTP connection. - Request clone_with_body(); - - /// Retrieve a reponse for the request, either from cache or by sending it to - /// the given backend server. Returns once the response headers have been - /// received, or an error occurs. - /// - /// # Examples - /// - /// Sending the client request to a backend without modification: - /// - /// ```cpp - /// auto backend_resp{Request::from_client().send("example_backend")}; - /// assert(backend_resp.get_status().is_success()); - /// ``` - /// - /// Sending a synthetic request: - /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com").send("example_backend")}; - /// assert(backend_resp.get_status().is_success()); - /// ``` - fastly::expected send(fastly::backend::Backend &backend); - fastly::expected send(std::string_view backend_name); - - /// Begin sending the request to the given backend server, and return a - /// `PendingRequest` that can yield the backend response or an error. - /// - /// This method returns as soon as the request begins sending to the backend, - /// and transmission of the request body and headers will continue in the - /// background. - /// - /// This method allows for sending more than one request at once and receiving - /// their responses in arbitrary orders. See `PendingRequest` for more details - /// on how to wait on, poll, or select between pending requests. - /// - /// This method is also useful for sending requests where the response is - /// unimportant, but the request may take longer than the Compute program is - /// able to run, as the request will continue sending even after the program - /// that initiated it exits. - /// - /// # Examples - /// - /// Sending a request to two backends and returning whichever response - /// finishes first: - /// - /// ```cpp - /// auto backend_resp_1{Request::get("https://example.com/") - /// .send_async("example_backend_1")}; - /// auto backend_resp_2{Request::get("https://example.com/") - /// .send_async("example_backend_2")}; - /// auto [selected_resp, _others] = - /// fastly::http::request::select({backend_resp_1, backend_resp_2}); - /// selected_resp.send_to_client(); - /// ``` - /// - /// Sending a long-running request and ignoring its result so that the program - /// can exit before - /// it completes: - /// - /// ```cpp - /// Request::post("https://example.com") - /// .with_body(some_large_file) - /// .send_async("example_backend"); - /// ``` - fastly::expected - send_async(fastly::backend::Backend &backend); - fastly::expected - send_async(std::string_view backend_name); - - /// Begin sending the request to the given backend server, and return a - /// `PendingRequest` that - /// can yield the backend response or an error along with a `StreamingBody` - /// that can accept - /// further data to send. - /// - /// The backend connection is only closed once `StreamingBody::finish()` is - /// called. The - /// `PendingRequest` will not yield a `Response` until the - /// `StreamingBody` is finished. - /// - /// This method is most useful for programs that do some sort of processing or - /// inspection of a - /// potentially-large client request body. Streaming allows the program to - /// operate on small - /// parts of the body rather than having to read it all into memory at once. - /// - /// This method returns as soon as the request begins sending to the backend, - /// and transmission - /// of the request body and headers will continue in the background. - /// - /// # Examples - /// - /// Count the number of lines in a UTF-8 client request body while sending it - /// to the backend: - /// - /// ```cpp - /// auto req{Request::from_client()}; - /// // Take the body so we can iterate through its lines later - /// auto req_body{req.take_body()}; - /// // Start sending the client request to the client with a now-empty body - /// auto [backend_body, pending_req] = req - /// .send_async_streaming("example_backend"); - /// - /// size_t num_lines{0}; - /// std::string buf; - /// while (std::getline(req_body, buf)) { - /// num_lines++; - /// // Write the line to the streaming backend body - /// backend_body << buf << "\n" << std::flush; - /// } - /// // Finish the streaming body to allow the backend connection to close - /// backend_body.finish(); - /// - /// std::cout - /// << "client request body contained " - /// << num_lines - /// << " lines" - /// << std::endl; - /// ``` - fastly::expected> - send_async_streaming(fastly::backend::Backend &backend); - fastly::expected> - send_async_streaming(std::string_view backend_name); - - /// Builder-style equivalent of `Request::set_body()`. - Request with_body(Body body) &&; - - /// Returns `true` if this request has a body. - bool has_body(); - - /// Take and return the body from this request. - /// - /// After calling this method, this request will no longer have a body. - Body take_body(); - - /// Set the given value as the request's body. - void set_body(Body body); - - /// Append another [`Body`] to the body of this request without reading or - /// writing any body contents. - /// - /// If this request does not have a body, the appended body is set as the - /// request's body. - /// - /// This method should be used when combining bodies that have not - /// necessarily been read yet, such as the body of the client. To append - /// contents that are already in memory as strings or bytes, you should - /// instead use - /// [`get_body_mut()`][`Self::get_body_mut()`] to write the contents to the - /// end of the body. - /// - /// # Examples - /// - /// ```cpp - /// auto req{Request::post("https://example.com").with_body("hello! client - /// says: ")}; req.append_body(Request::from_client().into_body()); - /// req.send("example_backend"); - /// ``` - void append_body(Body &body); - - /// Consume the request and return its body as a byte vector. - std::vector into_body_bytes(); - - /// Consume the request and return its body as a string. - std::string into_body_string(); - - /// Consume the request and return its body as a `Body` instance. - Body into_body(); - - /// Builder-style equivalent of - /// `Request::set_body_text_plain()`. - fastly::expected with_body_text_plain(std::string_view body) &&; - - /// Set the given string as the request's body with content type - /// `text/plain; charset=UTF-8`. - fastly::expected set_body_text_plain(std::string_view body); - - /// Builder-style equivalent of - /// `Request::set_body_text_html()`. - fastly::expected with_body_text_html(std::string_view body) &&; - - /// Set the given string as the request's body with content type `text/html; - /// charset=UTF-8`. - fastly::expected set_body_text_html(std::string_view body); - - /// Take and return the body from this request as a string. - /// - /// After calling this method, this request will no longer have a body. - std::string take_body_string(); - - /// Builder-style equivalent of - /// `Request::set_body_octet_stream()`. - Request with_body_octet_stream(std::vector body) &&; - - /// Set the given bytes as the request's body with content type - /// `application/octet-stream`. - void set_body_octet_stream(std::vector body); - - /// Take and return the body from this request as a vector of bytes. - /// - /// After calling this method, this request will no longer have a body. - std::vector take_body_bytes(); - - // ChunksIter read_body_chunks(size_t chunk_size); - - /// Get the MIME type described by the request's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header, or `std::nullopt` if that header is absent or contains an - /// invalid MIME type. - std::optional get_content_type(); - - /// Builder-style equivalent of - /// `Request::set_content_type()`. - Request with_content_type(std::string_view mime) &&; - - /// Set the MIME type described by the request's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header. - /// - /// Any existing `Content-Type` header values will be overwritten. - void set_content_type(std::string_view mime); - - /// Get the value of the request's - /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) - /// header, if it exists. - std::optional get_content_length(); - - /// Returns whether the given header name is present in the request. - fastly::expected contains_header(std::string_view name); - - /// Builder-style equivalent of `Request::append_header()`. - fastly::expected with_header(std::string_view name, - std::string_view value) &&; - - /// Builder-style equivalent of `Request::set_header()`. - fastly::expected with_set_header(std::string_view name, - std::string_view value) &&; - - /// Get the value of a header as a string, or `std::nullopt` if the header - /// is not present. - /// - /// If there are multiple values for the header, only one is returned, which - /// may be any of the values. See - /// `Request::get_header_all()` - /// all of the values. - fastly::expected> - get_header(std::string_view name); - - /// Get an iterator of all the values of a header. - fastly::expected get_header_all(std::string_view name); - fastly::expected get_headers(); - fastly::expected get_header_names(); - - /// Set a request header to the given value, discarding any previous values - /// for the given header name. - fastly::expected set_header(std::string_view name, - std::string_view value); - - /// Add a request header with given value. - /// - /// Unlike `Request::set_header()`, this does not discard existing values - /// for the same header name. - fastly::expected append_header(std::string_view name, - std::string_view value); - - /// Remove all request headers of the given name, and return one of the - /// removed header values if any were present. - fastly::expected> - remove_header(std::string_view name); - - /// Builder-style equivalent of `Request::set_method()`. - Request with_method(Method method) &&; - - /// Get the request method. - Method get_method(); - - /// Set the request method. - void set_method(Method method); - - /// Builder-style equivalent of `Request::set_url()`. - fastly::expected with_url(std::string_view url) &&; - - /// Get the request URL as a string. - std::string get_url(); - - /// Set the request URL. - fastly::expected set_url(std::string_view url); - - /// Get the path component of the request URL. - /// - /// # Examples - /// - /// ```cpp - /// auto req{Request::get("https://example.com/hello#world")}; - /// assert(req.get_path() == "/hello"); - /// ``` - std::string get_path(); - - /// Builder-style equivalent of `Request::set_path()`. - fastly::expected with_path(std::string_view path) &&; - - /// Set the path component of the request URL. - /// # Examples - /// - /// ```cpp - /// auto req{Request::get("https://example.com/")}; - /// req.set_path("/hello"); - /// assert!(req.get_url(), "https://example.com/hello"); - /// ``` - fastly::expected set_path(std::string_view path); - - /// Get the query component of the request URL, if it exists, as a - /// percent-encoded ASCII string. - std::optional get_query_string(); - - /// Get the value of a query parameter in the request's URL. - /// - /// This assumes that the query string is a `&` separated list of - /// `parameter=value` pairs. The value of the first occurrence of - /// `parameter` is returned. No URL decoding is performed. - std::optional get_query_parameter(std::string_view param); - - /// Builder-style equivalent of `Request::set_query()`. - fastly::expected with_query_string(std::string_view query) &&; - - /// Set the query string of the request URL query component to the given - /// string, performing percent-encoding if necessary. - /// - /// # Examples - /// - /// ```no_run - /// auto req{Request::get("https://example.com/foo")}; - /// req.set_query_string("hello=🌐!&bar=baz"); - /// assert(req.get_url(), - /// "https://example.com/foo?hello=%F0%9F%8C%90!&bar=baz"); - /// ``` - fastly::expected set_query_string(std::string_view query); - - /// Remove the query component from the request URL, if one exists. - void remove_query(); - - /// Builder-style equivalent of `Request::set_version()`. - Request with_version(Version version) &&; - - /// Get the HTTP version of this request. - Version get_version(); - - /// Set the HTTP version of this request. - void set_version(Version version); - - /// Builder-style equivalent of `Request::set_pass()`. - Request with_pass(bool pass) &&; - - /// Set whether this request should be cached if sent to a backend. - /// - /// By default this is `false`, which means the backend will only be reached - /// if a cached response is not available. Set this to `true` to send the - /// request directly to the backend without caching. - /// - /// # Overrides - /// - /// Setting this to `true` overrides any other custom caching behaviors for - /// this request, such as `Request::set_ttl()` or - /// `Request::set_surrogate_key()`. - void set_pass(bool pass); - - /// Builder-style equivalent of `Request::set_ttl()`. - Request with_ttl(uint32_t ttl) &&; - - /// Override the caching behavior of this request to use the given Time to - /// Live (TTL), in seconds. - /// - /// # Overrides - /// - /// This overrides the behavior specified in the response headers, and sets - /// the `Request::set_pass()` behavior to `false`. - void set_ttl(uint32_t ttl); - - /// Builder-style equivalent of - /// `Request::set_stale_while_revalidate()`. - Request with_stale_while_revalidate(uint32_t swr) &&; - - /// Override the caching behavior of this request to use the given - /// `stale-while-revalidate` time, in seconds. - /// - /// # Overrides - /// - /// This overrides the behavior specified in the response headers, and sets - /// the `Request::set_pass()` behavior to `false`. - void set_stale_while_revalidate(uint32_t swr); - - /// Builder-style equivalent of `Request::set_pci()`. - Request with_pci(bool pci) &&; - - /// Override the caching behavior of this request to enable or disable - /// PCI/HIPAA-compliant non-volatile caching. - /// - /// By default, this is `false`, which means the request may not be - /// PCI/HIPAA-compliant. Set it to `true` to enable compliant caching. - /// - /// See the [Fastly PCI-Compliant Caching and Delivery - /// documentation](https://docs.fastly.com/products/pci-compliant-caching-and-delivery) - /// for details. - /// - /// # Overrides - /// - /// This sets the `Request::set_pass()` behavior to `false`. - void set_pci(bool pci); - - /// Builder-style equivalent of - /// `Request::set_surrogate_key()`. - fastly::expected with_surrogate_key(std::string_view sk) &&; - - /// Override the caching behavior of this request to include the given - /// surrogate key(s), provided as a header value. - /// - /// The header value can contain more than one surrogate key, separated by - /// spaces. - /// - /// Surrogate keys must contain only printable ASCII characters (those - /// between `0x21` and `0x7E`, inclusive). Any invalid keys will be ignored. - /// - /// See the [Fastly surrogate keys - /// guide](https://docs.fastly.com/en/guides/purging-api-cache-with-surrogate-keys) - /// for details. - /// - /// # Overrides - /// - /// This sets the `Request::set_pass()` behavior to `false`, and - /// extends (but does not replace) any `Surrogate-Key` response headers from - /// the backend. - fastly::expected set_surrogate_key(std::string_view sk); - - std::optional get_client_ip_addr(); - std::optional get_server_ip_addr(); - - std::optional get_original_header_names(); - std::optional get_original_header_count(); - - /// Returns whether the request was tagged as contributing to a DDoS attack - /// - /// Returns `std::nullopt` if this is not the client request. - std::optional get_client_ddos_detected(); - - // std::optional> get_tls_client_hello(); - // std::optional> get_tls_ja3_md5(); - // std::optional get_tls_ja4(); - // std::optional get_tls_raw_client_certificate(); - // std::optional> - // get_tls_raw_client_certificate_bytes(); - // // TODO(@zkat): needs additional type - // // std::optional - // get_tls_client_cert_verify_result(); std::optional - // get_tls_cipher_openssl_name(); std::optional> - // get_tls_cipher_openssl_name_bytes(); std::optional> - // get_tls_protocol_bytes(); - - /// Set whether a `gzip`-encoded response to this request will be - /// automatically decompressed. - /// - /// Enabling this will set the `Accept-Encoding` header before the request - /// is sent, regardless of the original value in the request, to ensure that - /// any values originally sent by a browser or other client get replaced - /// with `gzip`, so that the backend will not try sending unsupported - /// compression algorithms. - /// - /// If the response to this request is `gzip`-encoded, it will be presented - /// in decompressed form, and the `Content-Encoding` and `Content-Length` - /// headers will be removed. - void set_auto_decompress_gzip(bool gzip); - - /// Builder-style equivalent of - /// `Request::set_auto_decompress_gzip()`. - Request with_auto_decompress_gzip(bool gzip) &&; - - // TODO(@zkat): needs enum - // void set_framing_headers_mode(FramingHeadersMode mode); - // Request *set_framing_headers_mode(FramingHeadersMode mode); - - /// Returns whether or not the client request had a `Fastly-Key` header - /// which is valid for purging content for the service. - /// - /// This function ignores the current value of any `Fastly-Key` header for - /// this request. - bool fastly_key_is_valid(); - - // void handoff_websocket(fastly::backend::Backend backend); - // void handoff_fanout(fastly::backend::Backend backend); - // Request *on_behalf_of(std::string_view service); - - /// Set the cache key to be used when attempting to satisfy this request - /// from a cached response. - void set_cache_key(std::string_view key); - - /// Set the cache key to be used when attempting to satisfy this request - /// from a cached response. - void set_cache_key(std::vector key); - - /// Builder-style equivalent of `Request::set_cache_key()`. - Request with_cache_key(std::string_view key) &&; - - /// Builder-style equivalent of `Request::set_cache_key()`. - Request with_cache_key(std::vector key) &&; - - /// Gets whether the request is potentially cacheable. - bool is_cacheable(); - - private: - auto &inner() { return req; } - Request(rust::Box r) : req(std::move(r)) {}; - rust::Box req; - }; + void append_body(Body &body); + + /// Consume the request and return its body as a byte vector. + std::vector into_body_bytes(); + + /// Consume the request and return its body as a string. + std::string into_body_string(); + + /// Consume the request and return its body as a `Body` instance. + Body into_body(); + + /// Builder-style equivalent of + /// `Request::set_body_text_plain()`. + fastly::expected with_body_text_plain(std::string_view body) &&; + + /// Set the given string as the request's body with content type + /// `text/plain; charset=UTF-8`. + fastly::expected set_body_text_plain(std::string_view body); + + /// Builder-style equivalent of + /// `Request::set_body_text_html()`. + fastly::expected with_body_text_html(std::string_view body) &&; + + /// Set the given string as the request's body with content type `text/html; + /// charset=UTF-8`. + fastly::expected set_body_text_html(std::string_view body); + + /// Take and return the body from this request as a string. + /// + /// After calling this method, this request will no longer have a body. + std::string take_body_string(); + + /// Builder-style equivalent of + /// `Request::set_body_octet_stream()`. + Request with_body_octet_stream(std::vector body) &&; + + /// Set the given bytes as the request's body with content type + /// `application/octet-stream`. + void set_body_octet_stream(std::vector body); + + /// Take and return the body from this request as a vector of bytes. + /// + /// After calling this method, this request will no longer have a body. + std::vector take_body_bytes(); + + // ChunksIter read_body_chunks(size_t chunk_size); + + /// Get the MIME type described by the request's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header, or `std::nullopt` if that header is absent or contains an + /// invalid MIME type. + std::optional get_content_type(); + + /// Builder-style equivalent of + /// `Request::set_content_type()`. + Request with_content_type(std::string_view mime) &&; + + /// Set the MIME type described by the request's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header. + /// + /// Any existing `Content-Type` header values will be overwritten. + void set_content_type(std::string_view mime); + + /// Get the value of the request's + /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) + /// header, if it exists. + std::optional get_content_length(); + + /// Returns whether the given header name is present in the request. + fastly::expected contains_header(std::string_view name); + + /// Builder-style equivalent of `Request::append_header()`. + fastly::expected with_header(std::string_view name, + std::string_view value) &&; + + /// Builder-style equivalent of `Request::set_header()`. + fastly::expected with_set_header(std::string_view name, + std::string_view value) &&; + + /// Get the value of a header as a string, or `std::nullopt` if the header + /// is not present. + /// + /// If there are multiple values for the header, only one is returned, which + /// may be any of the values. See + /// `Request::get_header_all()` + /// all of the values. + fastly::expected> + get_header(std::string_view name); + + /// Get an iterator of all the values of a header. + fastly::expected get_header_all(std::string_view name); + fastly::expected get_headers(); + fastly::expected get_header_names(); + + /// Set a request header to the given value, discarding any previous values + /// for the given header name. + fastly::expected set_header(std::string_view name, + std::string_view value); + + /// Add a request header with given value. + /// + /// Unlike `Request::set_header()`, this does not discard existing values + /// for the same header name. + fastly::expected append_header(std::string_view name, + std::string_view value); + + /// Remove all request headers of the given name, and return one of the + /// removed header values if any were present. + fastly::expected> + remove_header(std::string_view name); + + /// Builder-style equivalent of `Request::set_method()`. + Request with_method(Method method) &&; + + /// Get the request method. + Method get_method(); + + /// Set the request method. + void set_method(Method method); + + /// Builder-style equivalent of `Request::set_url()`. + fastly::expected with_url(std::string_view url) &&; + + /// Get the request URL as a string. + std::string get_url(); + + /// Set the request URL. + fastly::expected set_url(std::string_view url); + + /// Get the path component of the request URL. + /// + /// # Examples + /// + /// ```cpp + /// auto req{Request::get("https://example.com/hello#world")}; + /// assert(req.get_path() == "/hello"); + /// ``` + std::string get_path(); + + /// Builder-style equivalent of `Request::set_path()`. + fastly::expected with_path(std::string_view path) &&; + + /// Set the path component of the request URL. + /// # Examples + /// + /// ```cpp + /// auto req{Request::get("https://example.com/")}; + /// req.set_path("/hello"); + /// assert!(req.get_url(), "https://example.com/hello"); + /// ``` + fastly::expected set_path(std::string_view path); + + /// Get the query component of the request URL, if it exists, as a + /// percent-encoded ASCII string. + std::optional get_query_string(); + + /// Get the value of a query parameter in the request's URL. + /// + /// This assumes that the query string is a `&` separated list of + /// `parameter=value` pairs. The value of the first occurrence of + /// `parameter` is returned. No URL decoding is performed. + std::optional get_query_parameter(std::string_view param); + + /// Builder-style equivalent of `Request::set_query()`. + fastly::expected with_query_string(std::string_view query) &&; + + /// Set the query string of the request URL query component to the given + /// string, performing percent-encoding if necessary. + /// + /// # Examples + /// + /// ```no_run + /// auto req{Request::get("https://example.com/foo")}; + /// req.set_query_string("hello=🌐!&bar=baz"); + /// assert(req.get_url(), + /// "https://example.com/foo?hello=%F0%9F%8C%90!&bar=baz"); + /// ``` + fastly::expected set_query_string(std::string_view query); + + /// Remove the query component from the request URL, if one exists. + void remove_query(); + + /// Builder-style equivalent of `Request::set_version()`. + Request with_version(Version version) &&; + + /// Get the HTTP version of this request. + Version get_version(); + + /// Set the HTTP version of this request. + void set_version(Version version); + + /// Builder-style equivalent of `Request::set_pass()`. + Request with_pass(bool pass) &&; + + /// Set whether this request should be cached if sent to a backend. + /// + /// By default this is `false`, which means the backend will only be reached + /// if a cached response is not available. Set this to `true` to send the + /// request directly to the backend without caching. + /// + /// # Overrides + /// + /// Setting this to `true` overrides any other custom caching behaviors for + /// this request, such as `Request::set_ttl()` or + /// `Request::set_surrogate_key()`. + void set_pass(bool pass); + + /// Builder-style equivalent of `Request::set_ttl()`. + Request with_ttl(uint32_t ttl) &&; + + /// Override the caching behavior of this request to use the given Time to + /// Live (TTL), in seconds. + /// + /// # Overrides + /// + /// This overrides the behavior specified in the response headers, and sets + /// the `Request::set_pass()` behavior to `false`. + void set_ttl(uint32_t ttl); + + /// Builder-style equivalent of + /// `Request::set_stale_while_revalidate()`. + Request with_stale_while_revalidate(uint32_t swr) &&; + + /// Override the caching behavior of this request to use the given + /// `stale-while-revalidate` time, in seconds. + /// + /// # Overrides + /// + /// This overrides the behavior specified in the response headers, and sets + /// the `Request::set_pass()` behavior to `false`. + void set_stale_while_revalidate(uint32_t swr); + + /// Builder-style equivalent of `Request::set_pci()`. + Request with_pci(bool pci) &&; + + /// Override the caching behavior of this request to enable or disable + /// PCI/HIPAA-compliant non-volatile caching. + /// + /// By default, this is `false`, which means the request may not be + /// PCI/HIPAA-compliant. Set it to `true` to enable compliant caching. + /// + /// See the [Fastly PCI-Compliant Caching and Delivery + /// documentation](https://docs.fastly.com/products/pci-compliant-caching-and-delivery) + /// for details. + /// + /// # Overrides + /// + /// This sets the `Request::set_pass()` behavior to `false`. + void set_pci(bool pci); + + /// Builder-style equivalent of + /// `Request::set_surrogate_key()`. + fastly::expected with_surrogate_key(std::string_view sk) &&; + + /// Override the caching behavior of this request to include the given + /// surrogate key(s), provided as a header value. + /// + /// The header value can contain more than one surrogate key, separated by + /// spaces. + /// + /// Surrogate keys must contain only printable ASCII characters (those + /// between `0x21` and `0x7E`, inclusive). Any invalid keys will be ignored. + /// + /// See the [Fastly surrogate keys + /// guide](https://docs.fastly.com/en/guides/purging-api-cache-with-surrogate-keys) + /// for details. + /// + /// # Overrides + /// + /// This sets the `Request::set_pass()` behavior to `false`, and + /// extends (but does not replace) any `Surrogate-Key` response headers from + /// the backend. + fastly::expected set_surrogate_key(std::string_view sk); + + std::optional get_client_ip_addr(); + std::optional get_server_ip_addr(); + + std::optional get_original_header_names(); + std::optional get_original_header_count(); + + /// Returns whether the request was tagged as contributing to a DDoS attack + /// + /// Returns `std::nullopt` if this is not the client request. + std::optional get_client_ddos_detected(); + + // std::optional> get_tls_client_hello(); + // std::optional> get_tls_ja3_md5(); + // std::optional get_tls_ja4(); + // std::optional get_tls_raw_client_certificate(); + // std::optional> + // get_tls_raw_client_certificate_bytes(); + // // TODO(@zkat): needs additional type + // // std::optional + // get_tls_client_cert_verify_result(); std::optional + // get_tls_cipher_openssl_name(); std::optional> + // get_tls_cipher_openssl_name_bytes(); std::optional> + // get_tls_protocol_bytes(); + + /// Set whether a `gzip`-encoded response to this request will be + /// automatically decompressed. + /// + /// Enabling this will set the `Accept-Encoding` header before the request + /// is sent, regardless of the original value in the request, to ensure that + /// any values originally sent by a browser or other client get replaced + /// with `gzip`, so that the backend will not try sending unsupported + /// compression algorithms. + /// + /// If the response to this request is `gzip`-encoded, it will be presented + /// in decompressed form, and the `Content-Encoding` and `Content-Length` + /// headers will be removed. + void set_auto_decompress_gzip(bool gzip); + + /// Builder-style equivalent of + /// `Request::set_auto_decompress_gzip()`. + Request with_auto_decompress_gzip(bool gzip) &&; + + // TODO(@zkat): needs enum + // void set_framing_headers_mode(FramingHeadersMode mode); + // Request *set_framing_headers_mode(FramingHeadersMode mode); + + /// Returns whether or not the client request had a `Fastly-Key` header + /// which is valid for purging content for the service. + /// + /// This function ignores the current value of any `Fastly-Key` header for + /// this request. + bool fastly_key_is_valid(); + + // void handoff_websocket(fastly::backend::Backend backend); + // void handoff_fanout(fastly::backend::Backend backend); + // Request *on_behalf_of(std::string_view service); + + /// Set the cache key to be used when attempting to satisfy this request + /// from a cached response. + void set_cache_key(std::string_view key); + + /// Set the cache key to be used when attempting to satisfy this request + /// from a cached response. + void set_cache_key(std::vector key); + + /// Builder-style equivalent of `Request::set_cache_key()`. + Request with_cache_key(std::string_view key) &&; + + /// Builder-style equivalent of `Request::set_cache_key()`. + Request with_cache_key(std::vector key) &&; + + /// Gets whether the request is potentially cacheable. + bool is_cacheable(); + +private: + auto &inner() { return req; } + Request(rust::Box r) : req(std::move(r)) {}; + rust::Box req; +}; } // namespace fastly::http -namespace fastly -{ - using fastly::http::Request; +namespace fastly { +using fastly::http::Request; } #endif From 8e9972e1b3f1c967543ae44384a610b6e9f63c49 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 16:49:12 +0100 Subject: [PATCH 09/17] Formatting --- include/fastly/http/response.h | 1053 ++++++++++++++++---------------- 1 file changed, 524 insertions(+), 529 deletions(-) diff --git a/include/fastly/http/response.h b/include/fastly/http/response.h index ef270fa..efeeeb2 100644 --- a/include/fastly/http/response.h +++ b/include/fastly/http/response.h @@ -3,574 +3,569 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include #include #include #include -namespace fastly::backend -{ - class Backend; +namespace fastly::backend { +class Backend; } -namespace fastly::http -{ +namespace fastly::http { + +class Body; +class StreamingBody; +class Response; +class Request; +namespace request { +class PendingRequest; +std::pair, std::vector> +select(std::vector &reqs); +} // namespace request + +/// An HTTP response, including body, headers, and status code. +/// +/// # Sending to the client +/// +/// Each execution of a Compute program may send a single response back to the +/// client: +/// +/// - `Response::send_to_client()` +/// - `Response::stream_to_client()` +/// +/// If no response is explicitly sent by the program, a default `200 OK` +/// response is sent. +/// +/// # Creation and conversion +/// +/// Responses can be created programmatically: +/// +/// - `Response::new()` +/// - `Response::from_body()` +/// - `Response::from_status()` +/// +/// Responses are also returned from backend requests: +/// +/// - `Request::send()` +/// - `Request::send_async()` +/// - `Request::send_async_streaming()` +/// +/// # Builder-style methods +/// +/// `Response` can be used as a +/// [builder](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html), +/// allowing responses to be constructed and used through method chaining. +/// Methods with the `with_` name prefix, such as `Response::with_header()`, +/// return `std::move(*this)` to allow chaining. The builder style is typically +/// most useful when constructing and using a response in a single expression. +/// For example: +/// +/// ```cpp +/// Response() +/// .with_header("my-header", "hello!") +/// .with_header("my-other-header", "Здравствуйте!") +/// .send_to_client(); +/// ``` +/// +/// # Setter methods +/// +/// Setter methods, such as `Response::set_header()`, are prefixed by `set_`, +/// and can be used interchangeably with the builder-style methods, allowing you +/// to mix and match styles based on what is most convenient for your program. +/// Setter methods tend to work better than builder-style methods when +/// constructing a value involves conditional branches or loops. For example: +/// +/// ```cpp +/// auto resp{Response().with_header("my-header", "hello!")}; +/// if (needs_translation) { +/// resp.set_header("my-other-header", "Здравствуйте!"); +/// } +/// resp.send_to_client(); +/// ``` +class Response { + friend detail::AccessBridgeInternals; + friend Request; + friend request::PendingRequest; + friend std::pair, + std::vector> + request::select(std::vector &reqs); + +public: + /// Create a new `Response`. + /// + /// The new response is created with status code `200 OK`, no headers, and an + /// empty body. + Response(); + // TODO(@zkat): Make this a "friend"? + /// Return whether the response is from a backend request. + bool is_from_backend(); + + /// Make a new response with the same headers, status, and version of this + /// response, but no body. + /// + /// If you also need to clone the response body, use + /// `Response::clone_with_body()`. + Response clone_without_body(); - class Body; - class StreamingBody; - class Response; - class Request; - namespace request - { - class PendingRequest; - std::pair, std::vector> - select(std::vector &reqs); - } // namespace request + /// Clone this response by reading in its body, and then writing the same body + /// to the original and the cloned response. + /// + /// This method requires mutable access to this response because reading from + /// and writing to the body can involve an HTTP connection. + Response clone_with_body(); - /// An HTTP response, including body, headers, and status code. + /// Create a new `Response` with the given value as the body. + static Response from_body(Body body); + + /// Create a new response with the given status code. + static Response from_status(StatusCode status); + + /// Create a 303 See Other response with the given value as the `Location` + /// header. /// - /// # Sending to the client + /// # Examples /// - /// Each execution of a Compute program may send a single response back to the - /// client: + /// ```cpp + /// auto resp{Response::see_other("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::SEE_OTHER); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response see_other(std::string_view destination); + + /// Create a 308 Permanent Redirect response with the given value as the + /// `Location` header. /// - /// - `Response::send_to_client()` - /// - `Response::stream_to_client()` + /// # Examples /// - /// If no response is explicitly sent by the program, a default `200 OK` - /// response is sent. + /// ```cpp + /// auto resp{Response::redirect("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::PERMANENT_REDIRECT); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response redirect(std::string_view destination); + + /// Create a 307 Temporary Redirect response with the given value as the + /// `Location` header. /// - /// # Creation and conversion + /// # Examples /// - /// Responses can be created programmatically: + /// ```cpp + /// auto resp{Response::temporary_redirect("https://www.fastly.com")}; + /// assert(resp.get_status() == StatusCode::TEMPORARY_REDIRECT); + /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); + /// ``` + static Response temporary_redirect(std::string_view destination); + + /// Builder-style equivalent of `Response::set_body()`. + Response with_body(Body body) &&; + + /// Returns `true` if this response has a body. + bool has_body(); + + /// Set the given value as the response's body. + void set_body(Body body); + + /// Take and return the body from this response. /// - /// - `Response::new()` - /// - `Response::from_body()` - /// - `Response::from_status()` + /// After calling this method, this response will no longer have a body. + Body take_body(); + + /// Append another `Body` to the body of this response without reading or + /// writing any body contents. /// - /// Responses are also returned from backend requests: + /// If this response does not have a body, the appended body is set as the + /// response's body. /// - /// - `Request::send()` - /// - `Request::send_async()` - /// - `Request::send_async_streaming()` + /// This method should be used when combining bodies that have not necessarily + /// been read yet, such as a body returned from a backend response. /// - /// # Builder-style methods + /// # Examples /// - /// `Response` can be used as a - /// [builder](https://doc.rust-lang.org/1.0.0/style/ownership/builders.html), - /// allowing responses to be constructed and used through method chaining. - /// Methods with the `with_` name prefix, such as `Response::with_header()`, - /// return `std::move(*this)` to allow chaining. The builder style is typically - /// most useful when constructing and using a response in a single expression. - /// For example: + /// ```cpp + /// auto resp{Response::from_body("hello! backend says: ")}; + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// resp.append_body(backend_resp.into_body()); + /// resp.send_to_client(); + /// ``` + void append_body(Body body); + + /// Consume the response and return its body as a byte vector. + std::vector into_body_bytes(); + + /// Consume the response and return its body as a string. + std::string into_body_string(); + + /// Consume the response and return its body. + Body into_body(); + + /// Builder-style equivalent of + /// `Response::set_body_text_plain()`. + fastly::expected with_body_text_plain(std::string_view body) &&; + + /// Set the given string as the response's body with content type `text/plain; + /// charset=UTF-8`. + fastly::expected set_body_text_plain(std::string_view body); + + /// Builder-style equivalent of + /// `Response::set_body_text_html()`. + fastly::expected with_body_text_html(std::string_view body) &&; + + /// Set the given string as the response's body with content type `text/html; + /// charset=UTF-8`. + fastly::expected set_body_text_html(std::string_view body); + + /// Take and return the body from this response as a string. + /// + /// After calling this method, this response will no longer have a body. + std::string take_body_string(); + + /// Builder-style equivalent of + /// `Response::set_body_octet_stream()`. + Response with_body_octet_stream(std::vector body) &&; + + /// Set the given bytes as the response's body with content type + /// `application/octet-stream`. + void set_body_octet_stream(std::vector body); + + /// Take and return the body from this response as a vector of bytes. + /// + /// After calling this method, this response will no longer have a body. + std::vector take_body_bytes(); + + /// Get the MIME type described by the response's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header, or `std::nullopt` if that header is absent or contains an invalid + /// MIME type. + std::optional get_content_type(); + + /// Builder-style equivalent of + /// `Response::set_content_type()`. + Response with_content_type(std::string_view mime) &&; + + /// Set the MIME type described by the response's + /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) + /// header. + /// + /// Any existing `Content-Type` header values will be overwritten. + void set_content_type(std::string_view mime); + + /// Get the value of the response's + /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) + /// header, if it exists. + std::optional get_content_length(); + + /// Returns whether the given header name is present in the response. + fastly::expected contains_header(std::string_view name); + + /// Builder-style equivalent of `Response::append_header()`. + fastly::expected with_header(std::string_view name, + std::string_view value) &&; + + /// Builder-style equivalent of `Response::set_header()`. + fastly::expected with_set_header(std::string_view name, + std::string_view value) &&; + + /// Get the value of a header, or `std::nullopt` if the header is + /// not present. + /// + /// If there are multiple values for the header, only one is returned, which + /// may be any of the values. See + /// `Response::get_header_all()` + /// all of the values. + fastly::expected> + get_header(std::string_view name); + + /// Get an iterator of all the values of a header. + fastly::expected get_header_all(std::string_view name); + fastly::expected get_headers(); + fastly::expected get_header_names(); + + /// Set a response header to the given value, discarding any previous values + /// for the given header name. + fastly::expected set_header(std::string_view name, + std::string_view value); + + /// Add a request header with given value. + /// + /// Unlike `Response::set_header()`, this does not discard existing values for + /// the same header name. + fastly::expected append_header(std::string_view name, + std::string_view value); + + /// Remove all request headers of the given name, and return one of the + /// removed header values if any were present. + fastly::expected> + remove_header(std::string_view name); + + /// Builder-style equivalent of `Response::set_status()`. + Response with_status(StatusCode status) &&; + + /// Set the HTTP status code of the response. + /// + /// # Examples + /// + /// Using the constants from `StatusCode`: + /// + /// ```cpp + /// auto resp{fastly::Response::from_body("not found!")}; + /// resp.set_status(fastly::http::StatusCode::NOT_FOUND); + /// resp.send_to_client(); + /// ``` + /// + /// Using a `uint16_t`: /// /// ```cpp - /// Response() - /// .with_header("my-header", "hello!") - /// .with_header("my-other-header", "Здравствуйте!") - /// .send_to_client(); + /// auto resp{fastly::Response::from_body("not found!")}; + /// resp.set_status(404); + /// resp.send_to_client(); /// ``` + void set_status(StatusCode status); + + /// Builder-style equivalent of `Response::set_version()`. + Response with_version(Version version) &&; + + /// Get the HTTP version of this response. + Version get_version(); + + /// Set the HTTP version of this response. + void set_version(Version version); + + // TODO(@zkat): needs enum + // void set_framing_headers_mode(FramingHeadersMode mode); + // Response set_framing_headers_mode(FramingHeadersMode mode); + + /// Get the name of the `Backend` this response came from, or `std::nullopt` + /// if the response is synthetic. /// - /// # Setter methods + /// # Examples /// - /// Setter methods, such as `Response::set_header()`, are prefixed by `set_`, - /// and can be used interchangeably with the builder-style methods, allowing you - /// to mix and match styles based on what is most convenient for your program. - /// Setter methods tend to work better than builder-style methods when - /// constructing a value involves conditional branches or loops. For example: + /// From a backend response: /// /// ```cpp - /// auto resp{Response().with_header("my-header", "hello!")}; - /// if (needs_translation) { - /// resp.set_header("my-other-header", "Здравствуйте!"); + /// auto + /// backend_resp{Request::get("https://example.com/").send("example_backend")}; + /// assert(backend_resp.get_backend_name(), + /// std::optional("example_backend")); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend_name() == std::nullopt); + /// ``` + std::optional get_backend_name(); + + /// Get the backend this response came from, or `std::nullopt` if the response + /// is synthetic. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// assert( + /// backend_resp.get_backend() == + /// std::optional(Backend::from_name("example_backend")) + /// ); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend() == std::nullopt); + /// ``` + std::optional get_backend(); + + /// Get the address of the backend this response came from, or `std::nullopt` + /// when the response is synthetic or cached. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{Request::get("https://example.com/") + /// .with_pass(true) + /// .send("example_backend")}; + /// assert( + /// backend_resp.get_backend_addr() == + /// std::optional("127.0.0.1:443")); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.get_backend_addr() == std::nullopt); + /// ``` + std::optional get_backend_addr(); + + /// Take and return the request this response came from, or `std::nullopt` if + /// the response is synthetic. + /// + /// Note that the returned request will only have the headers and metadata of + /// the original request, as the body is consumed when sending the request. + /// + /// # Examples + /// + /// From a backend response: + /// + /// ```cpp + /// auto backend_resp{Request::post("https://example.com/") + /// .with_body("hello") + /// .send("example_backend")}; + /// auto backend_req{backend_resp.take_backend_request().value()}; + /// assert(backend_req.get_url() == "https://example.com/"); + /// assert(!backend_req.has_body()); + /// backend_req.with_body("goodbye").send("example_backend"); + /// ``` + /// + /// From a synthetic response: + /// + /// ```cpp + /// Response synthetic_resp; + /// assert(synthetic_resp.take_backend_request() == std::nullopt); + /// ``` + std::optional take_backend_request(); + + /// Begin sending the response to the client. + /// + /// This method returns as soon as the response header begins sending to the + /// client, and transmission of the response will continue in the background. + /// + /// Once this method is called, nothing else may be added to the response + /// body. To stream additional data to a response body after it begins to + /// send, use `Response::stream_to_client()`. + /// + /// # Panics + /// + /// This method panics if another response has already been sent to the client + /// by this method, by `Response::stream_to_client()`. + /// + /// # Examples + /// + /// Sending a backend response without modification: + /// + /// ```cpp + /// Request::get("https://example.com/").send("example_backend").send_to_client(); + /// ``` + /// + /// Removing a header from a backend response before sending to the client: + /// + /// ```cpp + /// auto + /// backend_resp{Request::get("https://example.com/").send("example_backend")}; + /// backend_resp.remove_header("bad-header"); + /// backend_resp.send_to_client(); + /// ``` + /// + /// Sending a synthetic response: + /// + /// ```cpp + /// Response::from_body("hello, world!").send_to_client(); + /// ``` + void send_to_client(); + + /// Begin sending the response to the client, and return a `StreamingBody` + /// that can accept further data to send. + /// + /// The client connection must be closed when finished writing the response by + /// calling `StreamingBody::finish()`. + /// + /// This method is most useful for programs that do some sort of processing or + /// inspection of a potentially-large backend response body. Streaming allows + /// the program to operate on small parts of the body rather than having to + /// read it all into memory at once. + /// + /// This method returns as soon as the response header begins sending to the + /// client, and transmission of the response will continue in the background. + /// + /// # Panics + /// + /// This method panics if another response has already been sent to the client + /// by this method, by `Response::send_to_client()`. + /// + /// # Examples + /// + /// Count the number of lines in a UTF-8 backend response body while sending + /// it to the client: + /// + /// ```cpp + /// auto backend_resp{ + /// Request::get("https://example.com/").send("example_backend") + /// }; + /// + /// // Take the body so we can iterate through its lines later + /// auto backend_resp_body{backend_resp.take_body()}; + /// + /// // Start sending the backend response to the client with a now-empty body + /// auto client_body{backend_resp.stream_to_client()}; + /// + /// size_t num_lines{0}; + /// std::string line; + /// while (getline(backend_resp_body, line)) { + /// num_lines++; + /// client_body << line; /// } - /// resp.send_to_client(); + /// // Finish the streaming body to close the client connection. + /// client_body.finish(); + /// + /// std::cout + /// << "backend response body contained " + /// << num_lines + /// << " lines" + /// << std::endl; /// ``` - class Response - { - friend detail::AccessBridgeInternals; - friend Request; - friend request::PendingRequest; - friend std::pair, - std::vector> - request::select(std::vector &reqs); - - public: - /// Create a new `Response`. - /// - /// The new response is created with status code `200 OK`, no headers, and an - /// empty body. - Response(); - // TODO(@zkat): Make this a "friend"? - /// Return whether the response is from a backend request. - bool is_from_backend(); - - /// Make a new response with the same headers, status, and version of this - /// response, but no body. - /// - /// If you also need to clone the response body, use - /// `Response::clone_with_body()`. - Response clone_without_body(); - - /// Clone this response by reading in its body, and then writing the same body - /// to the original and the cloned response. - /// - /// This method requires mutable access to this response because reading from - /// and writing to the body can involve an HTTP connection. - Response clone_with_body(); - - /// Create a new `Response` with the given value as the body. - static Response from_body(Body body); - - /// Create a new response with the given status code. - static Response from_status(StatusCode status); - - /// Create a 303 See Other response with the given value as the `Location` - /// header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::see_other("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::SEE_OTHER); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response see_other(std::string_view destination); - - /// Create a 308 Permanent Redirect response with the given value as the - /// `Location` header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::redirect("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::PERMANENT_REDIRECT); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response redirect(std::string_view destination); - - /// Create a 307 Temporary Redirect response with the given value as the - /// `Location` header. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::temporary_redirect("https://www.fastly.com")}; - /// assert(resp.get_status() == StatusCode::TEMPORARY_REDIRECT); - /// assert(resp.get_header("Location").value(), "https://www.fastly.com"); - /// ``` - static Response temporary_redirect(std::string_view destination); - - /// Builder-style equivalent of `Response::set_body()`. - Response with_body(Body body) &&; - - /// Returns `true` if this response has a body. - bool has_body(); - - /// Set the given value as the response's body. - void set_body(Body body); - - /// Take and return the body from this response. - /// - /// After calling this method, this response will no longer have a body. - Body take_body(); - - /// Append another `Body` to the body of this response without reading or - /// writing any body contents. - /// - /// If this response does not have a body, the appended body is set as the - /// response's body. - /// - /// This method should be used when combining bodies that have not necessarily - /// been read yet, such as a body returned from a backend response. - /// - /// # Examples - /// - /// ```cpp - /// auto resp{Response::from_body("hello! backend says: ")}; - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// resp.append_body(backend_resp.into_body()); - /// resp.send_to_client(); - /// ``` - void append_body(Body body); - - /// Consume the response and return its body as a byte vector. - std::vector into_body_bytes(); - - /// Consume the response and return its body as a string. - std::string into_body_string(); - - /// Consume the response and return its body. - Body into_body(); - - /// Builder-style equivalent of - /// `Response::set_body_text_plain()`. - fastly::expected with_body_text_plain(std::string_view body) &&; - - /// Set the given string as the response's body with content type `text/plain; - /// charset=UTF-8`. - fastly::expected set_body_text_plain(std::string_view body); - - /// Builder-style equivalent of - /// `Response::set_body_text_html()`. - fastly::expected with_body_text_html(std::string_view body) &&; - - /// Set the given string as the response's body with content type `text/html; - /// charset=UTF-8`. - fastly::expected set_body_text_html(std::string_view body); - - /// Take and return the body from this response as a string. - /// - /// After calling this method, this response will no longer have a body. - std::string take_body_string(); - - /// Builder-style equivalent of - /// `Response::set_body_octet_stream()`. - Response with_body_octet_stream(std::vector body) &&; - - /// Set the given bytes as the response's body with content type - /// `application/octet-stream`. - void set_body_octet_stream(std::vector body); - - /// Take and return the body from this response as a vector of bytes. - /// - /// After calling this method, this response will no longer have a body. - std::vector take_body_bytes(); - - /// Get the MIME type described by the response's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header, or `std::nullopt` if that header is absent or contains an invalid - /// MIME type. - std::optional get_content_type(); - - /// Builder-style equivalent of - /// `Response::set_content_type()`. - Response with_content_type(std::string_view mime) &&; - - /// Set the MIME type described by the response's - /// [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) - /// header. - /// - /// Any existing `Content-Type` header values will be overwritten. - void set_content_type(std::string_view mime); - - /// Get the value of the response's - /// [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) - /// header, if it exists. - std::optional get_content_length(); - - /// Returns whether the given header name is present in the response. - fastly::expected contains_header(std::string_view name); - - /// Builder-style equivalent of `Response::append_header()`. - fastly::expected with_header(std::string_view name, - std::string_view value) &&; - - /// Builder-style equivalent of `Response::set_header()`. - fastly::expected with_set_header(std::string_view name, - std::string_view value) &&; - - /// Get the value of a header, or `std::nullopt` if the header is - /// not present. - /// - /// If there are multiple values for the header, only one is returned, which - /// may be any of the values. See - /// `Response::get_header_all()` - /// all of the values. - fastly::expected> - get_header(std::string_view name); - - /// Get an iterator of all the values of a header. - fastly::expected get_header_all(std::string_view name); - fastly::expected get_headers(); - fastly::expected get_header_names(); - - /// Set a response header to the given value, discarding any previous values - /// for the given header name. - fastly::expected set_header(std::string_view name, - std::string_view value); - - /// Add a request header with given value. - /// - /// Unlike `Response::set_header()`, this does not discard existing values for - /// the same header name. - fastly::expected append_header(std::string_view name, - std::string_view value); - - /// Remove all request headers of the given name, and return one of the - /// removed header values if any were present. - fastly::expected> - remove_header(std::string_view name); - - /// Builder-style equivalent of `Response::set_status()`. - Response with_status(StatusCode status) &&; - - /// Set the HTTP status code of the response. - /// - /// # Examples - /// - /// Using the constants from `StatusCode`: - /// - /// ```cpp - /// auto resp{fastly::Response::from_body("not found!")}; - /// resp.set_status(fastly::http::StatusCode::NOT_FOUND); - /// resp.send_to_client(); - /// ``` - /// - /// Using a `uint16_t`: - /// - /// ```cpp - /// auto resp{fastly::Response::from_body("not found!")}; - /// resp.set_status(404); - /// resp.send_to_client(); - /// ``` - void set_status(StatusCode status); - - /// Builder-style equivalent of `Response::set_version()`. - Response with_version(Version version) &&; - - /// Get the HTTP version of this response. - Version get_version(); - - /// Set the HTTP version of this response. - void set_version(Version version); - - // TODO(@zkat): needs enum - // void set_framing_headers_mode(FramingHeadersMode mode); - // Response set_framing_headers_mode(FramingHeadersMode mode); - - /// Get the name of the `Backend` this response came from, or `std::nullopt` - /// if the response is synthetic. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com/").send("example_backend")}; - /// assert(backend_resp.get_backend_name(), - /// std::optional("example_backend")); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend_name() == std::nullopt); - /// ``` - std::optional get_backend_name(); - - /// Get the backend this response came from, or `std::nullopt` if the response - /// is synthetic. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// assert( - /// backend_resp.get_backend() == - /// std::optional(Backend::from_name("example_backend")) - /// ); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend() == std::nullopt); - /// ``` - std::optional get_backend(); - - /// Get the address of the backend this response came from, or `std::nullopt` - /// when the response is synthetic or cached. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto backend_resp{Request::get("https://example.com/") - /// .with_pass(true) - /// .send("example_backend")}; - /// assert( - /// backend_resp.get_backend_addr() == - /// std::optional("127.0.0.1:443")); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.get_backend_addr() == std::nullopt); - /// ``` - std::optional get_backend_addr(); - - /// Take and return the request this response came from, or `std::nullopt` if - /// the response is synthetic. - /// - /// Note that the returned request will only have the headers and metadata of - /// the original request, as the body is consumed when sending the request. - /// - /// # Examples - /// - /// From a backend response: - /// - /// ```cpp - /// auto backend_resp{Request::post("https://example.com/") - /// .with_body("hello") - /// .send("example_backend")}; - /// auto backend_req{backend_resp.take_backend_request().value()}; - /// assert(backend_req.get_url() == "https://example.com/"); - /// assert(!backend_req.has_body()); - /// backend_req.with_body("goodbye").send("example_backend"); - /// ``` - /// - /// From a synthetic response: - /// - /// ```cpp - /// Response synthetic_resp; - /// assert(synthetic_resp.take_backend_request() == std::nullopt); - /// ``` - std::optional take_backend_request(); - - /// Begin sending the response to the client. - /// - /// This method returns as soon as the response header begins sending to the - /// client, and transmission of the response will continue in the background. - /// - /// Once this method is called, nothing else may be added to the response - /// body. To stream additional data to a response body after it begins to - /// send, use `Response::stream_to_client()`. - /// - /// # Panics - /// - /// This method panics if another response has already been sent to the client - /// by this method, by `Response::stream_to_client()`. - /// - /// # Examples - /// - /// Sending a backend response without modification: - /// - /// ```cpp - /// Request::get("https://example.com/").send("example_backend").send_to_client(); - /// ``` - /// - /// Removing a header from a backend response before sending to the client: - /// - /// ```cpp - /// auto - /// backend_resp{Request::get("https://example.com/").send("example_backend")}; - /// backend_resp.remove_header("bad-header"); - /// backend_resp.send_to_client(); - /// ``` - /// - /// Sending a synthetic response: - /// - /// ```cpp - /// Response::from_body("hello, world!").send_to_client(); - /// ``` - void send_to_client(); - - /// Begin sending the response to the client, and return a `StreamingBody` - /// that can accept further data to send. - /// - /// The client connection must be closed when finished writing the response by - /// calling `StreamingBody::finish()`. - /// - /// This method is most useful for programs that do some sort of processing or - /// inspection of a potentially-large backend response body. Streaming allows - /// the program to operate on small parts of the body rather than having to - /// read it all into memory at once. - /// - /// This method returns as soon as the response header begins sending to the - /// client, and transmission of the response will continue in the background. - /// - /// # Panics - /// - /// This method panics if another response has already been sent to the client - /// by this method, by `Response::send_to_client()`. - /// - /// # Examples - /// - /// Count the number of lines in a UTF-8 backend response body while sending - /// it to the client: - /// - /// ```cpp - /// auto backend_resp{ - /// Request::get("https://example.com/").send("example_backend") - /// }; - /// - /// // Take the body so we can iterate through its lines later - /// auto backend_resp_body{backend_resp.take_body()}; - /// - /// // Start sending the backend response to the client with a now-empty body - /// auto client_body{backend_resp.stream_to_client()}; - /// - /// size_t num_lines{0}; - /// std::string line; - /// while (getline(backend_resp_body, line)) { - /// num_lines++; - /// client_body << line; - /// } - /// // Finish the streaming body to close the client connection. - /// client_body.finish(); - /// - /// std::cout - /// << "backend response body contained " - /// << num_lines - /// << " lines" - /// << std::endl; - /// ``` - StreamingBody stream_to_client(); - - /// Get the Time to Live (TTL) in the cache for this response, if it is - /// cached. - /// - /// The TTL provides the duration of "freshness" for the cached response - /// after it is inserted into the cache. If the response is stale, - /// the TTL is 0 (i.e. this returns `std::optional(0)`. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_ttl(); - - /// The current age of the response, if it is cached. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_age(); - - /// The time for which the response can safely be used despite being - /// considered stale, if it is cached. - /// - /// Returns `std::nullopt` if the response is not cached. - std::optional get_stale_while_revalidate(); - - private: - auto &inner() { return res; } - Response(rust::Box response) - : res(std::move(response)) {}; - rust::Box res; - }; + StreamingBody stream_to_client(); + + /// Get the Time to Live (TTL) in the cache for this response, if it is + /// cached. + /// + /// The TTL provides the duration of "freshness" for the cached response + /// after it is inserted into the cache. If the response is stale, + /// the TTL is 0 (i.e. this returns `std::optional(0)`. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_ttl(); + + /// The current age of the response, if it is cached. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_age(); + + /// The time for which the response can safely be used despite being + /// considered stale, if it is cached. + /// + /// Returns `std::nullopt` if the response is not cached. + std::optional get_stale_while_revalidate(); + +private: + auto &inner() { return res; } + Response(rust::Box response) + : res(std::move(response)) {}; + rust::Box res; +}; } // namespace fastly::http -namespace fastly -{ - using fastly::http::Response; +namespace fastly { +using fastly::http::Response; } #endif From 95b03cd628f79475543a957afc24a8e14b0afc47 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Fri, 12 Sep 2025 17:02:49 +0100 Subject: [PATCH 10/17] Make it harder to accidentally construct a tag type --- include/fastly/detail/rust_bridge_tags.h | 11 +++++++++-- include/fastly/esi.h | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/include/fastly/detail/rust_bridge_tags.h b/include/fastly/detail/rust_bridge_tags.h index 5c764d3..f96a66f 100644 --- a/include/fastly/detail/rust_bridge_tags.h +++ b/include/fastly/detail/rust_bridge_tags.h @@ -11,11 +11,18 @@ #define FASTLY_DETAIL_RUST_BRIDGE_TAGS_H namespace fastly::detail::rust_bridge_tags { +// Used to make it harder to accidentally create instances of the tag types. +struct IConfirmIHaveInheritedFromThisTag {}; + namespace esi { // esi.h:DispatchFragmentRequestFn -struct DispatchFragmentRequestFnTag {}; +struct DispatchFragmentRequestFnTag { + DispatchFragmentRequestFnTag(IConfirmIHaveInheritedFromThisTag) {} +}; // esi.h:ProcessFragmentResponseFn -struct ProcessFragmentResponseFnTag {}; +struct ProcessFragmentResponseFnTag { + ProcessFragmentResponseFnTag(IConfirmIHaveInheritedFromThisTag) {} +}; } // namespace esi } // namespace fastly::detail::rust_bridge_tags diff --git a/include/fastly/esi.h b/include/fastly/esi.h index 7ddfb64..6e0c255 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -43,7 +43,10 @@ class DispatchFragmentRequestFn using function_type = std::function(Request)>; template F> - DispatchFragmentRequestFn(F &&fn) : fn_(std::forward(fn)) {} + DispatchFragmentRequestFn(F &&fn) + : detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag( + detail::rust_bridge_tags::IConfirmIHaveInheritedFromThisTag{}), + fn_(std::forward(fn)) {} private: friend detail::AccessBridgeInternals; @@ -57,7 +60,10 @@ class ProcessFragmentResponseFn using function_type = std::function(Request &, Response)>; template F> - ProcessFragmentResponseFn(F &&fn) : fn_(std::forward(fn)) {} + ProcessFragmentResponseFn(F &&fn) + : detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag( + detail::rust_bridge_tags::IConfirmIHaveInheritedFromThisTag{}), + fn_(std::forward(fn)) {} private: friend detail::AccessBridgeInternals; From c0e34358b2d04eee1594a529a3984a63b6499aee Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 10:40:20 +0100 Subject: [PATCH 11/17] Testing and docs --- .clang-format | 1 + Cargo.lock | 1 + Cargo.toml | 1 + Doxyfile | 2 +- examples/esi.cpp | 28 ++++++++++++++++- include/fastly/esi.h | 44 +++++++++++++++++++++++--- src/cpp/esi.cpp | 30 ++++++++++++++++++ src/esi.rs | 74 ++++++++++++++++++++++++++------------------ src/lib.rs | 8 +++++ test/esi.cpp | 48 ++++++++++++++++++++++++++++ test/fastly.toml | 1 + 11 files changed, 202 insertions(+), 36 deletions(-) create mode 100644 .clang-format create mode 100644 test/esi.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3148815 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BreakBeforeBraces: Attach \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f3df208..358fcbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "link-cplusplus", "log", "log-fastly", + "quick-xml", "thiserror 2.0.12", ] diff --git a/Cargo.toml b/Cargo.toml index 7b141f2..b03fb3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ log = "0.4.27" log-fastly = "0.11.5" thiserror = "2.0.12" esi = "0.6.1" +quick-xml = "0.38.3" [build-dependencies] cxx-build = "1.0" diff --git a/Doxyfile b/Doxyfile index 93c3cac..ab0da1e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -991,7 +991,7 @@ WARN_LOGFILE = # spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = README.md CONTRIBUTING.md examples target/release/dist/fastly +INPUT = README.md CONTRIBUTING.md examples include/fastly # This tag can be used to specify the character encoding of the source files # that Doxygen parses. Internally Doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/examples/esi.cpp b/examples/esi.cpp index b8a32cc..fb4adc1 100644 --- a/examples/esi.cpp +++ b/examples/esi.cpp @@ -3,11 +3,12 @@ int main() { fastly::log::init_simple("logs", fastly::log::LogLevelFilter::Debug); auto req{fastly::http::Request::from_client()}; + fastly::log::info("Making backend request..."); auto bereq = fastly::http::Request(fastly::http::Method::GET, "https://esi-cpp-demo.edgecompute.app/") .with_auto_decompress_gzip(true); auto beresp = bereq.clone_without_body().send("esi-cpp-demo").value(); - auto names = req.get_header_names(); + fastly::log::info("Got backend response"); // Pass in the request made to the backend as a template for fragment // requests: this ensures that the fragment requests also ask for gzipped @@ -28,4 +29,29 @@ int main() { if (!result) { fastly::log::error("Failed to process response"); } + + std::string_view html = R"( + + + My Shopping Website + + +
+

My Shopping Website

+
+
+ + + + +
+ + +)"; + + fastly::log::info("Processing bare document..."); + fastly::esi::Processor processor2; + auto res = processor2.process_document(std::string(html), std::nullopt, + std::nullopt); + std::cout << *res; } diff --git a/include/fastly/esi.h b/include/fastly/esi.h index 6e0c255..ac9f000 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -19,9 +19,8 @@ namespace fastly::esi { struct Configuration { public: /// Create a new configuration object. - /// \param namespc The namespace to use for ESI tags. Defaults to "esi". - /// \param is_escaped_content Whether to escape content by default. Defaults - /// to true. + /// \param namespc The namespace to use for ESI tags. + /// \param is_escaped_content Whether to escape content by default. Configuration(std::string namespc = "esi", bool is_escaped_content = true) : namespace_(std::move(namespc)), is_escaped_content_(is_escaped_content) {} @@ -34,14 +33,20 @@ struct Configuration { bool is_escaped_content_; }; +/// Content that can be returned from a fragment request dispatcher. This can +/// either be a pending request, a response, or an empty value to indicate that +/// no content is available. using PendingFragmentContent = std::variant; +/// A callback type used to dispatch requests for ESI fragments. class DispatchFragmentRequestFn : public detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag { public: + /// The type of the dispatch function. using function_type = std::function(Request)>; + template F> DispatchFragmentRequestFn(F &&fn) : detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag( @@ -54,11 +59,14 @@ class DispatchFragmentRequestFn function_type fn_; }; +/// A callback type used to process responses from ESI fragment requests. class ProcessFragmentResponseFn : public detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag { public: + /// The type of the processing function. using function_type = std::function(Request &, Response)>; + template F> ProcessFragmentResponseFn(F &&fn) : detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag( @@ -71,13 +79,26 @@ class ProcessFragmentResponseFn function_type fn_; }; +/// An ESI processor that can process a response containing ESI tags, dispatch +/// requests for fragments, and process the fragment responses. class Processor { public: /// Create a new ESI processor with the given configuration. Processor(std::optional original_request_metadata = std::nullopt, Configuration config = Configuration()); - tl::expected process_response( + /// Process a response containing ESI tags, optionally using the given + /// callbacks to dispatch requests for fragments and process the fragment + /// responses. + /// + /// \param src_document The response containing ESI tags to process. + /// \param client_response_metadata Optional original client request data used + /// for fragment requests. + /// \param dispatch_fragment_request Optional callback to dispatch requests + /// for fragments. + /// \param process_fragment_response Optional callback to process fragment + /// responses. + fastly::expected process_response( Response &src_document, std::optional client_response_metadata = std::nullopt, std::optional dispatch_fragment_request = @@ -85,6 +106,21 @@ class Processor { std::optional process_fragment_response = std::nullopt); + /// Process a string containing ESI tags, optionally using the given + /// callbacks to dispatch requests for fragments and process the fragment + /// responses. + /// \param src_document The string containing ESI tags to process. + /// \param dispatch_fragment_request Optional callback to dispatch requests + /// for fragments. + /// \param process_fragment_response Optional callback to process fragment + /// responses. + fastly::expected process_document( + const std::string &src_document, + std::optional dispatch_fragment_request = + std::nullopt, + std::optional process_fragment_response = + std::nullopt); + private: rust::Box processor_; }; diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp index ae144ac..99a330d 100644 --- a/src/cpp/esi.cpp +++ b/src/cpp/esi.cpp @@ -106,4 +106,34 @@ tl::expected Processor::process_response( return tl::unexpected(error::FastlyError(err)); } } + +tl::expected Processor::process_document( + const std::string &src_document, + std::optional dispatch_fragment_request, + std::optional process_fragment_response) { + fastly::sys::error::FastlyError *err; + // We convert the callbacks to their tag types here, or pass null if not + // present. They will be converted back to their real types when the C++ + // callback bindings are invoked from Rust. + detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag + *dispatch_fragment_tag = + dispatch_fragment_request.has_value() ? &*dispatch_fragment_request + : nullptr; + detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag + *process_fragment_tag = + process_fragment_response.has_value() ? &*process_fragment_response + : nullptr; + + fastly::log::info("Processing ESI document: {}", src_document); + + std::string out; + bool success = fastly::sys::esi::m_esi_processor_process_document( + std::move(processor_), src_document, dispatch_fragment_tag, + process_fragment_tag, out, err); + if (success) { + return out; + } else { + return tl::unexpected(error::FastlyError(err)); + } +} } // namespace fastly::esi \ No newline at end of file diff --git a/src/esi.rs b/src/esi.rs index f1919f3..3f1246b 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -1,8 +1,4 @@ -use std::{ - pin::{self, Pin}, - ptr, - str::ParseBoolError, -}; +use std::{pin::Pin, ptr}; use cxx::CxxString; use esi::Configuration; @@ -101,21 +97,49 @@ pub fn m_esi_processor_process_response( // Make sure to take ownership, as this pointer is modelling an Optional> Some(unsafe { Box::from_raw(client_response_metadata) }) }; - match processor.0.process_response( - &mut src_document.0, - client_response_metadata.map(|r| r.0), - shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), - shim_process_fragment_response_fn(process_fragment_response).as_deref(), - ) { - Ok(_) => { - err.set(ptr::null_mut()); - true - } - Err(e) => { - err.set(Box::into_raw(Box::new(FastlyError::ESIError(e)))); - false - } - } + try_fe!( + err, + processor + .0 + .process_response( + &mut src_document.0, + client_response_metadata.map(|r| r.0), + shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), + shim_process_fragment_response_fn(process_fragment_response).as_deref(), + ) + .map_err(|e| FastlyError::ESIError(e)) + ); + true +} + +pub fn m_esi_processor_process_document( + processor: Box, + src_document: &CxxString, + dispatch_fragment_request: *const DispatchFragmentRequestFnTag, + process_fragment_response: *const ProcessFragmentResponseFnTag, + out: Pin<&mut CxxString>, + mut err: Pin<&mut *mut FastlyError>, +) -> bool { + let doc_str = try_fe!( + err, + src_document.to_str().map_err(|e| FastlyError::Utf8Error(e)) + ); + println!("Processing ESI document: {}", doc_str); + let reader = quick_xml::reader::Reader::from_str(doc_str); + let mut writer = quick_xml::Writer::new(out); + try_fe!( + err, + processor + .0 + .process_document( + reader, + &mut writer, + shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), + shim_process_fragment_response_fn(process_fragment_response).as_deref(), + ) + .map_err(|e| FastlyError::ESIError(e)) + ); + true } pub fn m_static_esi_processor_new( @@ -123,22 +147,12 @@ pub fn m_static_esi_processor_new( namespace: &CxxString, is_escaped_content: bool, ) -> Box { - println!( - "Original request metadata ptr: {:p}", - original_request_metadata - ); let original_request_metadata = if original_request_metadata.is_null() { None } else { // Make sure to take ownership, as this pointer is modelling an Optional> Some(unsafe { Box::from_raw(original_request_metadata) }) }; - println!( - "Original request metadata: {:?}", - original_request_metadata - .as_ref() - .map_or("None".into(), |r| r.0.get_url().to_string()) - ); Box::new(Processor(esi::Processor::new( original_request_metadata.map(|r| r.0), Configuration::default() diff --git a/src/lib.rs b/src/lib.rs index 6d4466b..734065c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1045,6 +1045,14 @@ mod ffi { process_fragment_response: *const ProcessFragmentResponseFnTag, mut err: Pin<&mut *mut FastlyError>, ) -> bool; + pub unsafe fn m_esi_processor_process_document( + processor: Box, + src_document: &CxxString, + dispatch_fragment_request: *const DispatchFragmentRequestFnTag, + process_fragment_response: *const ProcessFragmentResponseFnTag, + out: Pin<&mut CxxString>, + mut err: Pin<&mut *mut FastlyError>, + ) -> bool; pub unsafe fn m_static_esi_processor_new( // SAFETY: this parameter models an Option>, but CXX does not // support this type directly. Care must be taken to take ownership of the pointer diff --git a/test/esi.cpp b/test/esi.cpp new file mode 100644 index 0000000..066096a --- /dev/null +++ b/test/esi.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include +#include +#include + +using namespace fastly::esi; +using fastly::http::Request; +using fastly::http::Response; + +TEST_CASE("Configuration default and custom values") { + Configuration def; + REQUIRE(def.get_namespace() == "esi"); + REQUIRE(def.is_escaped_content()); + + Configuration custom("foo", false); + REQUIRE(custom.get_namespace() == "foo"); + REQUIRE_FALSE(custom.is_escaped_content()); +} + +std::string_view html = R"( + + + My Shopping Website + + +
+

My Shopping Website

+
+
+ + + + +
+ + +)"; + +TEST_CASE("Bare processor works") { + fastly::esi::Processor processor; + auto result = + processor.process_document(std::string(html), std::nullopt, std::nullopt); + REQUIRE(result.has_value()); + std::cout << *result; +} \ No newline at end of file diff --git a/test/fastly.toml b/test/fastly.toml index e088133..3049518 100644 --- a/test/fastly.toml +++ b/test/fastly.toml @@ -11,6 +11,7 @@ service_id = "" [local_server] backends.fastly.url = "https://www.fastly.com" backends.wikipedia.url = "https://en.wikipedia.org" +backends.esi-cpp-demo.url = "https://esi-cpp-demo.edgecompute.app" [local_server.kv_stores] test-store = { file = 'kv_store.json', format = 'json' } From 293a600510c3b837f12a00ee41a6b59eb12326ba Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 12:18:13 +0100 Subject: [PATCH 12/17] Working tests --- examples/esi.cpp | 28 +--------- src/cpp/esi.cpp | 2 - src/cpp/log.cpp | 2 +- src/esi.rs | 1 - test/esi.cpp | 140 +++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 137 insertions(+), 36 deletions(-) diff --git a/examples/esi.cpp b/examples/esi.cpp index fb4adc1..f8ee3ef 100644 --- a/examples/esi.cpp +++ b/examples/esi.cpp @@ -3,12 +3,11 @@ int main() { fastly::log::init_simple("logs", fastly::log::LogLevelFilter::Debug); auto req{fastly::http::Request::from_client()}; - fastly::log::info("Making backend request..."); + auto bereq = fastly::http::Request(fastly::http::Method::GET, "https://esi-cpp-demo.edgecompute.app/") .with_auto_decompress_gzip(true); auto beresp = bereq.clone_without_body().send("esi-cpp-demo").value(); - fastly::log::info("Got backend response"); // Pass in the request made to the backend as a template for fragment // requests: this ensures that the fragment requests also ask for gzipped @@ -29,29 +28,4 @@ int main() { if (!result) { fastly::log::error("Failed to process response"); } - - std::string_view html = R"( - - - My Shopping Website - - -
-

My Shopping Website

-
-
- - - - -
- - -)"; - - fastly::log::info("Processing bare document..."); - fastly::esi::Processor processor2; - auto res = processor2.process_document(std::string(html), std::nullopt, - std::nullopt); - std::cout << *res; } diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp index 99a330d..6dc4d3f 100644 --- a/src/cpp/esi.cpp +++ b/src/cpp/esi.cpp @@ -124,8 +124,6 @@ tl::expected Processor::process_document( process_fragment_response.has_value() ? &*process_fragment_response : nullptr; - fastly::log::info("Processing ESI document: {}", src_document); - std::string out; bool success = fastly::sys::esi::m_esi_processor_process_document( std::move(processor_), src_document, dispatch_fragment_tag, diff --git a/src/cpp/log.cpp b/src/cpp/log.cpp index 2e62853..4032e52 100644 --- a/src/cpp/log.cpp +++ b/src/cpp/log.cpp @@ -21,7 +21,7 @@ fastly::expected Endpoint::from_name(std::string_view name) { fastly::sys::error::FastlyError *err; fastly::sys::log::m_static_log_endpoint_try_from_name( static_cast(name), out, err); - if (err != nullptr) { + if (err == nullptr) { return FSLY_BOX(log, Endpoint, out); } else { return fastly::unexpected(err); diff --git a/src/esi.rs b/src/esi.rs index 3f1246b..acc9a37 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -124,7 +124,6 @@ pub fn m_esi_processor_process_document( err, src_document.to_str().map_err(|e| FastlyError::Utf8Error(e)) ); - println!("Processing ESI document: {}", doc_str); let reader = quick_xml::reader::Reader::from_str(doc_str); let mut writer = quick_xml::Writer::new(out); try_fe!( diff --git a/test/esi.cpp b/test/esi.cpp index 066096a..cc9771a 100644 --- a/test/esi.cpp +++ b/test/esi.cpp @@ -39,10 +39,140 @@ std::string_view html = R"( )"; -TEST_CASE("Bare processor works") { +TEST_CASE("Dispatch fragment callback works") { fastly::esi::Processor processor; - auto result = - processor.process_document(std::string(html), std::nullopt, std::nullopt); + + auto dispatch_fragment_request = [](fastly::http::Request req) + -> std::optional { + auto pending = req.send_async("esi-cpp-demo"); + if (pending) { + return fastly::esi::PendingFragmentContent{std::move(*pending)}; + } else { + return std::nullopt; + } + }; + + auto result = processor.process_document( + std::string(html), dispatch_fragment_request, std::nullopt); REQUIRE(result.has_value()); - std::cout << *result; -} \ No newline at end of file + + std::string_view expected = R"( + + + My Shopping Website + + +
+

My Shopping Website

+
+
+ + +
+
+ + + +
+

This is the page content.

+
+
+
+ + + +
+

This is the page content.

+
+ +
+ + +)"; + REQUIRE(*result == expected); +} + +TEST_CASE("Process response callback works") { + fastly::esi::Processor processor; + + auto dispatch_fragment_request = [](fastly::http::Request req) + -> std::optional { + auto pending = req.send_async("esi-cpp-demo"); + if (pending) { + return fastly::esi::PendingFragmentContent{std::move(*pending)}; + } else { + return std::nullopt; + } + }; + auto process_response = + [](fastly::http::Request &, + fastly::http::Response) -> std::optional { + return fastly::http::Response::from_body( + fastly::http::Body("")); + }; + + auto result = processor.process_document( + std::string(html), dispatch_fragment_request, process_response); + REQUIRE(result.has_value()); + + std::string_view expected = R"( + + + My Shopping Website + + +
+

My Shopping Website

+
+
+ + + + +
+ + +)"; + REQUIRE(*result == expected); +} + +TEST_CASE("Return error from dispatch fragment callback fails processing") { + fastly::esi::Processor processor; + + auto dispatch_fragment_request = [](fastly::http::Request) + -> std::optional { + return std::nullopt; + }; + + auto result = processor.process_document( + std::string(html), dispatch_fragment_request, std::nullopt); + REQUIRE_FALSE(result.has_value()); +} + +TEST_CASE("Return error from process response callback fails processing") { + fastly::esi::Processor processor; + + auto dispatch_fragment_request = [](fastly::http::Request req) + -> std::optional { + auto pending = req.send_async("esi-cpp-demo"); + if (pending) { + return fastly::esi::PendingFragmentContent{std::move(*pending)}; + } else { + return std::nullopt; + } + }; + auto process_response = [](fastly::http::Request &, fastly::http::Response) + -> std::optional { return std::nullopt; }; + + auto result = processor.process_document( + std::string(html), dispatch_fragment_request, process_response); + REQUIRE_FALSE(result.has_value()); +} + +// Required due to https://github.com/WebAssembly/wasi-libc/issues/485 +#include +int main(int argc, char *argv[]) { return Catch::Session().run(argc, argv); } \ No newline at end of file From 594b2793a726373d9907f35d8c21dee96217b1cc Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 12:23:52 +0100 Subject: [PATCH 13/17] Clippy --- src/esi.rs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/esi.rs b/src/esi.rs index acc9a37..cdf5400 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -12,10 +12,11 @@ use crate::{ pub(crate) struct Processor(esi::Processor); +type DispatchFragmentRequestFnType = + dyn Fn(fastly::Request) -> Result; fn shim_dispatch_fragment_request_fn( func: *const DispatchFragmentRequestFnTag, -) -> Option Result>> -{ +) -> Option> { if func.is_null() { return None; } @@ -49,16 +50,11 @@ fn shim_dispatch_fragment_request_fn( Some(shim) } +type ProcessFragmentResponseFnType = + dyn Fn(&mut fastly::Request, fastly::Response) -> Result; fn shim_process_fragment_response_fn( func: *const ProcessFragmentResponseFnTag, -) -> Option< - Box< - dyn Fn( - &mut fastly::Request, - fastly::Response, - ) -> Result, - >, -> { +) -> Option> { if func.is_null() { return None; } @@ -107,7 +103,7 @@ pub fn m_esi_processor_process_response( shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), shim_process_fragment_response_fn(process_fragment_response).as_deref(), ) - .map_err(|e| FastlyError::ESIError(e)) + .map_err(FastlyError::ESIError) ); true } @@ -120,10 +116,7 @@ pub fn m_esi_processor_process_document( out: Pin<&mut CxxString>, mut err: Pin<&mut *mut FastlyError>, ) -> bool { - let doc_str = try_fe!( - err, - src_document.to_str().map_err(|e| FastlyError::Utf8Error(e)) - ); + let doc_str = try_fe!(err, src_document.to_str().map_err(FastlyError::Utf8Error)); let reader = quick_xml::reader::Reader::from_str(doc_str); let mut writer = quick_xml::Writer::new(out); try_fe!( @@ -136,7 +129,7 @@ pub fn m_esi_processor_process_document( shim_dispatch_fragment_request_fn(dispatch_fragment_request).as_deref(), shim_process_fragment_response_fn(process_fragment_response).as_deref(), ) - .map_err(|e| FastlyError::ESIError(e)) + .map_err(FastlyError::ESIError) ); true } From 9ecaf36507948be4ef3d00861eecaae498d4709a Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 14:17:55 +0100 Subject: [PATCH 14/17] Fix tests --- include/fastly/detail/rust_bridge_tags.h | 11 ++++++----- include/fastly/esi.h | 10 ++-------- test/esi.cpp | 3 ++- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/include/fastly/detail/rust_bridge_tags.h b/include/fastly/detail/rust_bridge_tags.h index f96a66f..98f92a1 100644 --- a/include/fastly/detail/rust_bridge_tags.h +++ b/include/fastly/detail/rust_bridge_tags.h @@ -11,17 +11,18 @@ #define FASTLY_DETAIL_RUST_BRIDGE_TAGS_H namespace fastly::detail::rust_bridge_tags { -// Used to make it harder to accidentally create instances of the tag types. -struct IConfirmIHaveInheritedFromThisTag {}; - namespace esi { // esi.h:DispatchFragmentRequestFn struct DispatchFragmentRequestFnTag { - DispatchFragmentRequestFnTag(IConfirmIHaveInheritedFromThisTag) {} + // Ensure that only classes that inherit from this tag can be used as + // DispatchFragmentRequestFn. +protected: + DispatchFragmentRequestFnTag() = default; }; // esi.h:ProcessFragmentResponseFn struct ProcessFragmentResponseFnTag { - ProcessFragmentResponseFnTag(IConfirmIHaveInheritedFromThisTag) {} +protected: + ProcessFragmentResponseFnTag() = default; }; } // namespace esi } // namespace fastly::detail::rust_bridge_tags diff --git a/include/fastly/esi.h b/include/fastly/esi.h index ac9f000..2670b32 100644 --- a/include/fastly/esi.h +++ b/include/fastly/esi.h @@ -48,10 +48,7 @@ class DispatchFragmentRequestFn std::function(Request)>; template F> - DispatchFragmentRequestFn(F &&fn) - : detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag( - detail::rust_bridge_tags::IConfirmIHaveInheritedFromThisTag{}), - fn_(std::forward(fn)) {} + DispatchFragmentRequestFn(F &&fn) : fn_(std::forward(fn)) {} private: friend detail::AccessBridgeInternals; @@ -68,10 +65,7 @@ class ProcessFragmentResponseFn std::function(Request &, Response)>; template F> - ProcessFragmentResponseFn(F &&fn) - : detail::rust_bridge_tags::esi::ProcessFragmentResponseFnTag( - detail::rust_bridge_tags::IConfirmIHaveInheritedFromThisTag{}), - fn_(std::forward(fn)) {} + ProcessFragmentResponseFn(F &&fn) : fn_(std::forward(fn)) {} private: friend detail::AccessBridgeInternals; diff --git a/test/esi.cpp b/test/esi.cpp index cc9771a..e714351 100644 --- a/test/esi.cpp +++ b/test/esi.cpp @@ -31,7 +31,7 @@ std::string_view html = R"(
- +
@@ -39,6 +39,7 @@ std::string_view html = R"( )"; +#include TEST_CASE("Dispatch fragment callback works") { fastly::esi::Processor processor; From 74f7ebab5f1a2a2b257fc912d2e2ad9fde143cf3 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 15:34:29 +0100 Subject: [PATCH 15/17] Bump version --- CMakeLists.txt | 2 +- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3457aa4..02ec871 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # From https://github.com/XiangpengHao/cxx-cmake-example/blob/master/fastly/CMakeLists.txt cmake_minimum_required(VERSION 3.29) -project(compute-sdk-cpp VERSION 0.3.0 LANGUAGES CXX) +project(compute-sdk-cpp VERSION 0.4.0 LANGUAGES CXX) if(VERBOSE) set(VERBOSE_FLAG "--verbose") diff --git a/Cargo.lock b/Cargo.lock index 358fcbd..4ea9643 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.3.0" +version = "0.4.0" dependencies = [ "cxx", "cxx-build", diff --git a/Cargo.toml b/Cargo.toml index b03fb3f..e260689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fastly-sys" -version = "0.3.0" +version = "0.4.0" authors = ["Kat Marchán "] edition = "2024" From 099b3ee5d521c7dd4092340e57b0feaad54d9275 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Mon, 15 Sep 2025 18:06:11 +0100 Subject: [PATCH 16/17] Pin arguments to manual C callback bindings --- src/esi.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/esi.rs b/src/esi.rs index cdf5400..8202093 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -1,4 +1,7 @@ -use std::{pin::Pin, ptr}; +use std::{ + pin::{Pin, pin}, + ptr, +}; use cxx::CxxString; use esi::Configuration; @@ -21,8 +24,8 @@ fn shim_dispatch_fragment_request_fn( return None; } let shim = Box::new(move |req| { - let mut out_pending = ptr::null_mut(); - let mut out_completed = ptr::null_mut(); + let mut out_pending = pin!(ptr::null_mut()); + let mut out_completed = pin!(ptr::null_mut()); let result = unsafe { crate::manual_ffi::fastly_esi_manualbridge_DispatchFragmentRequestFn_call( func, @@ -59,7 +62,7 @@ fn shim_process_fragment_response_fn( return None; } let shim = Box::new(move |req: &mut fastly::Request, resp| { - let mut out_resp = ptr::null_mut(); + let mut out_resp = pin!(ptr::null_mut()); let result = unsafe { crate::manual_ffi::fastly_esi_manualbridge_ProcessFragmentResponseFn_call( func, From d63810cfd3852c9e351bf357bd41e7f5c12c97d2 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 16 Sep 2025 08:34:23 +0100 Subject: [PATCH 17/17] Use enum for dispatch fragment callback --- src/cpp/esi.cpp | 11 ++++++----- src/esi.rs | 16 +++++++++++----- src/lib.rs | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/cpp/esi.cpp b/src/cpp/esi.cpp index 6dc4d3f..cfcead6 100644 --- a/src/cpp/esi.cpp +++ b/src/cpp/esi.cpp @@ -7,7 +7,8 @@ namespace fastly::esi { // These functions are called by Rust to invoke the C++ callbacks. -extern "C" uint32_t fastly$esi$manualbridge$DispatchFragmentRequestFn$call( +extern "C" fastly::sys::esi::DispatchFragmentRequestFnResult +fastly$esi$manualbridge$DispatchFragmentRequestFn$call( const detail::rust_bridge_tags::esi::DispatchFragmentRequestFnTag &fn_tag, fastly::sys::http::Request *raw_req, fastly::sys::http::request::PendingRequest *&out_pending, @@ -21,20 +22,20 @@ extern "C" uint32_t fastly$esi$manualbridge$DispatchFragmentRequestFn$call( auto res = detail::AccessBridgeInternals::get(fn)(std::move(req)); if (!res) { - return 0; // Error + return fastly::sys::esi::DispatchFragmentRequestFnResult::Error; } if (std::holds_alternative(*res)) { out_pending = detail::AccessBridgeInternals::get( std::get(*res)) .into_raw(); - return 1; // Pending response + return fastly::sys::esi::DispatchFragmentRequestFnResult::PendingRequest; } else if (std::holds_alternative(*res)) { out_complete = detail::AccessBridgeInternals::get(std::get(*res)) .into_raw(); - return 2; // Complete Response + return fastly::sys::esi::DispatchFragmentRequestFnResult::CompletedRequest; } else { - return 3; // No content + return fastly::sys::esi::DispatchFragmentRequestFnResult::NoContent; } } diff --git a/src/esi.rs b/src/esi.rs index 8202093..a55d154 100644 --- a/src/esi.rs +++ b/src/esi.rs @@ -8,7 +8,9 @@ use esi::Configuration; use crate::{ error::FastlyError, - ffi::{DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag}, + ffi::{ + DispatchFragmentRequestFnResult, DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag, + }, http::{request::Request, response::Response}, try_fe, }; @@ -35,16 +37,20 @@ fn shim_dispatch_fragment_request_fn( ) }; match result { - 1 => Ok(unsafe { esi::PendingFragmentContent::PendingRequest(out_pending.read().0) }), - 2 => { + DispatchFragmentRequestFnResult::PendingRequest => { + Ok(unsafe { esi::PendingFragmentContent::PendingRequest(out_pending.read().0) }) + } + DispatchFragmentRequestFnResult::CompletedRequest => { Ok( unsafe { esi::PendingFragmentContent::CompletedRequest(out_completed.read().0) }, ) } - 3 => Ok(esi::PendingFragmentContent::NoContent), - 0 => Err(esi::ExecutionError::FunctionError( + DispatchFragmentRequestFnResult::NoContent => { + Ok(esi::PendingFragmentContent::NoContent) + } + DispatchFragmentRequestFnResult::Error => Err(esi::ExecutionError::FunctionError( "dispatch_fragment_request".into(), )), _ => unreachable!(), diff --git a/src/lib.rs b/src/lib.rs index 734065c..0fbd5e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,6 +223,15 @@ mod ffi { Trace = 5, } + #[namespace = "fastly::sys::esi"] + #[repr(usize)] + pub enum DispatchFragmentRequestFnResult { + Error = 0, + PendingRequest = 1, + CompletedRequest = 2, + NoContent = 3, + } + #[namespace = "fastly::sys::error"] extern "Rust" { type FastlyError; @@ -1067,7 +1076,9 @@ mod ffi { // Some types (notably callback functions) are not supported by CXX at all, so we // define manual FFI bindings for them here. mod manual_ffi { - use crate::ffi::{DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag}; + use crate::ffi::{ + DispatchFragmentRequestFnResult, DispatchFragmentRequestFnTag, ProcessFragmentResponseFnTag, + }; // We never rely on the layout of Rust types passed to these functions, // so we can ignore the improper_ctypes warning. @@ -1080,7 +1091,7 @@ mod manual_ffi { req: *mut crate::Request, out_pending: &mut *mut crate::PendingRequest, out_complete: &mut *mut crate::Response, - ) -> u32; + ) -> DispatchFragmentRequestFnResult; #[link_name = "fastly$esi$manualbridge$ProcessFragmentResponseFn$call"] pub(crate) fn fastly_esi_manualbridge_ProcessFragmentResponseFn_call(