denoland/deno · error · rusqlite::Error::SqliteFailure
SQLITE_CANTOPEN
SQLITE_CANTOPEN
Error message
unable to open database file: "{}" is a symlink What it means
The node:sqlite compatibility layer (ext/node_sqlite) applies the same symlink-refusal hardening as ext/kv: refuse_reparse_point_components checks each path component with symlink_metadata before opening the database, and returns rusqlite's SQLITE_CANTOPEN with this message if any component is a symlink, so node:sqlite operates only on the real filesystem.
Source
Thrown at ext/node_sqlite/database.rs:110
path.to_path_buf()
}
/// SQLite does not enforce `SQLITE_OPEN_NOFOLLOW` on Windows (its
/// `winFullPathname` never resolves reparse points), so reject symlinks and
/// junctions in every path component manually before opening.
#[cfg(windows)]
fn refuse_reparse_point_components(path: &Path) -> Result<(), rusqlite::Error> {
let mut current = PathBuf::new();
for component in path.components() {
current.push(component);
#[allow(
clippy::disallowed_methods,
reason = "node:sqlite operates on the real file system"
)]
match std::fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
return Err(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
Some(format!(
"unable to open database file: \"{}\" is a symlink",
current.display()
)),
));
}
Ok(_) => {}
// Missing components are created (or rejected) by SQLite itself.
Err(_) => break,
}
}
Ok(())
}
/// Static mapping of JavaScript property names to SQLite limits.
/// Order matches SQLite limit constant values (0-10).
/// Keep in sync with LIMIT_NAMES in ext/node/polyfills/sqlite.ts.
const LIMIT_MAPPING: [(&str, Limit); NUM_LIMITS] = [View on GitHub (pinned to 336da420f4)
Solutions
- Pass the fully resolved (realpath) database path to DatabaseSync instead of a symlinked one.
- Replace the symlink with a real file/directory or a bind mount.
- Create the database directly at its final location rather than linking to it.
- Run the same app under Node.js if symlinked DB paths are a hard requirement.
Example fix
// before
const db = new DatabaseSync("/var/app/current/db.sqlite"); // 'current' is a symlink
// after
import { realpathSync } from "node:fs";
const db = new DatabaseSync(realpathSync("/var/app/current/db.sqlite")); Defensive patterns
Strategy: validation
Validate before calling
import { lstatSync, realpathSync } from "node:fs";
if (lstatSync(dbPath).isSymbolicLink()) {
dbPath = realpathSync(dbPath);
} Type guard
function isRealFile(p: string): boolean {
const st = lstatSync(p, { throwIfNoEntry: false });
return st !== undefined && !st.isSymbolicLink();
} Try / catch
try {
db = new DatabaseSync(dbPath);
} catch (e) {
if (String(e).includes("is a symlink")) {
db = new DatabaseSync(realpathSync(dbPath));
} else throw e;
} Prevention
- Always pass realpath-resolved database paths to node:sqlite.
- Avoid deploying databases behind symlinked release directories (use stable real paths or bind mounts).
- Check with lstat (not stat) that neither the file nor its ancestors are symlinks.
- Document that node:sqlite in Deno requires real-filesystem paths.
When it happens
Trigger: new DatabaseSync(path) (node:sqlite) where path or an ancestor is a symlink — e.g. a symlinked database file or a symlinked data directory; also hit via sqlite3 CLI-style paths passed through the Node compat API.
Common situations: Node apps migrated to Deno where the DB lives behind a symlink (versioned db files, ln -s deployments, macOS /tmp, symlinked home dirs in containers).
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
AI-assisted analysis of denoland/deno@336da420f4 (2026-09-11).
Data as JSON: /api/errors/3694a29a78e204bb.
Report an issue: GitHub.