Skip to content

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.

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:

  1. The URL built from the Host header.
  2. If the hostname from the TLS handshake (or, for plain HTTP, the connection’s destination host) differs from the Host header, the URL built from it.
  3. 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.
[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 header

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

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

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, ws or wss URL; 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.

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-continue gets 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.

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?://host or ^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’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.

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 the i flag 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 reject
request if ${url} ~= /^https:\/\/api\.example\.com\/splash/i then reject_dict

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

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