Fix Clash Dashboard Not Loading: external-controller & external-ui Checklist Steps

What This Guide Addresses in Depth

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 it makes sense to hydrate. Meanwhile Clash itself proxies ordinary sites just fine because normal traffic never touches the Mihomo REST surface.

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.

Where another piece already dives deep—for example resolving “port already in use” collisions—we point there instead of pasting duplicated steps.

Mechanically:Misery loves company: this symptom is one of the most of of 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.

Mental Model: Three Separate Layers Mechanically

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 Step by Step

Forget the flashy UI for ninety seconds and ask the simplest question on earth: does http://CONTROLLER/version answer?

curl -sS http://127.0.0.1:9090/version

More strictly: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.

Curl exceptions 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.

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.

At a different level: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.

Suppose your eventual goal is controller at loopback port 9090. From the same OS user session that launches the tray app, execute:

Step 2 — external-controller Grammar and Placement Internals and Practice

YAML tolerates indentation, yet humans stumble more than parsers.

external-controller: 127.0.0.1:9090
secret: "replace-me"

Thinking it through:Certain templates abbreviate:

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.

First, a clarification:Narrowing voluntarily to loopback minimizes accidental exposure behind coffee-shop Wi-Fi. If shorthand caused confusion (“which address it makes sense to I bookmark?”), revert to explicit 127.0.0.1:9090 until understanding returns.

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.

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.

Mechanically:Typical minimal keys sit at profile root—not nested unintentionally beneath tun: or dns: unless you knowingly extend provider-specific overlays. Canonical shape resembles:

Step 3 — external-ui Versus Third-Party Hosting in Depth

external-ui points Mihomo toward a filesystem directory hosting static SPA assets packaged with your distribution—or downloaded separately for air-gapped machines.

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.

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.

More strictly: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.

Step 4 — Mixed Content: HTTPS Panels Calling HTTP localhost Mechanically

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.

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

Another pragmatic approach: temporarily open Mihomo-managed controller over HTTPS experimental flags—few maintainers advertise not jumpy ergonomics though. When diagnosis matters more than glossy polish, operate two browser profiles: Chromium with temporarily relaxed mixed-content instrumentation is dangerous; prefer structural alignment reasonably than weakening global security sliders.

At a different level:The SPA loads over HTTPS, then JavaScript forbids XMLHttpRequest downgrade to plaintext unless explicitly exempted—which typical corporate policies forbid.

Step 5 — IPv4 localhost Versus IPv6 (::1) Step by Step

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.

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 Internals and Practice

Symptoms overlap brute-force refusal: Mihomo refuses kick off 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.

sudo lsof -iTCP:9090 -sTCP:LISTEN on macOS or Linux.

Thinking it through:Free the port by retiring the offending service or remap external-controller. Re-sync every downstream consumer—including saved browser bookmarks—to the new numbering.

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:

Step 7 — secret, Bearer Headers, and “Unauthorized” AJAX in Depth

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.

When iterating secrets, recycle both YAML and UI fields simultaneously; mismatched remnants produce silent failure modes worse than blatant resets.

First, a clarification:Clearing site storage for SPA domains removes stale bearer tokens lurking from earlier experiments.

Browser devtools differentiate quickly: handshake requests might preflight Toggles when stray CORS interplay surfaces; loopback setups rarely escalate there but remote hosted panels injecting cross-origin XHR occasionally do.

Step 8 — LAN Access Versus Secure Stocks Mechanically

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 Step by Step

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.

WSL2 hybrids introduce another shim: localhost forwarding sometimes lags distributions re-syncs; curling from Windows CMD versus Linux shell hits distinct stacks.

Mechanically:Decide canonical debugging shell first; align bookmarks accordingly reasonably than bouncing randomly.

It holds up recipe: expose 0.0.0.0:9090 inside container, docker publish -p 9090:9090, browse host-side 127.0.0.1:9090.

Step 10 — GUI Overrides That Erase YAML Edits Internals and Practice

Full-featured forks persist partial settings inside opaque databases syncing back over hand-edited YAML. You append external-ui one at a time; next launch duplicates vanish silently because GUI rewrote authoritative store.

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 in Depth

Walk top to bottom calmly once:

R2: curl returns JSON baseline without certificate warnings or HTTP exception codes signaling auth failures.

R3: Browser uses matching IP family targeting loopback—not ambiguous localhost while debugging bifurcation.

R4: SPA panel scheme aligns API scheme expectations—Mixed Content consoles stay clean.

R5: After changing binds, revisit bookmarked launcher URLs—not cached older ports cached inside OS autocomplete.

R1: Core log declares controller listening precisely where you anticipate.

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 Mechanically

Dashboard dramas rarely indict upstream kernel quality—they expose configuration drift and misunderstandings about layering, stacked atop otherwise healthy cores.

More strictly:After structure returns, consolidating installation sources matters: grab updated clients from a single authoritative entry so version skew does not resurrect stale stocks when you reinstall next quarter.

Ready for onboarding tasks like subscription adds afterward? Explore how to add Clash subscription links everywhere once your panel stays steady. Until then—→ Download Clash for free and experience the difference.

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, most of of of incidents compress into repeatable minutes reasonably than frantic reinstall loops.

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 →