What Clash API Node Switching Should Actually Do

Automatic node switching is often described as “pick the fastest proxy,” but a useful Mihomo automation workflow needs a more careful definition of health. A node can return a fast ICMP-like delay and still fail HTTPS, reset long-lived connections, or become unreliable when several requests run at once. Conversely, a node with slightly higher latency may be the better choice if it completes repeated HTTPS checks without packet loss. The goal of a Clash API node switching script is therefore not to chase the smallest number on a speed test. It is to select a usable member of a proxy group, confirm that the member responds through the intended URL, and change the group only when the evidence justifies a switch.

Mihomo exposes a controller API that lets local tools inspect proxies, test delay, and select a member of a selector group. Desktop clients such as Clash Verge Rev, Mihomo Party, and other Mihomo-based interfaces usually expose this controller through a local address such as 127.0.0.1:9090, although the port and secret depend on your profile. The API does not create better nodes and it cannot repair an expired subscription. It simply gives your script the same control that you exercise manually when opening the proxy-group panel and clicking another node.

Before writing automation, decide which group should be controlled. A provider may define a top-level group named Proxy, 🚀 节点选择, or GLOBAL, while the actual node names appear underneath it. Changing a leaf node that is not referenced by your rules may produce a perfectly valid API response with no visible effect. The safest starting point is a dedicated selector group used by your important rules. Keep automatic selection separate from groups used for streaming, work services, or gaming until you have observed its behavior for several days.

Important distinction: a url-test group already performs built-in health-based selection. Use the API when you need custom thresholds, notifications, scheduled checks, maintenance windows, or selection logic that differs from Mihomo’s normal group behavior. Do not run an aggressive external switcher against the same group without a reason, or the two decision makers may continuously override each other.

Secure and Verify the Mihomo Controller

The controller endpoint is powerful because it can inspect configuration and change active proxies. Treat the secret as a local administrative credential, not as a harmless dashboard password. A controller bound to 0.0.0.0 can become reachable from your LAN, a virtual machine, a container network, or a Wi-Fi guest depending on firewall rules. If the secret is missing, some clients may accept requests without authentication. That is convenient for a one-minute local test and unsafe as a permanent automation design.

Open the active Mihomo configuration or the client’s controller settings and identify the external controller address and secret. Prefer a loopback bind such as 127.0.0.1:9090 when the script runs on the same machine. If a separate monitoring host must access it, bind to a private management interface, restrict the port with a firewall, use a long random secret, and avoid forwarding the controller through a public reverse proxy. HTTPS protects the transport but does not replace authentication or network restrictions.

First verify that the endpoint answers and that the script is using the correct authorization format. The usual request header is Authorization: Bearer YOUR_SECRET. A successful request to /version confirms that the port is reachable and that the response is coming from a Mihomo controller rather than an unrelated service.

export MIHOMO_API="http://127.0.0.1:9090"
export MIHOMO_SECRET="replace-with-a-long-random-secret"

curl --fail --silent --show-error \
  -H "Authorization: Bearer ${MIHOMO_SECRET}" \
  "${MIHOMO_API}/version"

Do not put the secret directly into a script committed to Git. Environment variables are acceptable for a personal cron job, while a systemd environment file, operating-system secret store, or restricted configuration file is preferable on a shared machine. Check file permissions and make sure shell history does not contain the secret. When debugging, print the HTTP status, selected group, and node name, but never print the authorization header or the complete subscription URL.

The next useful inspection is the proxy inventory. Mihomo returns a JSON object containing proxy names, types, current selections, and group members. Names can contain spaces, emoji, slashes, or non-Latin characters, so always pass them through a JSON encoder such as jq instead of constructing JSON by hand.

curl --fail --silent \
  -H "Authorization: Bearer ${MIHOMO_SECRET}" \
  "${MIHOMO_API}/proxies" |
  jq '.proxies["Proxy"] | {type, now, all}'

If the group name is not exactly Proxy, copy the key returned by the API or inspect the profile’s proxy-groups section. A 404 response usually means the group name is wrong, not that the node has failed. A 401 response means the controller rejected the secret. A connection refusal points to the wrong port, a stopped core, or a controller that is bound only to another address.

Build a Repeatable Health Check and Switch Script

