Skip to main content

worker/
request_init.rs

1use std::collections::HashMap;
2
3use crate::headers::Headers;
4use crate::http::Method;
5use crate::MtlsCertificate;
6
7use js_sys::{self, Object};
8use serde::Serialize;
9use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
10
11/// Optional options struct that contains settings to apply to the `Request`.
12#[derive(Debug)]
13pub struct RequestInit {
14    /// Currently requires a manual conversion from your data into a [`wasm_bindgen::JsValue`].
15    pub body: Option<JsValue>,
16    /// Headers associated with the outbound `Request`.
17    pub headers: Headers,
18    /// Cloudflare-specific properties that can be set on the `Request` that control how Cloudflare’s
19    /// edge handles the request.
20    pub cf: CfProperties,
21    /// The HTTP Method used for this `Request`.
22    pub method: Method,
23    /// The redirect mode to use: follow, error, or manual. The default for a new Request object is
24    /// follow. Note, however, that the incoming Request property of a FetchEvent will have redirect
25    /// mode manual.
26    pub redirect: RequestRedirect,
27    /// The cache mode for the request. `None` means use the default behavior.
28    pub cache: Option<CacheMode>,
29}
30
31impl RequestInit {
32    pub fn new() -> Self {
33        Default::default()
34    }
35
36    pub fn with_headers(&mut self, headers: Headers) -> &mut Self {
37        self.headers = headers;
38        self
39    }
40
41    pub fn with_method(&mut self, method: Method) -> &mut Self {
42        self.method = method;
43        self
44    }
45
46    pub fn with_redirect(&mut self, redirect: RequestRedirect) -> &mut Self {
47        self.redirect = redirect;
48        self
49    }
50
51    pub fn with_cache(&mut self, cache: CacheMode) -> &mut Self {
52        self.cache = Some(cache);
53        self
54    }
55
56    pub fn with_body(&mut self, body: Option<JsValue>) -> &mut Self {
57        self.body = body;
58        self
59    }
60
61    pub fn with_cf_properties(&mut self, props: CfProperties) -> &mut Self {
62        self.cf = props;
63        self
64    }
65
66    pub fn with_mtls_certificate(&mut self, certificate: &MtlsCertificate) -> &mut Self {
67        self.cf.mtls_certificate = Some(certificate.as_request_cf_value());
68        self
69    }
70}
71
72impl From<&RequestInit> for web_sys::RequestInit {
73    fn from(req: &RequestInit) -> Self {
74        let inner = web_sys::RequestInit::new();
75
76        if !req.headers.is_empty() {
77            inner.set_headers(req.headers.as_ref());
78        }
79
80        inner.set_method(req.method.as_ref());
81        inner.set_redirect(req.redirect.into());
82        if let Some(cache) = req.cache {
83            inner.set_cache(cache.into());
84        }
85        if let Some(body) = req.body.as_ref() {
86            inner.set_body(body);
87        }
88
89        // set the Cloudflare-specific `cf` property on FFI RequestInit
90        if !req.cf.is_default() {
91            let r = ::js_sys::Reflect::set(
92                inner.as_ref(),
93                &JsValue::from("cf"),
94                &JsValue::from(&req.cf),
95            );
96            debug_assert!(
97                r.is_ok(),
98                "setting properties should never fail on our dictionary objects"
99            );
100            let _ = r;
101        }
102
103        inner
104    }
105}
106
107impl Default for RequestInit {
108    fn default() -> Self {
109        Self {
110            body: None,
111            headers: Headers::new(),
112            cf: CfProperties::default(),
113            method: Method::Get,
114            redirect: RequestRedirect::default(),
115            cache: None,
116        }
117    }
118}
119
120/// <https://developers.cloudflare.com/workers/runtime-apis/request#requestinitcfproperties>
121#[derive(Debug)]
122pub struct CfProperties {
123    /// Whether Cloudflare Apps should be enabled for this request. Defaults to `true`.
124    pub apps: Option<bool>,
125    /// This option forces Cloudflare to cache the response for this request, regardless of what
126    /// headers are seen on the response. This is equivalent to setting the page rule “Cache Level”
127    /// (to “Cache Everything”). Defaults to `false`.
128    pub cache_everything: Option<bool>,
129    /// A request’s cache key is what determines if two requests are “the same” for caching
130    /// purposes. If a request has the same cache key as some previous request, then we can serve
131    /// the same cached response for both.
132    pub cache_key: Option<String>,
133    /// This option forces Cloudflare to cache the response for this request, regardless of what
134    /// headers are seen on the response. This is equivalent to setting two page rules: “Edge Cache
135    /// TTL” and “Cache Level” (to “Cache Everything”). The value must be zero or a positive number.
136    /// A value of 0 indicates that the cache asset expires immediately.
137    pub cache_ttl: Option<i32>,
138    /// This option is a version of the cacheTtl feature which chooses a TTL based on the response’s
139    /// status code. If the response to this request has a status code that matches, Cloudflare will
140    /// cache for the instructed time, and override cache directives sent by the origin. For
141    /// example: { "200-299": 86400, 404: 1, "500-599": 0 }. The value can be any integer, including
142    /// zero and negative integers. A value of 0 indicates that the cache asset expires immediately.
143    /// Any negative value instructs Cloudflare not to cache at all.
144    pub cache_ttl_by_status: Option<HashMap<String, i32>>,
145    /// Enables Image Resizing for this request.
146    pub image: Option<ResizeConfig>,
147    /// Enables or disables AutoMinify for various file types.
148    /// For example: `{ javascript: true, css: true, html: false }`.
149    pub minify: Option<MinifyConfig>,
150    /// Whether Mirage should be enabled for this request, if otherwise configured for this zone.
151    /// Defaults to true.
152    pub mirage: Option<bool>,
153    /// Sets Polish mode. The possible values are lossy, lossless or off.
154    pub polish: Option<PolishConfig>,
155    /// Directs the request to an alternate origin server by overriding the DNS lookup. The value of
156    /// `resolve_override` specifies an alternate hostname which will be used when determining the
157    /// origin IP address, instead of using the hostname specified in the URL. The Host header of
158    /// the request will still match what is in the URL. Thus, `resolve_override` allows a request  
159    /// to be sent to a different server than the URL / Host header specifies. However,
160    /// `resolve_override` will only take effect if both the URL host and the host specified by
161    /// `resolve_override` are within your zone. If either specifies a host from a different zone /
162    /// domain, then the option will be ignored for security reasons. If you need to direct a
163    /// request to a host outside your zone (while keeping the Host header pointing within your
164    /// zone), first create a CNAME record within your zone pointing to the outside host, and then
165    /// set `resolve_override` to point at the CNAME record.
166    ///
167    /// Note that, for security reasons, it is not possible to set the Host header to specify a host
168    /// outside of your zone unless the request is actually being sent to that host.
169    pub resolve_override: Option<String>,
170    /// Whether ScrapeShield should be enabled for this request, if otherwise configured for this
171    /// zone. Defaults to `true`.
172    pub scrape_shield: Option<bool>,
173    /// mTLS certificate binding for this subrequest.
174    pub mtls_certificate: Option<JsValue>,
175}
176
177impl From<&CfProperties> for JsValue {
178    fn from(props: &CfProperties) -> Self {
179        let obj = js_sys::Object::new();
180        let defaults = CfProperties::default();
181        let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
182
183        set_prop(
184            &obj,
185            &JsValue::from("apps"),
186            &JsValue::from(props.apps.unwrap_or(defaults.apps.unwrap_or_default())),
187        );
188
189        set_prop(
190            &obj,
191            &JsValue::from("cacheEverything"),
192            &JsValue::from(
193                props
194                    .cache_everything
195                    .unwrap_or(defaults.cache_everything.unwrap_or_default()),
196            ),
197        );
198
199        set_prop(
200            &obj,
201            &JsValue::from("cacheKey"),
202            &JsValue::from(
203                props
204                    .cache_key
205                    .clone()
206                    .unwrap_or(defaults.cache_key.unwrap_or_default()),
207            ),
208        );
209
210        set_prop(
211            &obj,
212            &JsValue::from("cacheTtl"),
213            &JsValue::from(
214                props
215                    .cache_ttl
216                    .unwrap_or(defaults.cache_ttl.unwrap_or_default()),
217            ),
218        );
219
220        let ttl_status_map = props
221            .cache_ttl_by_status
222            .clone()
223            .unwrap_or(defaults.cache_ttl_by_status.unwrap_or_default());
224        set_prop(
225            &obj,
226            &JsValue::from("cacheTtlByStatus"),
227            &ttl_status_map.serialize(&serializer).unwrap_or_default(),
228        );
229
230        set_prop(
231            &obj,
232            &JsValue::from("minify"),
233            &JsValue::from(props.minify.unwrap_or(defaults.minify.unwrap_or_default())),
234        );
235
236        set_prop(
237            &obj,
238            &JsValue::from("mirage"),
239            &JsValue::from(props.mirage.unwrap_or(defaults.mirage.unwrap_or_default())),
240        );
241
242        let polish_val = props.polish.unwrap_or(defaults.polish.unwrap_or_default());
243        set_prop(
244            &obj,
245            &JsValue::from("polish"),
246            &serde_wasm_bindgen::to_value(&polish_val).unwrap(),
247        );
248
249        set_prop(
250            &obj,
251            &JsValue::from("resolveOverride"),
252            &JsValue::from(
253                props
254                    .resolve_override
255                    .clone()
256                    .unwrap_or(defaults.resolve_override.unwrap_or_default()),
257            ),
258        );
259
260        set_prop(
261            &obj,
262            &JsValue::from("scrapeShield"),
263            &JsValue::from(
264                props
265                    .scrape_shield
266                    .unwrap_or(defaults.scrape_shield.unwrap_or_default()),
267            ),
268        );
269
270        if let Some(image) = &props.image {
271            set_prop(
272                &obj,
273                &JsValue::from("image"),
274                &serde_wasm_bindgen::to_value(&image).unwrap(),
275            );
276        }
277
278        if let Some(mtls_certificate) = &props.mtls_certificate {
279            set_prop(&obj, &JsValue::from("mtlsCertificate"), mtls_certificate);
280        }
281
282        obj.into()
283    }
284}
285
286fn set_prop(target: &Object, key: &JsValue, val: &JsValue) {
287    let r = ::js_sys::Reflect::set(target, key, val);
288    debug_assert!(
289        r.is_ok(),
290        "setting properties should never fail on our dictionary objects"
291    );
292    let _ = r;
293}
294
295impl CfProperties {
296    pub fn new() -> Self {
297        Default::default()
298    }
299
300    pub fn is_default(&self) -> bool {
301        let de = CfProperties::default();
302        self.apps == de.apps
303            && self.cache_everything == de.cache_everything
304            && self.cache_key == de.cache_key
305            && self.cache_ttl == de.cache_ttl
306            && self.cache_ttl_by_status == de.cache_ttl_by_status
307            && self.minify == de.minify
308            && self.mirage == de.mirage
309            && self.image.is_none()
310            && self.polish == de.polish
311            && self.resolve_override == de.resolve_override
312            && self.scrape_shield == de.scrape_shield
313            && self.mtls_certificate.is_none()
314    }
315}
316
317impl Default for CfProperties {
318    fn default() -> Self {
319        Self {
320            apps: Some(true),
321            cache_everything: Some(false),
322            cache_key: None,
323            cache_ttl: None,
324            cache_ttl_by_status: None,
325            minify: None,
326            mirage: Some(true),
327            image: None,
328            polish: None,
329            resolve_override: None,
330            scrape_shield: Some(true),
331            mtls_certificate: None,
332        }
333    }
334}
335
336/// Configuration options for Cloudflare's minification features:
337/// <https://www.cloudflare.com/website-optimization/>
338#[wasm_bindgen]
339#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq)]
340#[serde(rename_all = "kebab-case")]
341pub struct MinifyConfig {
342    pub js: bool,
343    pub html: bool,
344    pub css: bool,
345}
346
347/// Configuration options for Cloudflare's image optimization feature:
348/// <https://blog.cloudflare.com/introducing-polish-automatic-image-optimizati/>
349#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq)]
350#[serde(rename_all = "kebab-case")]
351pub enum PolishConfig {
352    #[default]
353    Off,
354    Lossy,
355    Lossless,
356}
357
358#[derive(Clone, Debug, serde::Serialize)]
359#[serde(untagged)]
360pub enum ResizeBorder {
361    Uniform {
362        color: String,
363        width: usize,
364    },
365    Varying {
366        color: String,
367        top: usize,
368        right: usize,
369        bottom: usize,
370        left: usize,
371    },
372}
373
374#[derive(Clone, Debug, Serialize)]
375#[serde(rename_all = "kebab-case")]
376pub enum ResizeOriginAuth {
377    SharePublicly,
378}
379
380/// Configuration options for Cloudflare's image resizing feature:
381/// <https://developers.cloudflare.com/images/image-resizing/>
382#[derive(Clone, Debug, Default, Serialize)]
383#[serde(rename_all = "kebab-case")]
384pub struct ResizeConfig {
385    /// Whether to preserve animation frames from input files. Default is `true`. Setting it to `false` reduces animations to still images.
386    pub anim: Option<bool>,
387    /// Background color to add underneath the image. Applies to images with transparency (for example, PNG) and images resized with `fit=pad`.
388    pub background: Option<String>,
389    /// Blur radius between `1` (slight blur) and `250` (maximum). Be aware that you cannot use this option to reliably obscure image content.
390    pub blur: Option<u8>,
391    /// Adds a border around the image. The border is added after resizing.
392    pub border: Option<ResizeBorder>,
393    /// Increase brightness by a factor. A value of `1.0` equals no change, a value of `0.5` equals half brightness, and a value of `2.0` equals twice as bright.
394    pub brightness: Option<f64>,
395    /// Slightly reduces latency on a cache miss by selecting a quickest-to-compress file format, at a cost of increased file size and lower image quality.
396    pub compression: Option<ResizeCompression>,
397    /// Increase contrast by a factor. A value of `1.0` equals no change, a value of `0.5` equals low contrast, and a value of `2.0` equals high contrast.
398    pub contrast: Option<f64>,
399    /// Device Pixel Ratio. Default is `1`. Multiplier for `width`/`height` that makes it easier to specify higher-DPI sizes in `<img srcset>`.
400    pub dpr: Option<f64>,
401    /// Drawing operations to overlay on the image.
402    pub draw: Option<ResizeDraw>,
403    /// Affects interpretation of `width` and `height`. All resizing modes preserve aspect ratio.
404    pub fit: Option<ResizeFit>,
405    /// Flips the image horizontally, vertically, or both. Can be used with the `rotate` parameter to set the orientation of an image.
406    pub flip: Option<ResizeFlip>,
407    /// The `auto` option will serve the WebP or AVIF format to browsers that support it. If this option is not specified, a standard format like JPEG or PNG will be used.
408    pub format: Option<ResizeFormat>,
409    /// Increase exposure by a factor. A value of `1.0` equals no change, a value of `0.5` darkens the image, and a value of `2.0` lightens the image.
410    pub gamma: Option<f64>,
411    /// Specifies how an image should be cropped when used with `fit=cover` and `fit=crop`. Available options are `auto`, `face`, a side (`left`, `right`, `top`, `bottom`), and relative coordinates (`XxY` with a valid range of `0.0` to `1.0`).
412    pub gravity: Option<ResizeGravity>,
413    /// Specifies maximum height of the image in pixels. Exact behavior depends on the `fit` mode (described below).
414    pub height: Option<usize>,
415    /// Controls amount of invisible metadata (EXIF data) that should be preserved. Color profiles and EXIF rotation are applied to the image even if the metadata is discarded.
416    pub metadata: Option<ResizeMetadata>,
417    /// Authentication method for accessing the origin image.
418    pub origin_auth: Option<ResizeOriginAuth>,
419    /// In case of a fatal error that prevents the image from being resized, redirects to the unresized source image URL. This may be useful in case some images require user authentication and cannot be fetched anonymously via Worker.
420    pub onerror: Option<ResizeOnerror>,
421    /// Specifies quality for images in JPEG, WebP, and AVIF formats. The quality is in a 1-100 scale, but useful values are between `50` (low quality, small file size) and `90` (high quality, large file size).
422    pub quality: Option<ResizeQuality>,
423    /// Number of degrees (`90`, `180`, or `270`) to rotate the image by. `width` and `height` options refer to axes after rotation.
424    pub rotate: Option<usize>,
425    /// Increases saturation by a factor. A value of `1.0` equals no change, a value of `0.5` equals half saturation, and a value of `2.0` equals twice as saturated.
426    pub saturation: Option<f64>,
427    /// Specifies strength of sharpening filter to apply to the image. The value is a floating-point number between `0` (no sharpening, default) and `10` (maximum).
428    pub sharpen: Option<f64>,
429    /// Specifies a number of pixels to cut off on each side. Allows removal of borders or cutting out a specific fragment of an image.
430    pub trim: Option<ResizeTrim>,
431    /// Specifies maximum width of the image. Exact behavior depends on the `fit` mode; use the `fit=scale-down` option to ensure that the image will not be enlarged unnecessarily.
432    pub width: Option<usize>,
433}
434
435#[derive(Clone, Copy, Debug, Serialize)]
436#[serde(rename_all = "kebab-case")]
437pub enum ResizeCompression {
438    Fast,
439}
440
441#[derive(Clone, Debug, Serialize)]
442#[serde(untagged)]
443pub enum ResizeDrawRepeat {
444    Uniform(bool),
445    Axis(String),
446}
447
448#[derive(Clone, Debug, Serialize)]
449#[serde(rename_all = "kebab-case")]
450pub struct ResizeDraw {
451    url: String,
452    opacity: Option<f64>,
453    repeat: Option<ResizeDrawRepeat>,
454    top: Option<usize>,
455    bottom: Option<usize>,
456    left: Option<usize>,
457    right: Option<usize>,
458}
459
460#[derive(Clone, Copy, Debug, Serialize)]
461#[serde(rename_all = "kebab-case")]
462pub enum ResizeFit {
463    ScaleDown,
464    Contain,
465    Cover,
466    Crop,
467    Pad,
468}
469
470#[derive(Clone, Copy, Debug, Serialize)]
471pub enum ResizeFlip {
472    #[serde(rename = "h")]
473    Horizontally,
474    #[serde(rename = "v")]
475    Vertically,
476    #[serde(rename = "hv")]
477    Both,
478}
479
480#[derive(Clone, Copy, Debug, Serialize)]
481#[serde(rename_all = "kebab-case")]
482pub enum ResizeFormat {
483    Avif,
484    Webp,
485    Json,
486    Jpeg,
487    Png,
488    BaselineJpeg,
489    PngForce,
490    Svg,
491}
492
493#[derive(Clone, Copy, Debug, Serialize)]
494#[serde(rename_all = "kebab-case")]
495pub enum ResizeGravitySide {
496    Auto,
497    Left,
498    Right,
499    Top,
500    Bottom,
501}
502
503#[derive(Clone, Copy, Debug, Serialize)]
504#[serde(rename_all = "kebab-case")]
505#[serde(untagged)]
506pub enum ResizeGravity {
507    Side(ResizeGravitySide),
508    Coords { x: f64, y: f64 },
509}
510
511#[derive(Clone, Copy, Debug, Serialize)]
512#[serde(rename_all = "kebab-case")]
513pub enum ResizeQualityLiteral {
514    Low,
515    MediumLow,
516    MediumHigh,
517    High,
518}
519
520#[derive(Clone, Copy, Debug, Serialize)]
521#[serde(rename_all = "kebab-case")]
522#[serde(untagged)]
523pub enum ResizeQuality {
524    Literal(ResizeQualityLiteral),
525    Specific { value: usize },
526}
527
528#[derive(Clone, Copy, Debug, Serialize)]
529#[serde(rename_all = "kebab-case")]
530pub enum ResizeMetadata {
531    Keep,
532    Copyright,
533    None,
534}
535
536#[derive(Clone, Copy, Debug, Serialize)]
537#[serde(rename_all = "kebab-case")]
538pub enum ResizeOnerror {
539    Redirect,
540}
541
542#[derive(Clone, Copy, Debug, Serialize)]
543#[serde(rename_all = "kebab-case")]
544pub struct ResizeTrim {
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub top: Option<usize>,
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub bottom: Option<usize>,
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub left: Option<usize>,
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub right: Option<usize>,
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub width: Option<usize>,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub height: Option<usize>,
557}
558
559#[derive(Clone, Copy, Debug, Default, Serialize)]
560#[serde(rename_all = "kebab-case")]
561pub enum RequestRedirect {
562    Error,
563    #[default]
564    Follow,
565    Manual,
566}
567
568impl From<RequestRedirect> for &str {
569    fn from(redirect: RequestRedirect) -> Self {
570        match redirect {
571            RequestRedirect::Error => "error",
572            RequestRedirect::Follow => "follow",
573            RequestRedirect::Manual => "manual",
574        }
575    }
576}
577
578impl From<RequestRedirect> for web_sys::RequestRedirect {
579    fn from(redir: RequestRedirect) -> Self {
580        match redir {
581            RequestRedirect::Error => web_sys::RequestRedirect::Error,
582            RequestRedirect::Follow => web_sys::RequestRedirect::Follow,
583            RequestRedirect::Manual => web_sys::RequestRedirect::Manual,
584        }
585    }
586}
587
588/// Cache mode for controlling how requests interact with the cache.
589/// Corresponds to JavaScript's `RequestInit.cache` property.
590#[derive(Clone, Copy, Debug, Eq, PartialEq)]
591pub enum CacheMode {
592    NoStore,
593    NoCache,
594    Reload,
595}
596
597impl From<CacheMode> for web_sys::RequestCache {
598    fn from(mode: CacheMode) -> Self {
599        match mode {
600            CacheMode::NoStore => web_sys::RequestCache::NoStore,
601            CacheMode::NoCache => web_sys::RequestCache::NoCache,
602            CacheMode::Reload => web_sys::RequestCache::Reload,
603        }
604    }
605}
606
607#[test]
608fn request_init_no_invalid_options() {
609    let mut init = RequestInit::new();
610    init.method = Method::Post;
611
612    let js_init: web_sys::RequestInit = (&init).into();
613
614    let _ = web_sys::Request::new_with_str_and_init("https://httpbin.org/post", &js_init).unwrap();
615}