libnyanpasu/clash-nyanpasu · error

listen() called before prepare()

Error message

listen() called before prepare()

What it means

listen() on macOS reads the identifier from the ID OnceCell to build the debug Unix socket path. If prepare() was not called first, ID.get() is None and this expect() panics. It is a lifecycle-order guard: prepare() must always precede listen().

Source

Thrown at backend/tauri-plugin-deep-link/src/macos.rs:105

        ID.get()
            .expect("URL event received before prepare() was called")
    );
    if let Ok(mut stream) = UnixStream::connect(addr) {
        if let Err(io_err) = stream.write_all(s.as_bytes()) {
            log::error!(
                "Error sending message to primary instance: {}",
                io_err.to_string()
            );
        };
    }
    std::process::exit(0);
}

pub fn listen<F: FnMut(String) + Send + 'static>(handler: F) -> Result<()> {
    #[cfg(debug_assertions)]
    let addr = format!(
        "/tmp/{}-deep-link.sock",
        ID.get().expect("listen() called before prepare()")
    );

    #[cfg(debug_assertions)]
    if HANDLER
        .set(match UnixStream::connect(&addr) {
            Ok(_) => Mutex::new(Box::new(secondary_handler)),
            Err(err) => {
                log::error!("Error creating socket listener: {}", err.to_string());
                if err.kind() == ErrorKind::ConnectionRefused {
                    let _ = remove_file(&addr);
                }
                Mutex::new(Box::new(handler))
            }
        })
        .is_err()
    {
        return Err(std::io::Error::new(
            ErrorKind::AlreadyExists,

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Always call prepare(identifier) before listen()
  2. Add prepare() at the top of your deep-link registration helper so ordering cannot regress
  3. In tests, call prepare() in a setup fixture before touching listen()

Example fix

// before
listen(|url| println!("{}", url))?;
// after
prepare("com.example.app");
listen(|url| println!("{}", url))?;
Defensive patterns

Strategy: validation

Validate before calling

assert!(ID.get().is_some(), "call prepare() before listen()");

Type guard

fn is_prepared() -> bool { ID.get().is_some() }

Prevention

When it happens

Trigger: Calling listen(handler) before prepare(identifier); conditional startup logic that registers deep links but skips prepare(); re-entrancy where listen is invoked from a hook that runs before plugin prepare.

Common situations: Copy-pasted plugin setup missing the prepare() call; app refactors that moved listen() earlier than prepare(); tests exercising listen() in isolation.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/b3029b107224ecc3. Report an issue: GitHub.