Skip to main content

worker/
context.rs

1use std::future::Future;
2use std::panic::AssertUnwindSafe;
3
4use crate::worker_sys::Context as JsContext;
5use crate::Result;
6
7use serde::de::DeserializeOwned;
8use wasm_bindgen::JsValue;
9use wasm_bindgen_futures::future_to_promise;
10
11/// A context bound to a `fetch` event.
12#[derive(Debug)]
13pub struct Context {
14    inner: JsContext,
15}
16
17unsafe impl Send for Context {}
18unsafe impl Sync for Context {}
19
20impl Context {
21    /// Constructs a context from an underlying JavaScript context object.
22    pub fn new(inner: JsContext) -> Self {
23        Self { inner }
24    }
25
26    /// Extends the lifetime of the "fetch" event which this context is bound to,
27    /// until the given future has been completed. The future is executed before the handler
28    /// terminates but does not block the response. For example, this is ideal for caching
29    /// responses or handling logging.
30    /// ```no_run
31    /// context.wait_until(async move {
32    ///     let _ = cache.put(request, response).await;
33    /// });
34    /// ```
35    pub fn wait_until<F>(&self, future: F)
36    where
37        F: Future<Output = ()> + 'static,
38    {
39        self.inner
40            .wait_until(&future_to_promise(AssertUnwindSafe(async {
41                future.await;
42                Ok(JsValue::UNDEFINED)
43            })))
44            .unwrap()
45    }
46
47    /// Prevents a runtime error response when the Worker script throws an unhandled exception.
48    /// Instead, the script will "fail open", which will proxy the request to the origin server
49    /// as though the Worker was never invoked.
50    pub fn pass_through_on_exception(&self) {
51        self.inner.pass_through_on_exception().unwrap()
52    }
53
54    /// Get the props passed to this worker execution context.
55    ///
56    /// Props provide a way to pass additional configuration to a worker based on the context
57    /// in which it was invoked. For example, when your Worker is called by another Worker via
58    /// a Service Binding, props can provide information about the calling worker.
59    ///
60    /// Props are configured in your wrangler.toml when setting up Service Bindings:
61    /// ```toml
62    /// [[services]]
63    /// binding = "MY_SERVICE"
64    /// service = "my-worker"
65    /// props = { clientId = "frontend", permissions = ["read", "write"] }
66    /// ```
67    ///
68    /// Then deserialize them to your custom type:
69    /// ```no_run
70    /// use serde::Deserialize;
71    ///
72    /// #[derive(Deserialize)]
73    /// struct MyProps {
74    ///     clientId: String,
75    ///     permissions: Vec<String>,
76    /// }
77    ///
78    /// let props = ctx.props::<MyProps>()?;
79    /// ```
80    ///
81    /// See: <https://developers.cloudflare.com/workers/runtime-apis/context/#props>
82    pub fn props<T: DeserializeOwned>(&self) -> Result<T> {
83        Ok(serde_wasm_bindgen::from_value(self.inner.props())?)
84    }
85}
86
87impl AsRef<JsContext> for Context {
88    fn as_ref(&self) -> &JsContext {
89        &self.inner
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use serde::Deserialize;
97    use wasm_bindgen::JsCast;
98    use wasm_bindgen_test::*;
99
100    #[derive(Debug, Deserialize, PartialEq)]
101    struct TestProps {
102        #[serde(rename = "clientId")]
103        client_id: String,
104        permissions: Vec<String>,
105    }
106
107    #[wasm_bindgen_test]
108    fn test_props_deserialization() {
109        // Create a mock ExecutionContext with props
110        let obj = js_sys::Object::new();
111
112        // Add props to the object
113        let props = js_sys::Object::new();
114        js_sys::Reflect::set(&props, &"clientId".into(), &"frontend-worker".into()).unwrap();
115
116        let permissions = js_sys::Array::new();
117        permissions.push(&"read".into());
118        permissions.push(&"write".into());
119        js_sys::Reflect::set(&props, &"permissions".into(), &permissions.into()).unwrap();
120
121        js_sys::Reflect::set(&obj, &"props".into(), &props.into()).unwrap();
122
123        // Create a Context from the mock object
124        let js_context: JsContext = obj.unchecked_into();
125        let context = Context::new(js_context);
126
127        // Test that props can be deserialized
128        let result: Result<TestProps> = context.props();
129        assert!(result.is_ok());
130
131        let props = result.unwrap();
132        assert_eq!(props.client_id, "frontend-worker");
133        assert_eq!(props.permissions, vec!["read", "write"]);
134    }
135
136    #[wasm_bindgen_test]
137    fn test_props_empty_object() {
138        // Create a mock ExecutionContext with empty props
139        let obj = js_sys::Object::new();
140        let props = js_sys::Object::new();
141        js_sys::Reflect::set(&obj, &"props".into(), &props.into()).unwrap();
142
143        let js_context: JsContext = obj.unchecked_into();
144        let context = Context::new(js_context);
145
146        #[derive(Debug, Deserialize, PartialEq)]
147        struct EmptyProps {}
148
149        let result: Result<EmptyProps> = context.props();
150        assert!(result.is_ok());
151    }
152}