# Fix Cloudflare Error 1037: Invalid rewrite rule

> Cloudflare Error 1037 means a rewrite expression failed to evaluate, often because a header or query argument is missing. How to find the rule and fix it.

By Boris Tane, Founder of Polylane · Published October 18, 2026 · 10 min read
Canonical: https://polylane.com/learn/troubleshooting/cloudflare-error-1037-invalid-rewrite-rule-failed-to/

```text
Error 1037: Invalid rewrite rule (failed to evaluate expression)
```

Error 1037 means Cloudflare could not evaluate a dynamic rewrite expression. Cloudflare says there are several causes, and a common one is a part of the expression with no value, such as a request header or query argument the request didn't send. Fix it by giving every lookup a fallback with coalesce(), or by adding a has_key() check to the rule's filter so the rewrite only runs when the value is there.

Error 1037 shows up on a site proxied by Cloudflare, usually right after someone deploys a URL rewrite rule. Some requests get through and others get an HTML error page. It is a 1xxx error, so the code is in the HTML body of the response. The status header carries a plain `500`. If your logs, analytics or uptime checks only record status codes, you will see a 500 spike and nothing else ([Cloudflare 1xxx errors](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/)).

The cause sits at Cloudflare's edge, which failed to work out the new URL for this request. The fault is in a rule you control, and so is the fix.

## What “failed to evaluate expression” means

