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
- What “failed to evaluate expression” means
- Confirming it is 1037 and finding the rule
- Cause 1: the rewrite reads a header the request didn’t send
- Cause 2: a missing query argument or cookie value
- Cause 3: an index past the end of the array
- Cause 4: nil passed through other functions
- Cause 5: a rule depends on a value an earlier rule set
- Keeping 1037 from coming back
- Common questions
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.
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
- Decide what should happen when the header is missing. Either the request takes a default path, or the rule shouldn’t apply at all.
- 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)
- 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")
- Use the lowercase header name in lookups, as Cloudflare’s own examples do.
- 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), 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
- Make the filter check the exact key the rewrite expression uses:
has_key(http.request.uri.args, "lang")
- If the rewrite needs a value anyway, add a fallback:
concat("/", coalesce(http.request.uri.args["lang"][0], "en"), http.request.uri.path)
- 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
- Read
[0]unless you really need a later value. - If you do need it, wrap the lookup:
coalesce(http.request.uri.args["filter"][1], "none"). - 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
- Find the innermost field lookup in the expression.
- Wrap that lookup in
coalesce(), inside the outer function:lower(coalesce(http.request.headers["x-tenant"][0], "public")). - 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
- Merge the two rules into one that computes the final path or query from the original request fields.
- Or base the second rule on the same source field the first rule read, with a
coalesce()fallback. - 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 insidecoalesce(), or the rule’s filter should check it withhas_key()orlen(). 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]withoutcoalesce(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)
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 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 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” 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 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” 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.