Cloudflare Worker Rust

This module enables the use of DataDome Bot Protection on Cloudflare, for Workers written in Rust. It is distributed as the datadome-cloudflare-worker crate.

Looking for the JavaScript version? See Cloudflare Worker.

Installation

Prerequisites

  • Wrangler 4 installed and configured.

  • A stable Rust toolchain with the wasm32-unknown-unknown target and worker-build:

    rustup target add wasm32-unknown-unknown
    cargo install worker-build
  • A Rust Worker project. If you don't have one, create it with cargo generate cloudflare/workers-rs.

Protect your traffic

Update an existing Rust Worker to start protecting your traffic with DataDome Bot Protection:

  1. Add the crate to your Cargo.toml, next to the worker crate you already depend on:

    [dependencies]
    datadome-cloudflare-worker = "1.0.0"
    worker = "0.8"
  2. In your entry file (src/lib.rs), hand every request to the module before your own logic:

    use datadome_cloudflare_worker::DataDome;
    use worker::*;
    
    #[event(fetch)]
    async fn fetch(req: Request, env: Env, _ctx: Context) -> Result<Response> {
        DataDome::new()
            .on_allowed_request(my_handler)
            .handle_request(req, &env)
            .await
    }
    
    // Your code: produces the response for every request DataDome lets through, usually by forwarding it to your origin.
    async fn my_handler(req: Request) -> Result<Response> {
        Fetch::Request(req).send().await
    }
    • on_allowed_request is required: your origin is always called through it. handle_request only exists once it has been called, so forgetting it is a compile error.
    • When DataDome blocks the request, the challenge or block response goes to the client directly and my_handler is skipped. To run code on it, add on_challenged_request (see Run code on challenged requests).
    • Settings come from the DATADOME_* variables and secrets of the Worker, or from the builder (see Configure in code).
  3. Make sure your wrangler.toml builds the Worker with worker-build and declares the route to protect:

    name = "my-worker"
    main = "build/index.js"
    compatibility_date = "2026-01-01"
    route = { pattern = "example.org/*", zone_name = "example.org" }
    
    [build]
    command = "worker-build --release"
  4. Create a Worker secret named DATADOME_SERVER_SIDE_KEY:

    wrangler secret put DATADOME_SERVER_SIDE_KEY

    A prompt appears. Enter the value of your DataDome server-side key you can find in your DataDome Dashboard.

  5. Optional, to inject the JS Tag into your HTML pages: create a second secret named DATADOME_CLIENT_SIDE_KEY with your client-side key, found on the same dashboard page (see Enable JS Tag injection). Skip it if your pages already load the JS Tag.

    wrangler secret put DATADOME_CLIENT_SIDE_KEY
  6. Deploy:

    wrangler deploy

Congrats! 🎉 Your traffic is now protected by DataDome Bot Protection.

Configuration

Every setting is resolved for each request from three sources. When the same setting is set in more than one, the one listed last wins:

  1. Built-in defaults.
  2. The builder setters, in code (see Configure in code).
  3. Worker variables and secrets named DATADOME_*, set in wrangler.toml or with wrangler secret put.

Values of variables are strings: booleans are true (case-insensitive), numbers are parsed, lists are JSON arrays of strings, regular expressions are patterns. An empty or unparsable value is ignored and the previous source applies.

Server-side settings

SettingDescriptionRequiredDefault
DATADOME_SERVER_SIDE_KEYDataDome server-side key. Store it as a secret.Yes—
DATADOME_TIMEOUTTimeout of the call to the DataDome API, in milliseconds (see How do I change the timeout value?)Optional300
DATADOME_ENDPOINTHost of the DataDome APIOptionalapi-cloudflare.datadome.co
DATADOME_URL_REGEX_EXCLUSIONRegular expression to ignore all matching URLs (see URL matching)Optional—
DATADOME_STATIC_ASSET_EXCLUSIONRegular expression of static-asset extensions to ignore. An empty value disables the list.OptionalDefault static-asset pattern
DATADOME_ENCODED_CHAR_INCLUSIONRegular expression of encoded characters; a static-asset URL whose path contains a match is inspected anyway. An empty value disables it.Optional%23|%25|%2f|%3b|%3f
DATADOME_REMOVE_MATRIX_PARAMSStrip matrix parameters (;name=value segment suffixes, RFC 3986) from the path before matchingOptionaltrue
DATADOME_URL_REGEX_INCLUSIONRegular expression to process only matching URLs, applied after the exclusionsOptional—
DATADOME_IP_EXCLUSIONJSON array of IPv4 and IPv6 addresses or CIDR ranges whose traffic is ignored, for example ["192.168.0.1", "10.0.0.0/8"]Optional—
DATADOME_ENABLE_GRAPHQL_SUPPORTExtract the GraphQL operation type, name, and count from requests to the GraphQL endpoint (see Enable GraphQL support)Optionalfalse
DATADOME_GRAPHQL_ENDPOINTComma-separated list of GraphQL endpoint paths; a request matches when its path contains one of themOptionalgraphql
DATADOME_ENABLE_MCP_SUPPORTReport MCP information for requests to the MCP endpoint (see Enable MCP support)Optionalfalse
DATADOME_MCP_ENDPOINTComma-separated list of MCP endpoint paths, matched like the GraphQL onesOptionalmcp
DATADOME_ENABLE_REFERRER_RESTORATIONRestore the original Referer header after a challenge (see Restore the referrer)Optionalfalse
DATADOME_MAXIMUM_BODY_SIZEMaximum number of request body bytes read for the GraphQL and MCP fieldsOptional25600
DATADOME_ENABLE_DEBUGGINGLog every step of the DataDome process in the Workers logs, not only the errorsOptionalfalse

