Skip to main content

Rewrite

Rewrite modifies HTTP requests or responses when specified conditions match. It can also return a redirect, reject response, or mock data immediately.

This page describes the new syntax supported since Loon 3.5.1 (978).

Scope

Rewrite applies only to HTTP traffic and HTTPS traffic decrypted through MitM. It runs before rule matching.

Visual builder

Use the Rewrite Builder to combine conditions and actions, then copy the generated configuration.

Quick start

Basic format:

<phase> if <condition> then <action> [| <action> ...]

Add a request header:

http-request if ${url} ~= /^https:\/\/api\.example\.com/ then request.header.set(name="X-Loon", value="true")

Modify a JSON response:

http-response if ${url} ~= /^https:\/\/api\.example\.com\/profile$/ && ${response.status} == 200 then response.json.replace(path="data.vip", value=true)

Join multiple actions with |. They run from left to right:

http-request if ${url} ~= /^https:\/\/api\.example\.com/ then request.header.set(name="X-Loon", value="true") | request.header.delete(name="Cookie")

Each Rewrite entry must be written on one line.

Phases

PhaseTimingAvailable data
http-requestBefore the request is sentURL, request method, request headers
http-responseAfter response headers are receivedRequest data, response status, response headers

Request and response changes must be configured separately:

http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.set(name="X-Test", value="request")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.header.set(name="X-Test", value="response")

Although response.body.mock(...) creates a response, it returns that response during the request phase and can therefore be used only with http-request.

Conditions

Comparison operators

OperatorDescription
==Exact equality
~=Regular-expression match
http-request if ${request.method} == "POST" then request.header.set(name="X-Method", value="POST")
http-response if ${response.header['Content-Type']} ~= /^application\/json(?:;|$)/i then response.header.set(name="X-JSON", value="true")

~= searches for a matching substring by default. Use ^ and $ to match the complete value.

Logical operators

OperatorDescription
&&AND
||OR
()Change precedence
http-request if ${request.method} == "POST" && (${request.header['X-Region']} == "CN" || ${request.header['X-Region']} == "HK") then request.header.set(name="X-Matched", value="true")

Precedence:

comparison operators > && > ||

Add parentheses when an expression uses both && and ||.

Variables

Every dynamic value uses ${...}:

SourceExample
Built-in variable${url}
Plugin argument${region}
Regular-expression capture${item.1}

Built-in variables

VariableTypeRequest phaseResponse phase
${url}String
${request.method}String
${request.header['name']}String or null
${response.status}Number
${response.header['name']}String or null

Header names are case-insensitive:

${request.header['content-type']}
${request.header['Content-Type']}

Response variables cannot be used during the request phase. The current version also does not support reading request or response bodies in a condition.

Plugin arguments

Declare arguments in the plugin's [Argument] section:

[Argument]
enabled = switch,true,tag=Enabled
price = input,9.99,type=number,tag=Price
region = select,"CN","US","JP",tag=Region

Reference them directly in Rewrite:

[Rewrite]
http-response if ${enabled} == true && ${request.header['X-Region']} == ${region} then response.json.replace(path="data.price", value=${price})
ControlSupported typesDefault type
inputString, NumberString
selectString, NumberString
switchBooleanBoolean

Use type=number when an input or select must return a number:

price = input,9.99,type=number
level = select,1,2,3,type=number

Arguments are data only. They cannot create new conditions or actions, and their values are not expanded a second time.

Regular-expression captures

Save a match with as <name>:

http-request if ${url} ~= /^https:\/\/api\.shop\.com\/item\/(\d+)/ as item then request.header.set(name="X-Item-ID", value="${item.1}")
VariableContent
${item.0}Complete match
${item.1}First capture group
${item.2}Second capture group

Restrictions:

  1. as can be used only with ~=.
  2. Each capture name must be unique within one Rewrite entry.
  3. A capture name cannot be the same as a plugin argument.
  4. A capture index cannot exceed the number of capture groups in the regular expression.
  5. The capture condition must run on every successful path and cannot be placed in an optional branch of ||.

Valid:

http-request if (${request.method} == "GET" || ${request.method} == "POST") && ${url} ~= /item\/(\d+)/ as item then request.header.set(name="X-Item", value="${item.1}")

Invalid:

http-request if ${url} ~= /item\/(\d+)/ as item || ${request.header['X-Debug']} == "true" then request.header.set(name="X-Item", value="${item.1}")

