wasm_framework/lib.rs
1#![deny(missing_docs)]
2//! Base support for wasm service using Confluence Workers
3//!
4use async_trait::async_trait;
5use js_sys::{Function, Reflect};
6use service_logging::{log, LogEntry, LogQueue, Logger, Severity};
7use std::cell::RefCell;
8use std::fmt;
9use wasm_bindgen::JsValue;
10
11mod error;
12pub use error::Error;
13mod method;
14pub use method::Method;
15mod request;
16pub use request::Request;
17mod response;
18pub use response::{Body, Response};
19mod media_type;
20pub use media_type::media_type;
21
22/// re-export url::Url
23pub use url::Url;
24
25mod context;
26pub use context::Context;
27mod httpdate;
28pub(crate) mod js_values;
29pub use httpdate::HttpDate;
30
31/// Logging support for deferred tasks
32#[derive(Debug)]
33pub struct RunContext {
34 /// queue of deferred messages
35 pub log_queue: RefCell<LogQueue>,
36}
37
38// workers are single-threaded
39unsafe impl Sync for RunContext {}
40
41impl RunContext {
42 /// log message (used by log! macro)
43 pub fn log(&self, entry: LogEntry) {
44 self.log_queue.borrow_mut().log(entry);
45 /*
46 let mut guard = match self.log_queue.lock() {
47 Ok(guard) => guard,
48 Err(_poisoned) => {
49 // lock shouldn't be poisoned because we don't have panics in production wasm,
50 // so this case shouldn't occur
51 return;
52 }
53 };
54 guard.log(entry);
55 */
56 }
57}
58
59/// Runnable trait for deferred tasks
60/// Deferred tasks are often useful for logging and analytics.
61/// ```rust
62/// use std::{rc::Rc,sync::Mutex};;
63/// use async_trait::async_trait;
64/// use service_logging::{log,Logger,LogQueue,Severity};
65/// use wasm_service::{Runnable,RunContext};
66///
67/// struct Data { s: String }
68/// #[async_trait]
69/// impl Runnable for Data {
70/// async fn run(&self, ctx: &RunContext) {
71/// log!(ctx, Severity::Info, msg: format!("Deferred with data: {}", self.s ));
72/// }
73/// }
74/// ```
75#[async_trait]
76pub trait Runnable {
77 /// Execute a deferred task. The task may append
78 /// logs to `lq` using the [`log`] macro. Logs generated
79 /// are sent to the log service after all deferred tasks have run.
80 ///
81 /// Note that if there is a failure sending logs to the logging service,
82 /// those log messages (and the error from the send failure) will be unreported.
83 async fn run(&self, ctx: &RunContext);
84}
85
86/// Generic page error return - doesn't require ctx
87#[derive(Clone, Debug)]
88pub struct HandlerReturn {
89 /// status code (default: 200)
90 pub status: u16,
91 /// body text
92 pub text: String,
93}
94
95impl fmt::Display for HandlerReturn {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "({},{})", self.status, self.text)
98 }
99}
100
101/// Generate handler return "error"
102pub fn handler_return(status: u16, text: &str) -> HandlerReturn {
103 HandlerReturn {
104 status,
105 text: text.to_string(),
106 }
107}
108
109impl Default for HandlerReturn {
110 fn default() -> Self {
111 Self {
112 status: 200,
113 text: String::default(),
114 }
115 }
116}
117
118/// Trait that defines app/service's request handler and router
119/// See [rustwasm-service-template](https://github.com/stevelr/rustwasm-service-template/blob/master/src/lib.rs)
120/// for a more complete example
121///
122///```rust
123/// use service_logging::{Severity::Verbose,log,Logger};
124/// use wasm_service::{Context,Handler,HandlerReturn,Request};
125/// use async_trait::async_trait;
126/// struct MyHandler {}
127/// #[async_trait(?Send)]
128/// impl Handler for MyHandler {
129/// /// Process incoming Request
130/// async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn> {
131/// // log all incoming requests
132/// log!(ctx, Verbose, method: req.method(), url: req.url());
133/// match (req.method(), req.url().path()) {
134/// (GET, "/hello") => {
135/// ctx.response().content_type("text/plain; charset=UTF-8").unwrap()
136/// .text("Hello world!");
137/// }
138/// _ => {
139/// ctx.response().status(404).text("Not Found");
140/// }
141/// }
142/// Ok(())
143/// }
144/// }
145///```
146#[async_trait(?Send)]
147pub trait Handler {
148 /// Implementation of application request handler
149 async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn>;
150}
151
152/// Configuration parameters for service
153/// Parameter E is your crate's error type
154pub struct ServiceConfig {
155 /// Logger
156 pub logger: Box<dyn Logger>,
157
158 /// Request handler
159 pub handlers: Vec<Box<dyn Handler>>,
160
161 /// how to handle internal errors. This function should modify ctx.response()
162 /// with results, which, for example, could include rendering a page or sending
163 /// a redirect. The default implementation returns status 200 with a short text message.
164 pub internal_error_handler: fn(req: &Request, ctx: &mut Context),
165
166 /// how to handle Not Found (404) responses. This function should modify ctx.response()
167 /// with results, which, for example, could include rendering a page or sending
168 /// a redirect. The default implementation returns status 404 with a short text message.
169 pub not_found_handler: fn(req: &Request, ctx: &mut Context),
170}
171
172impl Default for ServiceConfig {
173 /// Default construction of ServiceConfig does no logging and handles no requests.
174 fn default() -> ServiceConfig {
175 ServiceConfig {
176 logger: service_logging::silent_logger(),
177 handlers: Vec::new(),
178 internal_error_handler: default_internal_error_handler,
179 not_found_handler: default_not_found_handler,
180 }
181 }
182}
183
184struct DeferredData {
185 tasks: Vec<Box<dyn Runnable + std::panic::UnwindSafe>>,
186 logs: Vec<LogEntry>,
187 logger: Box<dyn Logger>,
188}
189
190/// Entrypoint for wasm-service. Converts parameters from javascript into [Request],
191/// invokes app-specific [Handler](trait.Handler.html), and converts [`Response`] to javascript.
192/// Also sends logs to [Logger](https://docs.rs/service-logging/0.3/service_logging/trait.Logger.html) and runs deferred tasks.
193pub async fn service_request(req: JsValue, config: ServiceConfig) -> Result<JsValue, JsValue> {
194 let mut is_err = false;
195 let map = js_sys::Map::from(req);
196 let req = Request::from_js(&map)?;
197 let mut ctx = Context::default();
198 let mut handler_result = Ok(());
199 for handler in config.handlers.iter() {
200 handler_result = handler.handle(&req, &mut ctx).await;
201 if ctx.is_internal_error().is_some() {
202 (config.internal_error_handler)(&req, &mut ctx);
203 is_err = true;
204 break;
205 }
206 // if handler set response, or returned HandlerReturn (which is a response), stop iter
207 if handler_result.is_err() || !ctx.response().is_unset() {
208 break;
209 }
210 }
211 if let Err(result) = handler_result {
212 // Convert HandlerReturn to status/body
213 ctx.response().status(result.status).text(result.text);
214 } else if ctx.response().is_unset() {
215 // If NO handler set a response, it's content not found
216 // the not-found handler might return a static page or redirect
217 (config.not_found_handler)(&req, &mut ctx);
218 }
219 let response = ctx.take_response();
220 if response.get_status() < 200 || response.get_status() > 307 {
221 is_err = true;
222 }
223 let severity = if response.get_status() == 404 {
224 Severity::Warning
225 } else if is_err {
226 Severity::Error
227 } else {
228 Severity::Info
229 };
230 log!(ctx, severity, _:"service", method: req.method(), url: req.url(), status: response.get_status());
231 if is_err {
232 // if any error occurred, send logs now; fast path (on success) defers logging
233 // also, if there was an error, don't execute deferred tasks
234 let _ = config
235 .logger
236 .send("http", ctx.take_logs())
237 .await
238 .map_err(|e| {
239 ctx.response()
240 .header("X-service-log-err-ret", e.to_string())
241 .unwrap()
242 });
243 } else {
244 // From incoming request, extract 'event' object, and get ref to its 'waitUntil' function
245 let js_event =
246 js_sys::Object::from(check_defined(map.get(&"event".into()), "missing event")?);
247 let wait_func = Function::from(
248 Reflect::get(&js_event, &JsValue::from_str("waitUntil"))
249 .map_err(|_| "event without waitUntil")?,
250 );
251 // this should always return OK (event has waitUntil property) unless api is broken.
252 let promise = deferred_promise(Box::new(DeferredData {
253 tasks: ctx.take_tasks(),
254 logs: ctx.take_logs(),
255 logger: config.logger,
256 }));
257 let _ = wait_func.call1(&js_event, &promise); // todo: handle result
258 }
259 Ok(response.into_js())
260}
261
262/// Default implementation of internal error handler
263/// Sets status to 200 and returns a short error message
264fn default_internal_error_handler(req: &Request, ctx: &mut Context) {
265 let error = ctx.is_internal_error();
266 log!(ctx, Severity::Error, _:"InternalError", url: req.url(),
267 error: error.map(|e| e.to_string()).unwrap_or_else(|| String::from("none")));
268 ctx.response()
269 .status(200)
270 .content_type(mime::TEXT_PLAIN_UTF_8)
271 .unwrap()
272 .text("Sorry, an internal error has occurred. It has been logged.");
273}
274
275/// Default implementation of not-found handler.
276/// Sets status to 404 and returns a short message "Not Found"
277pub fn default_not_found_handler(req: &Request, ctx: &mut Context) {
278 log!(ctx, Severity::Info, _:"NotFound", url: req.url());
279 ctx.response()
280 .status(404)
281 .content_type(mime::TEXT_PLAIN_UTF_8)
282 .unwrap()
283 .text("Not Found");
284}
285
286/// Future task that will run deferred. Includes deferred logs plus user-defined tasks.
287/// This function contains a rust async wrapped in a Javascript Promise that will be passed
288/// to the event.waitUntil function, so it gets processed after response is returned.
289fn deferred_promise(args: Box<DeferredData>) -> js_sys::Promise {
290 wasm_bindgen_futures::future_to_promise(async move {
291 // send first set of logs
292 if let Err(e) = args.logger.send("http", args.logs).await {
293 log_log_error(e);
294 }
295 // run each deferred task
296 // let log_queue = Mutex::new(LogQueue::default());
297 let log_queue = RefCell::new(LogQueue::default());
298 let run_ctx = RunContext { log_queue };
299 for t in args.tasks.iter() {
300 t.run(&run_ctx).await;
301 }
302
303 // if any logs were generated during processing of deferred tasks, send those
304 let logs = run_ctx.log_queue.borrow_mut().take();
305 if let Err(e) = args.logger.send("http", logs).await {
306 log_log_error(e);
307 }
308 // all done, return nothing
309 Ok(JsValue::undefined())
310 })
311}
312
313/// Returns javascript value, or Err if undefined
314fn check_defined(v: JsValue, msg: &str) -> Result<JsValue, JsValue> {
315 if v.is_undefined() {
316 return Err(JsValue::from_str(msg));
317 }
318 Ok(v)
319}
320
321/// logging fallback: if we can't send to external logger,
322/// log to "console" so it can be seen in worker logs
323fn log_log_error(e: Box<dyn std::error::Error>) {
324 web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
325 "Error sending logs: {:?}",
326 e
327 )))
328}