Custom fields, user and product identifiers are set in code, per request (see Add custom fields).

Client-side settings

SettingDescriptionRequiredDefault
DATADOME_CLIENT_SIDE_KEYClient-side key from the DataDome dashboard. When set, the JS Tag is injected into HTML responses (see Enable JS Tag injection)Optional—
DATADOME_JS_URLURL of the JS Tag, for a first-party setupOptionalhttps://js.datadome.co/tags.js
DATADOME_JS_ENDPOINTEndpoint of the JS Tag, added to its optionsOptional—
DATADOME_JS_TAG_OPTIONSJS Tag options, as a JSON object, written as window.ddoptionsOptional{}
DATADOME_JS_URL_REGEX_EXCLUSIONRegular expression of URLs that must not get the JS TagOptional—
DATADOME_JS_URL_REGEX_INCLUSIONRegular expression of URLs that get the JS Tag; all pages when unsetOptional—

URL matching

Patterns are matched against host + path (for example www.example.com/products/1.html), without the query string. The path is used exactly as received, percent-encoded. The JS Tag patterns are matched against the full URL instead.

The default static-asset pattern is:

(?i)\.(avi|avif|bmp|css|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|json|less|map|mka|mkv|mov|mp3|mp4|mpeg|mpg|ogg|ogm|opus|otf|pdf|png|svg|svgz|swf|tif|tiff|ttf|wav|webm|webp|woff|woff2|xml|zip)$
📘

Regex support

Regular expressions use the Rust regex syntax. Look-around and backreferences are not supported; an invalid pattern is ignored and the default is kept.

Advanced Configuration

Configure in code

The builder has one setter per setting, named like the variable without the DATADOME_ prefix, in lower case. A variable or secret with the same name still wins:

DataDome::new()
    .timeout(500)
    .url_regex_exclusion(Regex::new("^/health$").unwrap())
    .ip_exclusion(["10.0.0.0/8"])
    .on_allowed_request(my_handler)
    .handle_request(req, &env)
    .await

Run code on challenged requests

To log, count, or notify on challenged requests, or to adapt the response, add on_challenged_request. It runs once per challenged request (challenge page, hard block, redirect, 429), after the response is built and before it is sent, and returns the response to send to the client.

The hook receives:

  • the client request, carrying the enriched headers (x-datadome-botname, x-datadome-isbot, ...);
  • the DataDome response, with its status and the headers about to be sent to the client (X-DataDome, X-DataDome-CID, Set-Cookie, ...).
DataDome::new()
    .on_allowed_request(my_handler)
    .on_challenged_request(|req, response| async move {
        let botname = req.headers().get("x-datadome-botname")?.unwrap_or_default();
        let isbot = req.headers().get("x-datadome-isbot")?.unwrap_or_default();
        console_log!("challenged {} {} -> {} (botname: {botname}, isbot: {isbot})", req.method(), req.path(), response.status_code());
        Ok(response)
    })
    .handle_request(req, &env)
    .await

Do not forward the request from this hook. An error it returns is propagated to the caller, like an error from your handler. Allowed, skipped, and bypassed requests reach your handler instead. Without the hook, the response is sent untouched.

Add custom fields

📘

Custom fields feature

DataDome lets you enrich in real time our detection engine by sending us some custom fields with your business data. These fields can be used for specific detection models.

👋 Please reach out to our support team for reviewing the data received.

Pass them with request_metadata, as a list of field name and value. A value is a fixed string, a request header (DataDome::header), or a closure computing it from the request (DataDome::from_fn); an absent header, an empty value or None leaves the field unset. The fields are CustomFieldString1..3, CustomFieldInteger1..2, CustomFieldFloat1, UserID (opaque identifier, never personal data), and ProductID; all values are sent as strings and cut to the length the API accepts:

DataDome::new()
    .request_metadata([
        ("CustomFieldString1", "standard".into()),
        ("ProductID", DataDome::header("x-product-id")),
        ("UserID", DataDome::header("x-user-id")),
        // Computed: sent only when the header is a valid integer.
        ("CustomFieldInteger1", DataDome::from_fn(|req| {
            let value = req.headers().get("x-cart-size").ok().flatten()?;
            value.trim().parse::<i64>().ok().map(|n| n.to_string())
        })),
    ])
    .on_allowed_request(my_handler)
    .handle_request(req, &env)
    .await

A header value must be set by a trusted proxy that strips it from client requests.