If an optional capture group does not match, an action that references it fails and is skipped. Later actions continue to run.

Values and strings

Literals

TypeExample
String"hello world"
Number200, 9.99
Booleantrue, false
Nullnull
Regex/^https:\/\/example\.com/i

Fixed strings must use double quotes. These two values have different types:

value=9.99      # Number
value="9.99" # String

Regular expressions

Format:

/pattern/flags

Supported flags:

FlagDescription
iIgnore case
mMultiline mode
sLet . match line breaks

${...} is not expanded inside a regular-expression literal. To supply a pattern through a plugin argument, place the variable on the right side of ~=:

http-request if ${url} ~= ${urlPattern} then request.header.set(name="X-Matched", value="true")

Double-quoted strings

Double-quoted strings support variables:

request.header.set(name="X-Info", value="price=${price}, region=${region}")

Supported escapes:

SyntaxResult
\"Double quote
\\Backslash
\nLine feed
\rCarriage return
\tTab
\${Literal ${

Raw strings

Use backticks for fixed JSON or HTML to reduce escaping:

http-request if ${url} ~= /^https:\/\/api\.example\.com/ then response.body.mock(type="json", data=`{"code":0,"message":"ok"}`, status=200)

Raw strings do not process escapes or expand ${...}. Two consecutive backticks represent one literal backtick.

Use a double-quoted string when variables are needed:

http-request if ${url} ~= /^https:\/\/api\.example\.com\/item\/(\d+)/ as item then response.body.mock(type="json", data="{\"item\":\"${item.1}\"}", status=200)

The new syntax does not split a line at spaces, so \x20 is unnecessary.

Actions

Every action uses named arguments:

action(name=value, name=value)

Action names and argument names cannot use variables.

URL

url.replace

Run a regular-expression replacement on the complete URL:

http-request if ${url} ~= /^http:\/\/example\.com/ then url.replace(pattern=/^http:\/\/example\.com/, replacement="https://api.example.com")
ArgumentTypeDescription
patternRegexReplacement pattern
replacementStringReplacement text

Only the matched range is replaced; the rest of the URL is preserved.

redirect

http-request if ${url} ~= /^http:\/\/example\.com/ then redirect(status=302, location="https://new.example.com")
ArgumentTypeDescription
statusNumber302 or 307
locationStringText that replaces the URL pattern's matched range

When redirect is used, the condition must contain exactly one required URL regular expression.

Reject

http-request if ${url} ~= /^https:\/\/example\.com\/ads/ then reject(status=200, body="json-object")

Supported combinations:

statusbodyResponse
404"empty"Empty body
200"empty"Empty body
200"image"1×1 GIF
200"json-object"{}
200"json-array"[]
200"video"Blank video

Headers

Request headers:

http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.add(name="X-Loon", value="true")
http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.set(name="User-Agent", value="Loon")
http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.delete(name="Cookie")
http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.replace(name="User-Agent", pattern=/iPhone OS \d+/, replacement="iPhone OS 18")

Response headers:

http-response if ${url} ~= /^https:\/\/example\.com/ then response.header.add(name="X-Loon", value="true")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.header.set(name="Cache-Control", value="no-cache")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.header.delete(name="Set-Cookie")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.header.replace(name="Content-Type", pattern=/; charset=.+$/i, replacement="")
ActionArguments
*.header.addname=String, value=String
*.header.setname=String, value=String
*.header.deletename=String
*.header.replacename=String, pattern=Regex, replacement=String

Body regular-expression replacement

http-request if ${url} ~= /^https:\/\/example\.com/ then request.body.replace(pattern=/"price":\s*[0-9.]+/, replacement="\"price\":9.99")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.body.replace(pattern=/"enabled":\s*false/, replacement="\"enabled\":true")

Arguments:

pattern=Regex, replacement=String

JSON

Request JSON:

http-request if ${url} ~= /^https:\/\/example\.com/ then request.json.add(path="data.price", value=9.99)
http-request if ${url} ~= /^https:\/\/example\.com/ then request.json.delete(path="data.ads")
http-request if ${url} ~= /^https:\/\/example\.com/ then request.json.replace(path="data.price", value=${price})
http-request if ${url} ~= /^https:\/\/example\.com/ then request.json.jq(filter=".data.ads = []")

Response JSON:

http-response if ${url} ~= /^https:\/\/example\.com/ then response.json.add(path="data.price", value=9.99)
http-response if ${url} ~= /^https:\/\/example\.com/ then response.json.delete(path="data.ads")
http-response if ${url} ~= /^https:\/\/example\.com/ then response.json.replace(path="data.price", value=${price})
http-response if ${url} ~= /^https:\/\/example\.com/ then response.json.jq(file="response-filter.jq")
ActionArguments
*.json.addpath=String, value=Any
*.json.deletepath=String
*.json.replacepath=String, value=Any
*.json.jqfilter=String or file=String

JSON actions run only when the body is valid JSON. Key paths use dot notation and [n] for array indexes:

data.apps[0].appName

value can be a String, Number, Boolean, null, or variable.

Mock body

Mock a request body:

http-request if ${url} ~= /^https:\/\/example\.com/ then request.body.mock(type="json", data=`{"price":9.99}`)
http-request if ${url} ~= /^https:\/\/example\.com/ then request.body.mock(type="json", file="request_body.json")

Mock a response body:

http-request if ${url} ~= /^https:\/\/example\.com/ then response.body.mock(type="json", data=`{"code":0}`, status=200)
http-request if ${url} ~= /^https:\/\/example\.com/ then response.body.mock(type="json", file="response_body.json", status=200)
ArgumentDescription
typeBody type
dataData written directly
fileRead from a plugin file; mutually exclusive with data
base64Whether the data is Base64 encoded; defaults to false
statusResponse status code; defaults to 200; response only

Supported type values:

json, text, css, html, javascript, plain,
png, gif, jpeg, tiff, svg, mp4, form-data

Use file for medium or large data.

Execution rules

Multiple Rewrite entries

Within one phase, every matching Rewrite entry runs in configuration order:

http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.set(name="X-Value", value="first")
http-request if ${url} ~= /\/api\// then request.header.set(name="X-Value", value="second")

For a request to https://example.com/api/user, the final value is:

X-Value: second

Source priority is local configuration > plugins. Entries in the same file run from top to bottom.

Multiple actions

Actions in one Rewrite entry run from left to right in | order.

If an action fails at runtime:

  1. Keep changes made by earlier actions.
  2. Skip the current action and record the error.
  3. Continue with later actions.

Invalid argument names, argument types, or regular expressions are configuration errors and cause the complete Rewrite entry to be rejected during loading.

New and legacy syntax

The legacy syntax remains compatible and can be mixed with the new syntax:

[Rewrite]
^https://example\.com header-add X-Order old
http-request if ${url} ~= /^https:\/\/example\.com/ then request.header.set(name="X-Order", value="new")

Both syntaxes enter the same execution sequence and run in configuration order. Loon does not rewrite old configurations automatically.

Common migration mappings:

Legacy actionNew action
headerurl.replace(...)
302, 307redirect(...)
reject-*reject(...)
header-*request.header.*
response-header-*response.header.*
request-body-replace-regexrequest.body.replace(...)
response-body-replace-regexresponse.body.replace(...)
request-body-json-*request.json.*
response-body-json-*response.json.*
mock-request-bodyrequest.body.mock(...)
mock-response-bodyresponse.body.mock(...)

Complete example

[Argument]
enabled = switch,true,tag=Enabled
price = input,9.99,type=number,tag=Price
region = select,"CN","US","JP",tag=Region

[Rewrite]
http-request if ${enabled} == true && ${request.method} == "GET" && ${url} ~= /^https:\/\/api\.shop\.com\/item\/(\d+)/ as item && ${request.header['X-Region']} == ${region} then request.header.set(name="X-Item-ID", value="${item.1}") | request.header.set(name="X-Region", value="${region}")

http-response if ${enabled} == true && ${url} ~= /^https:\/\/api\.shop\.com\/item\/(\d+)/ as item && ${response.status} == 200 then response.json.replace(path="data.price", value=${price}) | response.header.set(name="X-Rewritten-Item", value="${item.1}")

Configuration validation

When loading the configuration, Loon checks:

  • Whether referenced arguments and captures exist.
  • Whether capture names are duplicated or conflict.
  • Whether a capture index is out of range.
  • Whether variables and actions are available in the current phase.
  • Whether action argument names and types are valid.
  • Whether regular expressions compile.

An error includes the line number and reason:

Rewrite line 18: undefined argument ${price2}
Rewrite line 21: capture item has only 2 groups; ${item.3} is invalid
Rewrite line 25: ${response.status} is unavailable during http-request