URL Rewrite
URL rewrite rules act on a request’s URL: they can answer it locally with an empty or placeholder response (reject), redirect it (302 / 307), or quietly change the path the server receives (transparent rewrite). They apply to decrypted HTTPS hosts and to plain HTTP.
How URLs are matched
Section titled “How URLs are matched”For each request MeowX builds the URL from the scheme (https for decrypted connections, http for plain HTTP), the Host header and the request path. A port is added only if the Host header doesn’t already include one and the connection uses a non-default port. Hostnames are lowercased.
To match the way Surge does, a rule is tested against up to three URLs, and a match on any of them counts:
- The URL built from the
Hostheader. - If the hostname from the TLS handshake (or, for plain HTTP, the connection’s destination host) differs from the
Hostheader, the URL built from it. - If the request line contains an absolute URL, that URL.
Further details:
- Patterns are ICU regular expressions and are case-sensitive, as in community modules. Escape dots and slashes:
^https:\/\/ad\.example\.com\/. - Every rule, including rules that act on the response, is matched against the original URL the app requested, even if a transparent rewrite changed what is sent upstream.
- Only the first matching URL rewrite applies. The reject family, 302 and 307 end processing anyway; for transparent rewrites MeowX also stops at the first match, as Surge does.
- Map Local is only checked when no URL rewrite answered the request locally.
Surge [URL Rewrite]
Section titled “Surge [URL Rewrite]”[URL Rewrite]# regex replacement type^https:\/\/ad\.example\.com\/ - reject^https:\/\/api\.example\.com\/v1\/splash - reject-dict^https?:\/\/example\.com\/old\/(.*) https://example.com/new/$1 302^https:\/\/api\.example\.com\/v1\/feed https://api.example.com/v2/feed headerEach line is regex replacement type. For the reject family the replacement is a placeholder such as -. The two-word form regex reject (with any reject type) is also accepted. If only two words are given and the second isn’t a reject type, it is read as a header rewrite. Patterns may be wrapped in double quotes.
| Type | Result |
|---|---|
reject |
404 with an empty body |
reject-200 |
200 with an empty body |
reject-img, reject-tinygif |
200 with a 1×1 transparent GIF (image/gif) |
reject-dict |
200 with {} (application/json) |
reject-array |
200 with [] (application/json) |
302, 307 |
Redirect to the replacement URL. $1, $2 … insert capture groups. |
header |
Transparent rewrite (see below). This is the default type. |
Replacement semantics
Section titled “Replacement semantics”For 302, 307 and header, only the part of the URL that the pattern matched is replaced, and the rest is kept. A pattern ending in .* therefore replaces the whole URL, while one without it keeps the remaining path:
| Rule | Request | Result |
|---|---|---|
^https:\/\/a\.com\/old https://a.com/new 302 |
https://a.com/old/page?x=1 |
https://a.com/new/page?x=1 |
^https:\/\/a\.com\/old.* https://a.com/new 302 |
https://a.com/old/page?x=1 |
https://a.com/new |
Transparent rewrites
Section titled “Transparent rewrites”A transparent rewrite changes the URL sent to the server without the app noticing. MeowX supports same-host transparent rewrites: the new URL must keep the scheme, host and port of the original connection (ws and wss count as http and https). The path and query can change freely, and the Host header is updated.
- The target must be an
http,https,wsorwssURL; otherwise the rewrite is discarded. - A rule whose target is clearly on a different host is reported as “Cross-host transparent rewrite not supported” when the module is compiled.
- If the target can only be known at run time (for example it is built from capture groups) and turns out to be another host, the request is forwarded unchanged and diagnostics records “Cross-host rewrite not supported”. Use a 302 instead.
Surge’s built-in {{{GATEWAY_ADDRESS}}} placeholder isn’t supported.
Local responses
Section titled “Local responses”Rejects, redirects and Map Local responses are generated by MeowX without contacting the server:
- If the request has a body, MeowX reads and discards it; if it is larger than 1 MB, the connection is closed after the response.
- A request with
Expect: 100-continuegets the final response straight away, and the connection is closed afterwards. - Over HTTP/2 the response is sent on that request’s stream; other requests on the same connection are not affected.
- Request records mark these as Local response · no upstream.
Hosts answered entirely locally
Section titled “Hosts answered entirely locally”When MeowX can tell from the rules that every request to a host will be answered locally, it doesn’t connect to the real server at all. That is the case for a host (port 443, no wildcards) when either:
- the first URL rewrite that could match it is a reject of any kind and covers the whole host (a literal
^https?://hostor^https?://host/.*with no path restriction), or - no URL rewrite could match it, and a Map Local rule covers the whole host in the same way.
This is typical for ad hosts. It costs nothing upstream and works even when the real server only supports TLS 1.2 (which otherwise can’t be decrypted). Any request to such a host that would need forwarding is answered with 502.
Loon plugins
Section titled “Loon plugins”Classic [Rewrite]
Section titled “Classic [Rewrite]”Loon’s classic syntax puts the type before the target: regex type [target].
[Rewrite]^https:\/\/ad\.example\.com\/ reject^https:\/\/api\.example\.com\/splash reject-dict^https?:\/\/example\.com\/old\/(.*) 302 https://example.com/new/$1^https:\/\/api\.example\.com\/v1\/feed header https://api.example.com/v2/feed| Type | Result |
|---|---|
reject |
Drops the connection (Loon’s meaning, not a 404). Over HTTP/2 only that request’s stream is reset. |
reject-200, reject-img, reject-tinygif, reject-dict, reject-array |
As in Surge |
302, 307 |
Redirect |
header |
Same-host transparent rewrite |
Because many plugins published for Loon are written in Surge order (regex target type) or Quantumult X order (regex url type target), MeowX recognizes those orders too. In arguments, write spaces as \x20. reject-video, mock-request-body and other types are unsupported. Header operations, mock-response-body and body operations in [Rewrite] are covered on the Header rewrite, Map Local and Body rewrite pages.
New syntax
Section titled “New syntax”Loon 3.5.1 introduced lines of the form request if ${url} ~= /regex/flags then action, placed in [Rewrite]. MeowX supports them when:
- the condition is exactly
${url} ~= /…/(the regex may use theiflag for case-insensitive matching); conditions combined with&&or||, or using other variables such as headers, are unsupported; - the action is one MeowX implements.
Reject actions (request phase only; a response … then reject line is unsupported):
| Action | Result |
|---|---|
reject |
Drop the connection |
reject(404) |
404, empty body |
reject(200) |
200, empty body |
reject_dict or reject_dict(200) |
200 with {} |
reject_array or reject_array(200) |
200 with [] |
reject_img or reject_img(200) |
200 with a 1×1 GIF |
[Rewrite]request if ${url} ~= /^https:\/\/ad\.example\.com\// then rejectrequest if ${url} ~= /^https:\/\/api\.example\.com\/splash/i then reject_dictHeader actions (request.header.add …), body and JSON actions (response.body.replace, response.json.jq …) and response.body.mock are described on the Header rewrite, Body rewrite and Map Local pages. Other actions, such as redirects or script(…), are unsupported and listed as such in the module’s details.
URL-REGEX rules in [Rule]
Section titled “URL-REGEX rules in [Rule]”A [Rule] line such as URL-REGEX,"^https:\/\/ad\.example\.com\/banner",REJECT can’t be decided when a connection is opened, so MeowX turns it into a URL rewrite. The pattern may be quoted (commas inside quotes are kept); without quotes, everything before the policy is the pattern.
| Policy | Result |
|---|---|
REJECT, REJECT-NO-DROP |
404, empty body |
REJECT-200 |
200, empty body |
REJECT-TINYGIF, REJECT-IMG |
200 with a 1×1 GIF |
REJECT-DICT |
200 with {} |
REJECT-ARRAY |
200 with [] |
REJECT-DROP |
Drop the connection |
DIRECT, PROXY, others |
Unsupported: choosing an outbound by URL isn’t possible |
