Fix Clash Dashboard Not Loading: external-controller & external-ui Checklist Steps
What This Guide Addresses Key Takeaways
This article stands apart from sprawling tutorials on global mode, tun stacks, sniffers, or rule providers. Those documents matter elsewhere; here only the diagnostic slice around external-controller, optional external-ui, and localhost ergonomics earns space.
You already ticked every box inside the tray app description: external controller enabled, maybe a bundled Yacd or Metacubexd button, REST port remembered as 9090. Then you open your browser at localhost and hear silence—connection refused, a forever spinner, or a blank white rectangle where the SPA are best off hydrate. Meanwhile Clash itself proxies ordinary sites just fine because normal traffic never touches the Mihomo REST surface.
Misery loves company: this symptom is one of the the greater part of common help-channel topics, yet the fix rarely lives inside subscription URLs or GEOIP tweaks. Instead you need disciplined inspection of bind addresses, YAML field placement, mixin merge precedence, Mixed Content borders between HTTPS dashboards and plaintext APIs, accidental double definitions of external-controller, and—only after the socket truly listens—the usual firewall chatter.
The key point:Where another piece already dives deep—for example resolving “port already in use” collisions—we point there instead of pasting duplicated steps.
Mental Model: Three Separate Layers in Brief
Separate three ideas before you poke YAML. Layer one is listening: Mihomo binds a TCP port for its REST shim. Layer two is serving HTML: embedded static UI bundles (when external-ui resolves) or detached front ends you host yourself—both ultimately call the same REST port for JSON.
Layer three is trust: secrets, bearer headers, Mixed Content blocking, LAN exposure, accidental exposure of plaintext tokens to foreign Wi-Fi—all security topics sitting above raw connectivity. Symptoms can fail at layer one (127.0.0.1 rejects because nobody listens yet) while subscription sync still works; never assume “internet works ⇒ dashboard works.” Conversely, fixing layer one does nothing if Mixed Content shackles scripts at layer three.
Step 1 — Confirm the REST Endpoint With curl A Closer Look
When curl fails, open the Mihomo runtime log—not the glossy GUI façade—and read the earliest lines mentioning external-controller or RESTful API.
Forget the flashy UI for ninety seconds and ask the simplest question on earth: does http://CONTROLLER/version answer?
Let me put it this way:Suppose your eventual goal is controller at loopback port 9090. From the same OS user session that launches the tray app, execute:
curl -sS http://127.0.0.1:9090/version
If you did not enable TLS or custom API base paths, plaintext HTTP suffices for the first probe. Immediate refusal strongly suggests Mihomo either failed to bind, bound another port, snapped to IPv6-only [::1], or your curl command targets wrong digits. Immediate JSON output means REST is healthy—you will later chase browser SPA issues separately.
Concretely:Curl faults referencing connection reset versus timeout still differ: resets often mean RST from firewall policy; timeouts mean routing black holes or captive intermediaries rewriting traffic—rare strictly for loopback, but imaginable across containers.
Modern cores print the finalized listen directive after mixin merges. Capture that exact host:port string; treat it as your single ground truth regardless of fuzzy memory.
Step 2 — external-controller Grammar and Placement What Matters
Multiple definitions of external-controller silently fight: later merges win or silently drop depending on tool-specific patch behavior. Inspect final effective config produced by GUIs—they often concatenate base profile + mixin + remote patch.
YAML tolerates indentation, yet humans stumble more than parsers.
Where this lands:Typical minimal keys sit at profile root—not nested unintentionally beneath tun: or dns: unless you knowingly extend provider-specific overlays. Canonical shape resembles:
external-controller: 127.0.0.1:9090
secret: "replace-me"
A fair few templates abbreviate:
Do not skip this:external-controller: :9090
That shorthand listens on every front end—functionally akin to binding 0.0.0.0 for IPv4 plus visible dual-stack combos depending on implementation. Convenience trades away safety: every device on reachable networks may attempt hits unless firewalls constrain.
Narrowing voluntarily to loopback minimizes accidental exposure behind coffee-shop Wi-Fi. If shorthand caused confusion (“which address are best off I bookmark?”), revert to explicit 127.0.0.1:9090 until understanding returns.
The key point:Surprise removal after remote sync commonly explains regressions occurring “Friday only.” Snapshot your effective rendered profile after each remote pull when debugging ghost REST outages.
Step 3 — external-ui Versus Third-Party Hosting Key Takeaways
When you consciously host Yacd or Metacubexd on a standalone static HTTP server—even local Python http.server—you still instruct the SPA where the API lives via environment parameters or bundled config fields. Serving UI at HTTPS while the API listens plain HTTP on localhost explodes Mixed Content shields; we dissect that mismatch next.
external-ui points Mihomo toward a filesystem directory hosting static SPA assets packaged with your distribution—or downloaded separately for air-gapped machines.
Typical relative paths appear small—just a handful of ASCII characters—but they must correctly resolve from the Mihomo binary working directory, not whichever folder your editor last opened.
Embedding via /ui route from Mihomo avoids cross-origin juggling when both originate same scheme and host—but not every GUI exposes that route cleanly. Document whichever combination you stabilized so future reinstalls mimic it verbatim.
Step 4 — Mixed Content: HTTPS Panels Calling HTTP localhost in Brief
Workable exits include: switching to an HTTP-hosted panel copy on localhost (same scheme), tunneling localhost through HTTPS via reverse proxy tricks—overkill domestically—or using native embedded UI fetched over HTTP from Mihomo (http://127.0.0.1:9090/ui).
Let me put it this way:Browsers escalate Mixed Content ruthlessly modern versions. Symptoms look like stalled network rows inside Developer Tools labelled blocked:mixed-content. Common pattern: bookmarking official public Yacd hosting over HTTPS (https://yacd.haishan.me style deployments) yet pointing secrets at http://127.0.0.1:9090.
The SPA loads over HTTPS, then JavaScript forbids XMLHttpRequest downgrade to plaintext unless explicitly exempted—which typical corporate policies forbid.
Another pragmatic approach: temporarily open Mihomo-managed controller over HTTPS experimental flags—few maintainers advertise trustworthy ergonomics though. When diagnosis matters more than glossy polish, work two browser profiles: Chromium with temporarily relaxed mixed-content instrumentation is dangerous; prefer structural alignment fairly than weakening global security sliders.
Step 5 — IPv4 localhost Versus IPv6 (::1) A Closer Look
You typed http://localhost:9090 believing universality yet the OS resolver maps localhost to ::1 first while Mihomo listens only IPv4-specific 127.0.0.1. Connection refused echoes ensue—not because controllers died, merely address family divergence.
Concretely:Narrow fix: always curl and browse explicit 127.0.0.1 during setup, or widen bind family with dual-stack listening if Mihomo exposes that toggle. Conversely if you bind [::1]:9090, IPv4 curls fail until matched. Treat address families as picky puzzle pieces snapping only when aligned symmetrically.
Step 6 — When the Controller Port Matches Another Daemon What Matters
Free the port by retiring the offending service or remap external-controller.
Symptoms overlap brute-force refusal: Mihomo refuses spin up or quietly suppresses REST while leaving proxy listeners alive when partial startup paths exist—but often the entire tray shows red. Before spiraling imaginative firewall fantasies, confirm another process latched TCP 9090.
Our dedicated Windows-centric walkthrough Fix Clash Port in Use on Windows explains netstat, PID mapping, and YAML changes without repeating LAN policy nuance unrelated here. Cross-platform analogues behave similarly:
Where this lands:sudo lsof -iTCP:9090 -sTCP:LISTEN on macOS or Linux.
Upgrade every downstream consumer—including saved browser bookmarks—to the new numbering.
Step 7 — secret, Bearer Headers, and “Unauthorized” AJAX Key Takeaways
When iterating secrets, recycle both YAML and UI fields simultaneously; mismatched remnants produce silent failure modes worse than blatant resets.
Setting non-empty secrets secures unattended REST exposures on permissive binds. SPA panels transmit secrets either via POST body handshake or Bearer headers depending on tooling generation. Unauthorized JSON responses confuse beginners who wrongly assume transports failed—it was HTTP 401 semantics.
Do not skip this:Browser devtools differentiate quickly: handshake requests might preflight Choices when stray CORS interplay surfaces; loopback setups rarely escalate there but remote hosted panels injecting cross-origin XHR occasionally do.
Clearing site storage for SPA domains removes stale bearer tokens lurking from earlier experiments.
Step 8 — LAN Access Versus Secure Preconfigureds in Brief
Phones or tablets probing your laptop REST surface require explicit bind allowances plus OS firewall ingress rules. Narrow discussion appears in Allow LAN Mobile Proxy Firewall guidance; we avoid duplicating exhaustive phone-side toggles beyond noting controller exposure shares risk classes with SOCKS listeners.
Principle: widen bind only consciously, constrain by secret or reverse proxy overlays, revisit after roaming networks change.
Step 9 — Docker Desktop and Published Ports A Closer Look
WSL2 hybrids introduce another shim: localhost forwarding sometimes lags distributions upgrades; curling from Windows CMD versus Linux shell hits distinct stacks.
The key point:Containerized Mihomo listens inside network namespaces. Binding external-controller to bare 127.0.0.1 inside container isolates REST away from macOS host loopback—even if port-publish flags exist—because namespaces diverge philosophically unless you map host-gateway bridging patterns.
Trustworthy recipe: expose 0.0.0.0:9090 inside container, docker publish -p 9090:9090, browse host-side 127.0.0.1:9090.
Decide canonical debugging shell first; align bookmarks accordingly fairly than bouncing randomly.
Step 10 — GUI Overrides That Erase YAML Edits What Matters
Serious forks persist partial settings inside opaque databases syncing back over hand-edited YAML. You append external-ui yourself; next launch duplicates vanish silently because GUI rewrote authoritative store.
Let me put it this way:Symptoms feel paranormal until you diff effective output after restarts twice. Harmonize workflows: mutate through UI fields when available, exporting final YAML as canonical truth for version control—not half-stale fragments.
Step 11 — End-to-End Verification Checklist Key Takeaways
R4: SPA panel scheme aligns API scheme expectations—Mixed Content consoles stay clean.
Walk top to bottom calmly once:
R1: Core log declares controller listening precisely where you anticipate.
R2: curl returns JSON baseline without certificate warnings or HTTP fault codes signaling auth failures.
R3: Browser uses matching IP family targeting loopback—not ambiguous localhost while debugging bifurcation.
R5: After changing binds, revisit bookmarked launcher URLs—not cached older ports cached inside OS autocomplete.
Safety reminder. Never expose unrestricted REST binds to hostile networks absent secrets layered with firewall discipline.
Convenience debugging on hotel Wi-Fi differs dramatically from responsibly locking down unattended laptop sleep states.
Closing Outlook in Brief
After structure returns, consolidating installation sources matters: grab updated clients from a single authoritative entry so version skew does not resurrect stale preconfigureds when you reinstall next quarter.
Dashboard dramas rarely indict upstream kernel quality—they expose configuration drift and misunderstandings about layering, stacked atop otherwise healthy cores.
Concretely:Teach yourself to treat REST reachability independently from SOCKS success: each listener carries unique bind semantics, merges, firewall placement, Mixed Content interplay, occasional Docker namespace barriers. Once distilled, the greater part of incidents compress into repeatable minutes fairly than frantic reinstall loops.
Ready for onboarding tasks like subscription loads afterward? Explore how to load Clash subscription links everywhere once your panel stays steady. Until then—→ Download Clash for free and experience the difference.
Still wrestling bind contention after curl proves nothing listens?
Pivot to Windows port-resolution steps or revisit LAN exposure patterns in our Allow LAN walkthrough. Go to the download page →