A URL rewrite rule has two expressions. The **filter expression** decides which requests the rule applies to. The **rewrite expression** works out the new path or query string. A static rewrite swaps in a fixed string. A dynamic rewrite runs an expression in the Rules language, built from request fields and functions ([URL Rewrite Rules](https://developers.cloudflare.com/rules/transform/url-rewrite/)).

Error 1037 means the rewrite expression could not produce a value. Cloudflare's [Error 1037 page](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1037/) gives the usual reason: one element of the expression was undefined when it ran. Its example is a dynamic rewrite that reads `http.request.headers["x-source"][0]` and gets a request with no `X-Source` header.

The Rules language docs explain why this happens. An absent map key or an out-of-bounds array index gives a **missing value** (nil). You can't type nil into an expression yourself. Functions usually return nil when one of their arguments is nil ([Values: missing values](https://developers.cloudflare.com/ruleset-engine/rules-language/values/#missing-values)). In a filter expression, nil just turns into `true` or `false` in comparisons. In a rewrite expression, nil leaves Cloudflare with no path or query to write, so the request fails with 1037.

```mermaid alt="How a request with a missing header turns a dynamic URL rewrite into Error 1037, and the two fixes"
graph TB
    A[Request reaches Cloudflare] --> B{Filter expression matches?}
    B -->|no| C[Rule skipped, request passes]
    B -->|yes| D[Rewrite expression reads a field]
    D --> E{Header or query arg present?}
    E -->|yes| F[New path built, request goes on]
    E -->|no| G[Missing value in the expression]
    G --> H[Error 1037, HTTP 500]
    G --> I[Fix: coalesce gives a fallback]
    B --> J[Fix: has_key in the filter]
    style H fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
    style I fill:#d1fae5,stroke:#6ee7b7,color:#065f46
    style J fill:#d1fae5,stroke:#6ee7b7,color:#065f46
```

That is why 1037 is usually intermittent. The rule works when you test it from a client that sends the header, and fails for the bots, health checks and older app versions that don't.

## Confirming it is 1037 and finding the rule

First make sure you have the right error. Cloudflare has several rewrite errors next to each other, and each has its own cause:

- **1035**: invalid request rewrite (invalid URI path), returned as `500`
- **1036**: invalid request rewrite (maximum length exceeded), returned as `400`
- **1037**: invalid rewrite rule (failed to evaluate expression), returned as `500`
- **1040**: header modification not allowed, returned as `500`
- **1041**: invalid header value, returned as `500`

All of these come from the [1xxx error list](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/). Fetch the failing URL and check the body:

```bash
curl -s https://example.com/app/dashboard | grep -o "Error 10[0-9][0-9]"
# Error 1037
```

Next, find the rule. Use [Cloudflare Trace](https://developers.cloudflare.com/rules/trace-request/) on the failing URL to see which URL rewrite rules match it ([URL Rewrite Rules: troubleshooting](https://developers.cloudflare.com/rules/transform/url-rewrite/#troubleshooting)). Start with rules set to **Rewrite to > Dynamic**, since a static rewrite swaps in a fixed string with no expression to evaluate. If several rules match, read each one's rewrite expression.

From a terminal, you can list the rules in the zone's `http_request_transform` ruleset, where URL rewrite rules live ([Create a URL rewrite rule via API](https://developers.cloudflare.com/rules/transform/url-rewrite/create-api/)):

```bash
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RULESET_ID" \
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  | jq '.result.rules[] | {ref, expression, rewrite: .action_parameters.uri}'
```

In every rule that has a `path.expression` or `query.expression`, look for map lookups and indexes: `headers["..."]`, `args["..."]`, `[0]`, `[1]`. Each of these can come back empty.

## Cause 1: the rewrite reads a header the request didn't send

This is the case Cloudflare documents, and the one people hit most. A typical rule sends traffic to a different path based on a header:

```txt
concat("/src/", http.request.headers["x-source"][0], http.request.uri.path)
```

`http.request.headers` is a map, and every value in it is an array of strings ([Values: maps](https://developers.cloudflare.com/ruleset-engine/rules-language/values/#maps)). If `X-Source` is absent, the key is missing and `[0]` gives nil. Since functions usually return nil when an argument is nil, the whole expression is likely to come out nil.

### How to tell it's yours

Send the same request twice, once without the header and once with it:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/app/dashboard
# 500

curl -s -o /dev/null -w "%{http_code}\n" -H "X-Source: web" https://example.com/app/dashboard
# 200
```

If the status changes when you add the header, you have found the cause.

### Fix it with a fallback or a guard

1. Decide what should happen when the header is missing. Either the request takes a default path, or the rule shouldn't apply at all.
2. For a default, wrap the lookup in `coalesce()`. It returns the first argument that isn't nil, and it needs at least two arguments of the same type ([Functions: coalesce](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#coalesce)):

```txt
concat("/src/", coalesce(http.request.headers["x-source"][0], "default"), http.request.uri.path)
```

3. To skip the rule, add a `has_key()` check to the filter expression. It returns true when the key exists in the map ([Functions: has_key](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#has_key)):

```txt
starts_with(http.request.uri.path, "/app/") and has_key(http.request.headers, "x-source")
```

4. Use the lowercase header name in lookups, as Cloudflare's own examples do.
5. Deploy, then run both curl requests again. Both should return the status you expect.

The guard is better when a missing header means the rule simply doesn't apply. The fallback is better when every request on that path must be rewritten. You can use both together.

## Cause 2: a missing query argument or cookie value

Query arguments work like headers. `http.request.uri.args` is also a map of string arrays ([Values: maps](https://developers.cloudflare.com/ruleset-engine/rules-language/values/#maps)), so `http.request.uri.args["lang"][0]` is nil on any URL without `?lang=`. A rule that turns `/page?lang=fr` into `/fr/page` will fail with 1037 on a plain `/page`, unless the filter makes sure the argument is there.

A common trap: the filter checks something related, such as `http.request.uri.query contains "lang"`, which also matches `?language=en`. The filter passes, the `lang` key is still missing, and the rewrite fails.

### How to tell it's yours

```bash
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/page?language=en"
# 500
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/page?lang=fr"
# 200
```

### Fix the filter to match the field the rewrite reads

1. Make the filter check the exact key the rewrite expression uses:

```txt
has_key(http.request.uri.args, "lang")
```

2. If the rewrite needs a value anyway, add a fallback:

```txt
concat("/", coalesce(http.request.uri.args["lang"][0], "en"), http.request.uri.path)
```

3. Push the corrected rule through the API in one update. The rule shape follows Cloudflare's dynamic rewrite example ([Create a URL rewrite rule via API](https://developers.cloudflare.com/rules/transform/url-rewrite/create-api/)):

```bash
curl "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/$RULESET_ID" \
  --request PUT \
  --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  --json '{
    "rules": [
      {
        "ref": "rewrite_lang_prefix",
        "expression": "has_key(http.request.uri.args, \"lang\")",
        "description": "Prefix path with lang argument",
        "action": "rewrite",
        "action_parameters": {
          "uri": {
            "path": {
              "expression": "concat(\"/\", coalesce(http.request.uri.args[\"lang\"][0], \"en\"), http.request.uri.path)"
            }
          }
        }
      }
    ]
  }'
```

A `PUT` to the ruleset replaces its whole rule list. Include every rule you want to keep.

## Cause 3: an index past the end of the array

The value for a header or argument can be present and still too short. Reading out of bounds gives a missing value ([Values: arrays](https://developers.cloudflare.com/ruleset-engine/rules-language/values/#arrays)). `http.request.uri.args["filter"][1]` works on `?filter=waf&filter=cdn` and is nil on `?filter=waf`. Headers sent once behave the same way when the expression asks for index `[1]`.

### How to tell it's yours

The rewrite expression uses an index above `[0]`, and the failing requests carry the parameter once while the passing ones carry it more than once.

### Fix it

1. Read `[0]` unless you really need a later value.
2. If you do need it, wrap the lookup: `coalesce(http.request.uri.args["filter"][1], "none")`.
3. In the filter, `len()` returns the number of elements in an array ([Functions: len](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/#len)). Requiring two values looks like this:

```txt
len(http.request.uri.args["filter"]) >= 2
```

## Cause 4: nil passed through other functions

A missing value doesn't always come straight from a lookup. Functions usually return nil when an argument is nil ([Values: functions](https://developers.cloudflare.com/ruleset-engine/rules-language/values/#functions)), and some functions say so in their reference. `join()` returns nil if any argument is nil. `trim()` returns nil for a nil source. `hash_in_range()` returns nil when all of its seed values are missing ([Functions](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/)). By that general rule, `lower(http.request.headers["x-tenant"][0])` inside a `concat()` is likely to fail the same way a bare lookup does.

### Fix it

1. Find the innermost field lookup in the expression.
2. Wrap that lookup in `coalesce()`, inside the outer function: `lower(coalesce(http.request.headers["x-tenant"][0], "public"))`.
3. Check each argument of each function the same way. One unguarded lookup anywhere in the expression is enough to cause 1037.

## Cause 5: a rule depends on a value an earlier rule set

URL rewrite rules run in order. Within the `http_request_transform` phase, field values don't change: later rules still see the original request, including the original values in their filter expressions ([URL Rewrite Rules: important remarks](https://developers.cloudflare.com/rules/transform/url-rewrite/#important-remarks)). If rule 1 rewrites the query string to add `?region=eu` and rule 2 reads `http.request.uri.args["region"][0]`, rule 2 still sees the original query, where that key doesn't exist.

### How to tell it's yours

Trace the URL and see whether two or more rewrite rules match it. Check whether the failing rule reads a field that only an earlier rule would create.

### Fix it

1. Merge the two rules into one that computes the final path or query from the original request fields.
2. Or base the second rule on the same source field the first rule read, with a `coalesce()` fallback.
3. For logic that needs real branching across several steps, Cloudflare points to [Snippets](https://developers.cloudflare.com/rules/snippets/) for more complex rewrites.

## Keeping 1037 from coming back

The pattern behind every case is the same: someone writes a rewrite expression assuming a field will always be there, and some clients don't send it. A few habits close that gap.

- **Pair every lookup with a guard or a fallback.** Any `headers[...]`, `args[...]` or index in a rewrite expression should sit inside `coalesce()`, or the rule's filter should check it with `has_key()` or `len()`. Make this a review rule for anyone who edits rewrite rules.
- **Test the empty case before deploying.** For each field the rewrite reads, send one request without it. A short script with the curl commands above, run against a staging zone or right after deploy, catches most 1037s within minutes.
- **Keep rules in code.** Manage rewrite rules through the Cloudflare API or Terraform ([URL Rewrite Rules](https://developers.cloudflare.com/rules/transform/url-rewrite/)) so expression changes go through pull requests. A diff that adds `[0]` without `coalesce(` is easy to flag in review.
- **Alert on edge 500s by path.** Because 1037 is returned as a `500`, a jump in 500s on the paths a rewrite rule covers, with no matching errors in your origin logs, likely points at the edge. Comparing edge status codes with origin logs is the quickest way to tell a rule failure from an application failure.
- **Watch for new clients.** A rule that worked for a year can start failing when a new mobile build, partner integration or crawler skips the header. Check the rewrite rules whenever you change which headers your clients send.

Running on Cloudflare? See [how Polylane monitors Cloudflare in production](https://polylane.com/for/cloudflare/).

## Common questions

**What HTTP status code does Cloudflare Error 1037 return?**

It returns HTTP 500. The 1037 code itself is in the HTML body of the error page, so tools that only log status codes will show a 500 with no further detail. Fetch the URL with curl and look for Error 1037 in the body to confirm it.

**Does Error 1037 mean my origin server is down?**

Not usually. Cloudflare reports 1037 when a rewrite expression fails to evaluate, and URL rewrites run at Cloudflare's edge. If your origin logs show no matching errors for the failing requests, that is a likely sign the fault is in the rule and not your application.

**Can a static URL rewrite cause Error 1037?**

A static rewrite only swaps in a fixed string, so it has no Rules language expression to evaluate and is the less likely suspect. Dynamic rewrites use an expression to build the new path or query. Check the dynamic rules that match the failing URL first.

**Why does the rewrite rule work in my browser but fail for some users?**

Your test client probably sends the header or query argument the expression reads, and some real clients don't. An absent map key gives a missing value, and a rewrite expression with a missing value fails. Repeat the request with curl without that header or argument and you should get the 500.

**Should I use coalesce() or has_key() to fix it?**

Use has_key() in the filter expression when a request without the field shouldn't be rewritten at all. Use coalesce() inside the rewrite expression when every request should be rewritten and a default value makes sense. Many rules use both, so the filter is precise and the expression can't fail.

**How is Error 1037 different from 1035 and 1036?**

1037 means the expression could not be evaluated, usually because of a missing value. 1035 means the rewrite produced an invalid URI path, and 1036 means the result was too long. 1035 and 1037 both return 500, while 1036 returns 400.

**Can one URL rewrite rule read a query argument added by an earlier rule?**

No. Field values don't change within the http_request_transform phase, so later rules still see the original request. Merge the two rules into one, or compute the value from the original fields with a coalesce() fallback.

## Sources

- [Cloudflare 1xxx errors](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/)
- [Error 1037](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1037/)
- [Rules language: Functions](https://developers.cloudflare.com/ruleset-engine/rules-language/functions/)
- [Rules language: Values](https://developers.cloudflare.com/ruleset-engine/rules-language/values/)
- [URL Rewrite Rules](https://developers.cloudflare.com/rules/transform/url-rewrite/)
- [Create a URL rewrite rule in the dashboard](https://developers.cloudflare.com/rules/transform/url-rewrite/create-dashboard/)
- [Create a URL rewrite rule via API](https://developers.cloudflare.com/rules/transform/url-rewrite/create-api/)

## About the author

Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.

## Related

- [How to Debug Cloudflare Workers Errors: Logs, Traces and Error Codes](https://polylane.com/learn/troubleshooting/how-to-debug-cloudflare-workers-errors/): Debug Cloudflare Workers errors step by step: read 1101 and 1102 codes, enable Workers Logs and source maps, use wrangler tail, DevTools and local traces.
- [Cloudflare Workers error 1101: causes and how to fix it](https://polylane.com/learn/troubleshooting/cloudflare-workers-error-1101/): Error 1101 means your Cloudflare Worker threw an uncaught JavaScript exception. Find the exception in logs, match it to its cause, fix it and roll back fast.
- [Fix Cloudflare “Error 1102: Worker exceeded resource limits”](https://polylane.com/learn/troubleshooting/cloudflare-error-1102-worker-exceeded-resource-limits/): Why a Cloudflare Worker returns Error 1102, how to tell a CPU time overrun from a 128 MB memory overrun, and the code and Wrangler changes that fix each.
- [How to correlate incidents with deploys using evidence](https://polylane.com/learn/deployment-safety/how-to-correlate-incidents-with-deploys/): Learn how to correlate incidents with deploys using release markers, metrics, logs and verified GitHub, Vercel, Cloudflare and Kubernetes change events.
- [Fix Cloudflare “Error 500: internal server error”](https://polylane.com/learn/troubleshooting/cloudflare-error-500-internal-server-error/): Why Cloudflare shows Error 500: internal server error, how to tell if your origin, a Worker or a Page Rule caused it, and the fix for each cause.

Get started with one command: `curl -fsSL https://polylane.com/setup | bash` installs the CLI, connects your coding agents, and creates the account.
