Get started Dashboard
Troubleshooting ·

Fix Cloudflare Error 1037: Invalid rewrite rule

Explore with AI

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.

Ongoing incident? Our founding team can help you get your Cloudflare app back up.
On this page

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).

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).

Error 1037 means the rewrite expression could not produce a value. Cloudflare’s Error 1037 page 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). 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.

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. Fetch the failing URL and check the body:

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

Next, find the rule. Use Cloudflare Trace on the failing URL to see which URL rewrite rules match it (URL Rewrite Rules: 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):

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:

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). 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:

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):
concat("/src/", coalesce(http.request.headers["x-source"][0], "default"), http.request.uri.path)
  1. 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):
starts_with(http.request.uri.path, "/app/") and has_key(http.request.headers, "x-source")
  1. Use the lowercase header name in lookups, as Cloudflare’s own examples do.
  2. 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.

Query arguments work like headers. http.request.uri.args is also a map of string arrays (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

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:
has_key(http.request.uri.args, "lang")
  1. If the rewrite needs a value anyway, add a fallback:
concat("/", coalesce(http.request.uri.args["lang"][0], "en"), http.request.uri.path)
  1. 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):
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). 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). Requiring two values looks like this:
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), 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). 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). 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 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) 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.

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 (7)

  1. Cloudflare 1xxx errors
  2. Error 1037
  3. Rules language: Functions
  4. Rules language: Values
  5. URL Rewrite Rules
  6. Create a URL rewrite rule in the dashboard
  7. Create a URL rewrite rule via API

About the author

Boris Tane

Founder of Polylane

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

Nobody should be on-call. Polylane watches your infra, finds what broke, and writes the fix.

Get started for free