dani-garcia/vaultwarden · error · ConfigInvalid

S3 support is not enabled

Error message

S3 support is not enabled

What it means

operator_for_path returns an OpenDAL config-invalid error when a storage path uses the s3:// scheme but the binary was compiled without the 's3' cargo feature. S3 storage code is cfg-gated out, so the scheme is unsupported.

Solutions

  1. Rebuild with S3 support: cargo build --release --features s3
  2. Use a Docker image/tag built with the s3 feature
  3. Switch attachments back to local filesystem storage (a normal directory path)
  4. Verify enabled features via the build logs or vaultwarden's version/diagnostics endpoint

Example fix

// before
cargo build --release
// after
cargo build --release --features s3
Defensive patterns

Strategy: fallback

Validate before calling

if (path.startsWith('s3://') && !features.s3) {
  throw new Error('s3:// path used but binary lacks the s3 feature; rebuild with --features s3');
}

Type guard

function s3Supported(storagePath) { return !storagePath.startsWith('s3://') || buildFeatures.includes('s3'); }

Try / catch

try { operator = operatorForPath(path); } catch (e) {
  if (String(e).includes('S3 support is not enabled')) { logger.error('Rebuild with --features s3 or use local storage'); process.exit(1); }
  throw e;
}

Prevention

When it happens

Trigger: Setting an attachment/storage path (e.g. ATTACHMENTS_FOLDER) to s3://... and running a Vaultwarden build compiled without --features s3 (or the prebuilt image without S3 support).

Common situations: Using the default Docker image that lacks the s3 feature; building from source without cargo build --features s3; switching storage backends without rebuilding the binary.

Related errors


AI-assisted analysis of dani-garcia/vaultwarden@cc67d644f6 (2026-09-19). Data as JSON: /api/errors/28380954b137d4d7. Report an issue: GitHub.

Appendix: source

Thrown at src/storage.rs:64

    std::path::Path::new(path).file_name()?.to_str().map(str::to_owned)
}

pub(crate) fn is_fs_operator(operator: &opendal::Operator) -> bool {
    operator.info().scheme() == opendal::services::FS_SCHEME
}

pub(crate) fn operator_for_path(path: &str) -> Result<opendal::Operator, crate::Error> {
    // Cache of previously built operators by path
    static OPERATORS_BY_PATH: LazyLock<dashmap::DashMap<String, opendal::Operator>> =
        LazyLock::new(dashmap::DashMap::new);

    if let Some(operator) = OPERATORS_BY_PATH.get(path) {
        return Ok(operator.clone());
    }

    let operator = if path.starts_with("s3://") {
        #[cfg(not(s3))]
        return Err(opendal::Error::new(opendal::ErrorKind::ConfigInvalid, "S3 support is not enabled").into());

        #[cfg(s3)]
        s3::operator_for_path(path)?
    } else {
        let builder = opendal::services::Fs::default().root(path);
        opendal::Operator::new(builder)?
    };

    OPERATORS_BY_PATH.insert(path.to_owned(), operator.clone());

    Ok(operator)
}

#[cfg(s3)]
mod s3 {
    use std::sync::LazyLock;

    use opendal_http_transport_reqwest::ReqwestTransport;

View on GitHub (pinned to cc67d644f6)