A reliable script should separate discovery, testing, decision making, and switching. Start with a small candidate set rather than testing every proxy in a large subscription on every minute. Testing dozens of nodes creates traffic, consumes provider allowances, and can make the results less representative of normal use. Choose nodes from one region or transport family first, then expand the candidate list when you understand the provider’s limits.

The Mihomo delay endpoint can test a proxy against a URL and timeout. The exact route is commonly written as /proxies/:name/delay?url=..., with the proxy name URL-encoded. The response normally contains a delay value when the request succeeds. Your test URL should be stable, small, and available from the network location you care about. A generic HTTPS endpoint that returns quickly is usually more informative than a large download or a web page filled with third-party resources.

Here is a compact Bash example. It expects curl and jq, tests several named nodes, ignores failed responses, and selects the lowest delay below a defined threshold. The group is changed only after a candidate passes the test.

#!/usr/bin/env bash
set -u

API="${MIHOMO_API:-http://127.0.0.1:9090}"
SECRET="${MIHOMO_SECRET:?Set MIHOMO_SECRET first}"
GROUP="${MIHOMO_GROUP:-Proxy}"
TEST_URL="${MIHOMO_TEST_URL:-https://www.gstatic.com/generate_204}"
TIMEOUT="${MIHOMO_TIMEOUT:-5000}"
MAX_DELAY="${MIHOMO_MAX_DELAY:-1800}"

nodes=("Singapore 01" "Japan 02" "US West 01")

api_get() {
  curl --fail --silent --show-error \
    -H "Authorization: Bearer ${SECRET}" "$1"
}

best_name=""
best_delay=999999

for node in "${nodes[@]}"; do
  encoded=$(jq -rn --arg value "$node" '$value|@uri')
  result=$(api_get \
    "${API}/proxies/${encoded}/delay?url=$(jq -rn --arg u "$TEST_URL" '$u|@uri')&timeout=${TIMEOUT}" \
    2>/dev/null) || continue

  delay=$(jq -r '.delay // empty' <<< "$result")
  [[ "$delay" =~ ^[0-9]+$ ]] || continue

  if (( delay < best_delay && delay <= MAX_DELAY )); then
    best_delay="$delay"
    best_name="$node"
  fi
done

if [[ -z "$best_name" ]]; then
  echo "No healthy candidate found" >&2
  exit 1
fi

payload=$(jq -nc --arg name "$best_name" '{name:$name}')
curl --fail --silent --show-error \
  -X PUT \
  -H "Authorization: Bearer ${SECRET}" \
  -H "Content-Type: application/json" \
  --data "$payload" \
  "${API}/proxies/$(jq -rn --arg g "$GROUP" '$g|@uri')"

printf 'Selected %s with %s ms\n' "$best_name" "$best_delay"

This example is intentionally conservative in one important way: it does not switch merely because another node is one or two milliseconds faster. In real networks, delay varies from one test to the next. Add a margin, such as requiring the new node to be at least 15 percent faster, or require the current node to fail two or three consecutive checks before changing it. Otherwise a group can flap between two nearly equal nodes, interrupting downloads and breaking persistent connections.

For more control, query the group’s current now value before making the PUT request. If the current member is healthy and the candidate is only marginally better, keep the existing selection. You can also maintain a small state file containing the last failure count and last switch time. A cooldown of five to fifteen minutes prevents a temporary CDN delay from triggering repeated changes.

Remember that a successful API switch means Mihomo accepted the new group member; it does not prove that every application is now using that member. Rules may send traffic to another group, an application may bypass the system proxy, or an existing TCP connection may continue on its original route. Verify the result through the client’s connection panel or the API’s active connection data. A new request is a better test than refreshing a page that is already cached.

Schedule Checks Without Creating Flapping

Scheduling is where a useful script becomes an unreliable one if the interval is too short. A five-second loop can flood the controller and provider, while a daily check cannot react to a failing node. For ordinary desktop browsing, a check every five or ten minutes is a reasonable starting point. For a workstation running long downloads or API jobs, use a preflight check before the job and a slower background check during the job. Avoid changing nodes in the middle of a WebSocket session, SSH connection, database migration, or large upload unless continuity is less important than recovery.

