getzola/zola · error

Couldn't lock imageproc (set_base_url)

Error message

Couldn't lock imageproc (set_base_url)

What it means

Site::set_base_url locks the Site's imageproc Mutex (a std::sync::Mutex guard shared with other threads, e.g. the template functions) and panics with this expect message if the lock is unavailable. A std Mutex only fails when the mutex is poisoned — i.e. another thread panicked while holding it — or on unrecoverable locking errors.

Solutions

  1. Fix the original panic that poisoned the imageproc mutex (look for an earlier panic backtrace in the logs).
  2. Use lock().unwrap_or_else(|p| p.into_inner()) to recover from poisoning if the data is still consistent.
  3. Ensure no code path calls set_base_url after a worker thread has failed.
  4. If re-entrancy is possible (set_base_url called from within image processing), restructure to avoid nested locking.

Example fix

// before
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
// after
let mut imageproc = self.imageproc.lock().unwrap_or_else(|p| p.into_inner());
Defensive patterns

Strategy: try-catch

Try / catch

// Recover from a poisoned mutex instead of panicking
let mut imageproc = match site.imageproc.lock() {
    Ok(g) => g,
    Err(poisoned) => poisoned.into_inner(), // log and recover
};

Prevention

When it happens

Trigger: Calling set_base_url after any thread holding the imageproc lock has panicked (poisoned mutex). With ReentrantLock-like re-entrancy in older code, also calling it from a thread that already holds the lock.

Common situations: A panic inside image processing (e.g. in get_image_metadata or resize_image template functions) poisons the mutex; a later set_base_url call then panics with this message.

Related errors


AI-assisted analysis of getzola/zola@61d3082821 (2026-09-03). Data as JSON: /api/errors/1ac22e6f5a75f7ff. Report an issue: GitHub.

Appendix: source

Thrown at components/site/src/lib.rs:174

        self.live_reload = get_available_port(interface, port_to_avoid);
    }

    /// Only used in `zola serve` to re-use the initial websocket port
    pub fn enable_live_reload_with_port(&mut self, live_reload_port: u16) {
        self.live_reload = Some(live_reload_port);
    }

    /// Reloads the templates and rebuild the site without re-markdown the Markdown.
    pub fn reload_templates(&mut self) -> Result<()> {
        self.tera = load_tera(&self.base_path, &self.config)?;
        tpls::register_early_global_fns(self);
        // TODO: be smarter than that, no need to recompile sass for example
        self.build()
    }

    pub fn set_base_url(&mut self, base_url: String) {
        self.config.base_url = base_url;
        let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
        imageproc.set_base_url(&self.config);
    }

    pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
        self.output_path = path.as_ref().to_path_buf();
    }

    pub fn minify(&mut self) {
        self.config.minify_html = true;
    }

    /// Reads all .md files in the `content` directory and create pages/sections
    /// out of them
    pub fn load(&mut self) -> Result<()> {
        self.library = Arc::new(Library::new(&self.config));

        // not the most elegant loop, but this is necessary to use skip_current_dir
        // which we can only decide to use after we've deserialised the section

View on GitHub (pinned to 61d3082821)