jackwener/OpenCLI · error · ArgumentError
file not readable: ${abs} (${e.message})
Error message
file not readable: ${abs} (${e.message}) What it means
This ArgumentError is thrown by `attachment-upload` in clis/slock/attachment-upload.js:56 when `fs.statSync(abs)` fails on the resolved path. It wraps the underlying Node error message (ENOENT, EACCES, ENOTDIR, etc.), meaning the path does not exist, is inaccessible to the current user, or a path component is not a directory. The command checks locally before spinning up the browser/upload flow.
Source
Thrown at clis/slock/attachment-upload.js:56
domain: SLOCK_DOMAIN,
strategy: Strategy.COOKIE,
browser: true,
siteSession: 'persistent',
args: [
{ name: 'file', positional: true, required: true, help: 'Local file path to upload (single file; max 50 MB)' },
{ name: 'channel', positional: true, required: true, help: 'channelId UUID or #name — server requires the attachment be scoped to a channel' },
{ name: 'server', help: 'Override active server slug' },
],
columns: ['attachmentId', 'filename', 'mimeType', 'sizeBytes'],
func: async (page, kwargs) => {
const filePath = String(kwargs.file ?? '').trim();
if (!filePath) throw new ArgumentError('file path required');
const channel = String(kwargs.channel ?? '').trim();
if (!channel) throw new ArgumentError('channel required (UUID or #name); server rejects uploads without channelId');
const abs = path.resolve(filePath);
let stat;
try { stat = fs.statSync(abs); }
catch (e) { throw new ArgumentError(`file not readable: ${abs} (${e.message})`); }
if (!stat.isFile()) throw new ArgumentError(`not a regular file: ${abs}`);
if (stat.size === 0) throw new ArgumentError(`file is empty: ${abs}`);
if (stat.size > MAX_BYTES) {
throw new ArgumentError(`file is ${stat.size} bytes, exceeds server limit ${MAX_BYTES} (50 MiB). Split or compress before upload.`);
}
const buf = fs.readFileSync(abs);
const filename = path.basename(abs);
const b64 = buf.toString('base64');
await page.goto(SLOCK_HOME_URL);
const snippet = `
${authHeadersFragment({ serverScoped: true, serverIdOverride: kwargs.server })}
${channelResolveFragment(channel)}
// multipart wants the browser to set its own boundary — strip content-type.
const uploadHeaders = { authorization: headers.authorization, accept: headers.accept };
if (headers['x-server-id']) uploadHeaders['x-server-id'] = headers['x-server-id'];View on GitHub (pinned to 49907e53dc)
Solutions
- Verify the file exists and the path is spelled correctly: `ls -l <path>`; use an absolute path to rule out CWD confusion.
- Expand `~` yourself (unquoted tilde or `$HOME`) — the CLI's `path.resolve` does not expand tilde.
- Check read permissions on the file and every parent directory (`namei -l <path>`); fix with chmod/chown or run as a user with access.
- Confirm the producing step of the file completed successfully before uploading.
Example fix
// before: tilde never expands inside quotes -> ENOENT attachment-upload '~/Downloads/report.pdf' '#general' // after: let the shell expand tilde or use $HOME attachment-upload ~/Downloads/report.pdf '#general'
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
import path from 'node:path';
const abs = path.resolve(filePath);
if (!fs.existsSync(abs)) {
throw new Error(`file not readable: ${abs} does not exist`);
}
fs.accessSync(abs, fs.constants.R_OK); // throws EACCES with a clear message Try / catch
try {
await upload({ file: abs, channel });
} catch (e) {
if (e instanceof ArgumentError && e.message.startsWith('file not readable')) {
console.error(`Check the path exists and is readable: ${e.message}`);
process.exitCode = 1;
} else throw e;
} Prevention
- Use absolute paths in scripts/cron/CI so results never depend on the working directory.
- Avoid single-quoted `~/...` paths — the CLI does not expand tilde; let the shell or `$HOME` do it.
- Run `fs.accessSync(p, fs.constants.R_OK)` (or `test -r`) as a preflight check before uploading.
- Verify the file-producing step succeeded before the upload step in pipelines.
When it happens
Trigger: Uploading a file path that does not exist (typo, wrong working directory), a path the process cannot read (permission denied), a broken symlink target, or a path with unexpanded `~` or glob characters. Also fires when a preceding pipeline step that should have produced the file failed silently.
Common situations: Relative paths resolved against an unexpected CWD in cron/CI; missing `~` expansion when the path is single-quoted in shell (`'~/file.pdf'`); files deleted between generation and upload; running the CLI as a user without read permission on the file or a parent directory.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- ${label}文件不存在: ${resolved}
- weixin create-draft cover-image does not exist: ${absPath}
- Image file not found: ${absPath}
- File not found: ${path}
- File could not be read: ${path}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/8e2d0949cc50f200.
Report an issue: GitHub.