Distinguish Full Subscriptions from Node Providers
When a subscription update fails, first identify what is being downloaded and by which component. A GUI client's subscription updater and mihomo's proxy-providers are separate mechanisms: the former typically imports a complete configuration, while the latter refreshes only a set of proxy nodes. Working nodes don't mean the subscription URL is currently reachable, and a successful download doesn't guarantee that every node in it works.
- Client subscription: May include a complete configuration with
dns,proxy-groups,rules, and other settings. The client manages its update schedule; its automatic-update option is separate from the kernel Provider timer. - Node Provider: Read by mihomo at runtime. The remote content must use a node-list format it supports. This article uses YAML with a top-level
proxies:key; policy groups reference it withuse. - Health checks: Send requests to a test URL to check node reachability and latency. They don't download subscriptions or fix expired subscription credentials.
Pinpoint the Error: Check HTTP, Then Parsing, Then Nodes
Save the log from a failed update and note the time, request hostname, and final error message. Don't rely on a screenshot that only says “Update failed.” If the log includes subscription query parameters, redact the token before sharing it. The errors below occur at different stages and need different fixes.
| Log or status | Check first | Next step |
|---|---|---|
no such host |
Subscription hostname resolution | Run getent and check the hostname for typos |
i/o timeout、context deadline exceeded |
Connection route, egress, and timeout stage | Test direct and local-proxy connections separately |
x509: certificate has expired |
Local clock and server certificate validity | Correct the clock, then check the site's certificate |
certificate signed by unknown authority |
System CA certificates and corporate HTTPS inspection | Verify the source of trusted root certificates |
| HTTP 401 / 403 | Token, access policy, and User-Agent | Check the provider's instructions and response body |
| HTTP 429 | Request rate limit | Wait for Retry-After and increase the interval |
yaml: unmarshal errors |
Response format, field types, and indentation | Check whether the response is HTML or an error message |
A Timeout Doesn't Necessarily Mean a Node Is Down
The subscription hostname may only be reachable on certain networks, or requests may be throttled at the proxy egress. HTTPS_PROXY in a Linux terminal may differ from the desktop system proxy, and a client's updater may use different settings too. So a site opening in your browser doesn't prove that background subscription requests take the same route.
The commands below prompt for the subscription URL so the token isn't written directly to shell history. The response is saved in a restricted-permission temporary directory. They don't follow redirects or print the subscription body. If you get a 301 or 302, inspect the redirect target locally and verify its source before accessing it.
umask 077
workdir=$(mktemp -d)
read -r -s -p 'Subscription URL: ' SUB_URL
printf '\n'
curl --noproxy '*' --connect-timeout 10 --max-time 30 \
-sS -D "$workdir/headers.txt" \
-o "$workdir/body.txt" \
-w 'HTTP=%{http_code} total=%{time_total}s\n' \
"$SUB_URL"
getent ahosts example.com
Replace example.com in the final line with the hostname from your subscription URL—hostname only, without the scheme or token. --noproxy '*' bypasses curl's explicit proxy settings. If TUN mode or a transparent router proxy is enabled, traffic may still be intercepted, so this result doesn't prove a direct physical connection.
Once you've confirmed that the kernel is listening on mixed-port 7890, compare with an explicit HTTP proxy request. The command below reuses the variable from the previous step. Its output reports the actual status of this request; it doesn't assume success.
curl --noproxy '' --proxy http://127.0.0.1:7890 \
--connect-timeout 10 --max-time 30 \
-sS -D "$workdir/proxy-headers.txt" \
-o "$workdir/proxy-body.txt" \
-w 'HTTP=%{http_code} total=%{time_total}s\n' \
"$SUB_URL"
# Delete the temporary file containing subscription data after checking it locally
unset SUB_URL
rm -rf -- "$workdir"
unset workdir
Certificate Errors, Rejected User-Agents, and Incompatible Formats
Certificate Errors: Check the Clock; Don't Disable Verification
Run timedatectl status and check the date, time zone, and synchronization status. Certificates are validated against absolute time; a different time-zone display alone doesn't cause a certificate to expire. Once the clock is correct, check that the system CA bundle is installed, whether a corporate network is replacing HTTPS certificates, and whether the subscription service's certificate has expired.
On Debian and Ubuntu, check that the ca-certificates package is installed. If an administrator provides a corporate root certificate, install it following your distribution's procedure and verify its source. Don't use curl -k or disable certificate verification as a permanent fix. Subscription content contains connection credentials, and skipping verification increases the risk of tampering.
401 or 403? Check Credentials Before Changing the User-Agent
A 401 often means the token has expired. A 403 may also be caused by account status, IP restrictions, User-Agent policies, or a protection page. Follow the provider's response; don't keep changing the User-Agent just because you see a 403. An HTTP 200 in a browser may return a login page, not a valid subscription.
If the provider explicitly requires a particular mihomo User-Agent, set the request header in the Provider. Each value under header must be a string list. Add the following field to the relevant HTTP Provider. It doesn't replace account authorization and must not be used to bypass access controls.
header:
User-Agent:
- mihomo
Parse Errors: Check the Response Body, Not Just the File Extension
A subscription URL ending in .yaml doesn't guarantee that the server returns YAML. Check the response headers and the start of the body locally. An HTML protection page, a JSON error, a Base64-encoded node list, a full configuration, and a Provider node list are different data formats. HTTP 200 only means the request succeeded at the HTTP level.
The Provider response used in this article should have the following top-level structure. The example contains no real nodes; proxies: [] only illustrates the field type. In practice, the service must return a populated node list. Don't use a full configuration as a node Provider, and don't expect its rules to merge into the main configuration automatically.
proxies: []
- Each Provider name in the main configuration must be unique. Choose a stable name, such as
airport. - YAML uses spaces for indentation, not tabs. Quote the URL to avoid ambiguity from special characters.
- Convert node formats in a trusted environment. Don't submit tokenized subscriptions to an unknown conversion service.
Scheduled proxy-providers Refresh: Fields and References
This configuration shows how the pieces fit together: fetch the node list every 3600 seconds and run health checks every 600 seconds. The PROXY policy group references the nodes with use: [airport]; the final rule sends unmatched traffic to that group. This isn't a ready-to-use subscription. example.com is a documentation-only hostname—replace it with the Provider URL from your service before use.
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
proxy-providers:
airport:
type: http
url: "https://example.com/subscription.yaml"
path: ./providers/airport.yaml
interval: 3600
health-check:
enable: true
url: "https://www.gstatic.com/generate_204"
interval: 600
timeout: 5000
lazy: true
proxy-groups:
- name: PROXY
type: select
use:
- airport
rules:
- MATCH,PROXY
| Field | Example value | Purpose and units |
|---|---|---|
interval |
3600 | Remote Provider refresh interval, in seconds |
path |
./providers/airport.yaml |
Local cache path relative to the kernel working directory |
health-check.interval |
600 | Node health check interval, in seconds |
health-check.timeout |
5000 | Timeout for each check, in milliseconds |
health-check.lazy |
true | Skip periodic checks when unused to reduce probes |
health-check.url should be a lightweight URL that reliably returns the expected response. The URL above is a common example, but it may not be reachable from every egress. If the test URL is down, many healthy nodes may appear faulty at once. Check the probe target before removing nodes in bulk.
Which Egress Does the Update Request Use?
The current mihomo HTTP Provider supports a proxy field to choose a proxy or policy group for downloads. If it's unset, don't assume the Provider will use the desktop system proxy. If the subscription requires a proxy, make sure that egress can be established independently before the first subscription download.
Don't set the refresh interval too low. 3600 seconds is one hour, which is usually more appropriate than polling every minute. Follow your provider's limits. Each health check may make requests for multiple nodes, so more nodes mean more requests overall. Shortening the check interval won't make the subscription service update faster.
Apply the Configuration: Validation, Permissions, and Restart Order
First, confirm how the kernel is running. A GUI client may regenerate its runtime configuration and overwrite manual changes the next time you switch subscriptions, so use the client's supported persistent-override mechanism. When running mihomo independently, check the actual startup arguments for -d and -f. Don't edit a similarly named file that isn't being loaded.
- Back up the current configuration and keep a restorable copy.
- Add the Provider and policy group to the main configuration, and make sure the
usevalue exactly matches the Provider name. - Create the cache directory and ensure the user running the kernel can write to it.
- Validate the configuration and restart the service only if validation passes.
- Check the startup and subsequent update logs for download, parsing, or permission errors.
The steps below assume the working directory is ~/.config/mihomo, the configuration file is config.yaml, and a systemd user service named mihomo.service already exists. For a system-level service, adjust the configuration directory and user scope in the commands to match your deployment.
cp ~/.config/mihomo/config.yaml \
~/.config/mihomo/config.yaml.bak
mkdir -p ~/.config/mihomo/providers
chmod 700 ~/.config/mihomo/providers
chmod 600 ~/.config/mihomo/config.yaml
mihomo -t -d ~/.config/mihomo \
-f ~/.config/mihomo/config.yaml
# Run only after validation passes
systemctl --user restart mihomo.service
journalctl --user -u mihomo.service -n 100 --no-pager
mihomo -t validates the configuration, but doesn't prove that every remote resource and node works. If validation passes but updates still fail, check the actual runtime logs. For permission denied, check the directory owner, service user, and cache path. Don't make the entire configuration directory writable by every user.
If a Provider cache exists, old nodes may keep working after an update fails. Seeing nodes in the UI doesn't mean the refresh succeeded. Check the logs and run stat ~/.config/mihomo/providers/airport.yaml to inspect the cache. The modification time is only a clue; it doesn't prove that the remote content changed.
FAQ: Why Isn't Auto-Refresh Working?
Will the interval keep running after I close the client?
This YAML won't automatically restart a stopped kernel. Scheduled Provider refreshes require mihomo to keep running; sleep, a stopped service, or a lost network connection can prevent them. For a persistent Linux deployment, configure systemd to match your setup. A successful manual start doesn't mean the service is enabled at boot.
Why didn't my rules or DNS change after the subscription updated?
proxy-providers updates the node list, not the entire main configuration. You can use rule-providers separately for rule sources, but rule sets must also be referenced in rules. To replace the whole configuration, use the client's subscription update flow and check the priority of local overrides. See the configuration reference for how the fields fit together.
Why can't I access a particular site if the health check passes?
A health check tests only its target, not every website. A specific site may also be affected by DNS resolution, rule matching, egress region, or restrictions on the destination service. Check the policy group and node actually selected, then troubleshoot the destination hostname. One successful probe doesn't mean the entire connection path is working.
How can I tell when the issue is fixed?
- The subscription request returns valid content, not a login page or error body.
- The runtime log shows no Provider parsing errors, and the policy group lists the expected nodes.
- A first launch can fetch the subscription without relying on an old cache.
- Wait one configured interval, then check whether another refresh occurs.
- Settings persist after restarting the client, and tokens and node credentials don't appear in public logs.
Follow the same order every time: download path → HTTP status → content format → Provider reference → scheduled refresh → node checks. This separates “can't fetch the subscription” from “nodes don't work” and avoids reinstalling the client without addressing the actual issue. For a first-time setup, follow the quick start guide to create a minimal configuration, then add automatic refresh settings one at a time.