On Linux or macOS, a cron entry can run the script periodically. Use an absolute path, redirect output to a dedicated log, and prevent overlapping executions. Two copies testing and switching at the same time can race: one process may select a node based on old results while another has already changed the group.

# Run every ten minutes, with a lock preventing overlapping jobs
*/10 * * * * flock -n /tmp/mihomo-switch.lock /usr/local/bin/mihomo-switch.sh \
  >> /var/log/mihomo-switch.log 2>&1

On Windows, Task Scheduler can launch the same logic through PowerShell, WSL, or a small Python program. Configure the task to run whether or not the user is actively signed in only when the controller and Mihomo core are also available in that session. A scheduled task that starts before the desktop client or service has opened its controller will report false failures. Add a short startup delay and test the controller before testing nodes.

Logging should answer four questions: when did the check run, which URL was tested, what result did each candidate return, and why was a switch made or skipped? Record status codes and delay values, but avoid logging credentials. A useful line might say that the current node failed twice, the candidate returned 420 ms, and the switch was accepted. That is far more actionable than a generic “proxy changed” message.

Use two signals when possible: combine the controller delay result with one real HTTPS request made through the mixed port. Delay testing answers whether Mihomo can reach the target through a proxy; an application-level request also reveals DNS behavior, TLS negotiation, HTTP status handling, and whether the client is actually configured to use Clash.

Notifications are optional but useful for unattended systems. Send a desktop notification, write to journald, or call a private webhook only after removing the controller secret from the message. Include the old node, new node, measured delay, and reason. Add a notification for “no healthy candidate” so a silent failover does not become a long outage that you discover only when a build or download has stopped.

Troubleshoot API Responses and Unexpected Behavior

When a script fails, begin with the HTTP layer rather than changing YAML randomly. A 401 Unauthorized response means the Authorization header or secret is wrong. A 404 Not Found commonly indicates an incorrect endpoint, an unencoded proxy name, or a group that does not exist in the active profile. A 400 Bad Request often points to malformed JSON or an invalid delay URL. A 408, 504, or a timeout from the delay endpoint means that candidate did not complete the test within the selected limit; it is not proof that the controller itself is down.

If the API returns a successful switch but the browser remains on the old route, inspect the rule path. A request may be matched by DIRECT, a special service group, or a rule provider group rather than the selector you changed. Check the active connection entry for the destination hostname and matched rule. If the connection does not appear at all, the application may bypass the system proxy, use its own DNS resolver, or require TUN mode. Terminal tools frequently need explicit variables such as HTTP_PROXY, HTTPS_PROXY, and ALL_PROXY, while some native applications require TUN capture.

DNS can make a healthy node appear broken. With fake-IP or redirection-based DNS, the hostname observed by the application and the hostname tested by your script may not follow the same path. Test the same domain and protocol that the failing application uses. A fast check against a small static endpoint does not guarantee that a large streaming API, a regional service, or an SNI-sensitive transport will work. Keep separate test URLs when your workload has distinct requirements.

Unexpected switching can also come from built-in Mihomo behavior. Review whether the selected group is select, url-test, fallback, or load-balance. A script that sends a PUT request to a dynamic group may be overwritten by the group’s own health logic on the next interval. If the group contains filters, hidden providers, or nested groups, the node name accepted by the API may not be a directly usable outbound. Inspect the JSON returned by /proxies before assuming the profile has the shape shown in a tutorial.

Finally, test one variable at a time. First call /version, then inspect /proxies, then test one known-good node, then test one candidate, and only after that issue a PUT request. Capture the response body with curl -i during a controlled test, but remove the secret before sharing logs. This sequence distinguishes a dead controller from a bad node, a bad node from a bad test URL, and a successful selection from an application that is not using Clash.

Compared with many generic proxy auto-switchers, which often hide their thresholds, require a separate daemon, or provide little documentation for controller failures, a hand-built Mihomo script makes every decision visible: the tested URL, timeout, failure count, cooldown, and selected group can all be reviewed and adjusted. Clash Official Site is useful in this scenario because its client guidance connects the API workflow with practical profile, TUN, rule, and log checks instead of treating node switching as a magic speed button. If you want a manageable starting point before scheduling your first health check, download a Clash client and verify manual group switching first; once that works, automation becomes a controlled extension rather than another unknown variable.