Restore the referrer

After passing a DataDome challenge on browsers other than Firefox, the referrer value is updated to the current URL, which can lead to inconsistent results in website analytics.

Set DATADOME_ENABLE_REFERRER_RESTORATION to true to restore the Referer header to its original value for your origin. Contact our support team first: they will review your requirements and provide you with the best recommendations.

Enable GraphQL support

Set DATADOME_ENABLE_GRAPHQL_SUPPORT to true.

Once enabled, POST requests targeting a GraphQL endpoint with content-type: application/json will be analyzed to extract GraphQL operation name and type.

Multiple endpoints can be configured using the DATADOME_GRAPHQL_ENDPOINT setting.

Enable MCP support

Set DATADOME_ENABLE_MCP_SUPPORT to true.

Once enabled, requests targeting a Model Context Protocol (MCP) endpoint will be analyzed to extract MCP properties according to the official specification:

  • mcp-session-id and mcp-protocol-version headers
  • jsonRpcVersion, jsonRpcRequestId, and mcpMethod request body fields
  • mcpParamsClientInfoName and mcpParamsClientInfoVersion request body fields when the method is initialize
  • mcpParamsToolName request body field when the method is tools/call

Multiple endpoints can be configured using the DATADOME_MCP_ENDPOINT setting.

Enable JS Tag injection

Set DATADOME_CLIENT_SIDE_KEY to your client-side key, as a secret like the server-side key (step 5 of Protect your traffic). The module then inserts the JS Tag loader at the beginning of the <head> or <body> of every HTML page your origin returns for an allowed request:

<script>
  window.ddjskey = 'YOUR_CLIENT_SIDE_KEY';
  window.ddoptions = {};
</script>
<script src="https://js.datadome.co/tags.js" async></script>

Use DATADOME_JS_URL, DATADOME_JS_ENDPOINT, and DATADOME_JS_TAG_OPTIONS to change the script URL and the options, and DATADOME_JS_URL_REGEX_INCLUSION or DATADOME_JS_URL_REGEX_EXCLUSION to choose the pages.

📘

If your pages already load the JS Tag, leave DATADOME_CLIENT_SIDE_KEY unset.

Logging

Enriched headers

The DataDome API returns enriched headers with each request. The module copies the ones the API lists in x-datadome-request-headers onto the request, and the ones in x-datadome-headers onto the response sent to the client. Where to read them depends on the response:

  • Allowed request: your handler receives the request with the headers, and the response it returns gets the response headers.
  • Challenged request: your handler is not called; the on_challenged_request hook receives the request with the headers, and the response with its headers.
  • Not analysed (excluded URL, timeout, or error of the API call, answer without a verdict): no header is added, and your handler receives the request untouched.
async fn my_handler(req: Request) -> Result<Response> {
    for (name, value) in req.headers().entries() {
        if name.starts_with("x-datadome") {
            console_log!("{name}: {value}");
        }
    }
    Fetch::Request(req).send().await
}

DataDome logs

  1. Enable the Workers logs in your wrangler.toml:

    [observability.logs]
    enabled = true
  2. Deploy with wrangler deploy.

  3. By default, the module logs only the requests DataDome could not analyse (error of the API call) and the post-replay failures. Timeouts are logged only in debug mode. Set DATADOME_ENABLE_DEBUGGING to true to log every step, including timeouts and excluded URLs.

The logs are JSON lines with the following shape:

{
  "step": "check Protection API response status",
  "result": "allow request",
  "reason": "response is allowed by DataDome",
  "company": "DataDome"
}

Uninstallation

  1. Remove datadome-cloudflare-worker from your Cargo.toml.
  2. In src/lib.rs, call your handler directly instead of DataDome::new()...handle_request.
  3. Delete the DATADOME_* variables from your wrangler.toml, and the secrets with wrangler secret delete DATADOME_SERVER_SIDE_KEY and wrangler secret delete DATADOME_CLIENT_SIDE_KEY.
  4. Deploy with wrangler deploy.

FAQ

How do I change the timeout value?

Set DATADOME_TIMEOUT in milliseconds, in wrangler.toml or with the timeout setter. When the timeout expires, the module logs the step and forwards the request to your handler untouched.

How do I test or debug the Worker?

Tail the logs with wrangler tail --format pretty, or read them in the Cloudflare dashboard once the Workers logs are enabled (see DataDome logs). With DATADOME_ENABLE_DEBUGGING set to true, every request logs its response and the reason.

The request is forwarded to your handler unprotected, with a log line, when:

  • The DataDome API did not answer within DATADOME_TIMEOUT (logged in debug mode only).
  • The DataDome API could not be reached, or answered without the x-datadomeresponse header.

To test the challenge, send requests with the BLOCKUA (blocked, HTTP 403) and CI-Allowed (allowed, HTTP 200) user agents:

curl -i -A CI-Allowed https://example.org/
curl -i -A BLOCKUA https://example.org/

Are requests in the cache protected by DataDome?

Yes. The Worker is executed on the incoming request before the Cloudflare cache lookup, like the JavaScript module.


Did this page help you?