thedotmack/claude-mem · error · Error
user_id must be non-empty
Error message
user_id must be non-empty
What it means
Validation guard in SyncHubDO.getMetadata: userId must be a non-empty string. getMetadata reports hub stats (op count, device count, head seq) for a user's DO, and the userId selects the DO instance, so it cannot be blank.
Solutions
- Pass the authenticated user id (from JWT sub or session) to getMetadata
- Validate the id is non-empty before constructing the DO stub
- Fix id extraction (trim, correct claim name) so it never yields ''
Example fix
// before
const meta = await stub.getMetadata(token.sub);
// after
const uid = (token.sub ?? '').trim();
if (!uid) throw new Error('no user id in token');
const meta = await stub.getMetadata(uid); Defensive patterns
Strategy: validation
Validate before calling
const uid = (user?.id ?? '').trim();
if (!uid) throw new Error('userId required for getMetadata'); Type guard
const hasUserId = (u: unknown): u is { id: string } => typeof u === 'object' && u !== null && typeof (u as any).id === 'string' && (u as any).id.length > 0; Try / catch
try {
return await stub.getMetadata(userId);
} catch (e) {
if (String(e).includes('user_id must be non-empty')) return null;
throw e;
} Prevention
- Resolve the user id from the verified auth token, not raw client input
- Trim and assert non-empty before constructing DO stubs
- Have monitoring endpoints require an explicit user identifier
When it happens
Trigger: Calling getMetadata('') or with null/undefined; admin/diagnostic endpoint receiving a request without a user identifier in path or query.
Common situations: Monitoring dashboards constructed before the user session is known, id extraction from a malformed JWT/URL returning empty string.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- name must be 1-80 characters
- ops[ ]
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/b2184fe566abd388.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:576
getStatus(deviceId: string | null = null, deviceName: string | null = null): StatusOutcome {
// Status is an authenticated read/probe, not device admission. A known
// device may refresh its display metadata, but an arbitrary X-Device-Id
// must not consume one of the account's 64 durable device slots.
if (deviceId !== null) this.touchExistingDevice(deviceId, normalizeDeviceName(deviceName));
const sql = this.ctx.storage.sql;
return {
protocol_version: 2,
epoch: this.meta("epoch"),
head_seq: this.headSeq(),
projected_seq: this.projectedSeq(),
op_count: sql.exec<{ n: number }>("SELECT COUNT(*) AS n FROM canonical_ops").one().n,
device_count: sql.exec<{ n: number }>("SELECT COUNT(*) AS n FROM devices").one().n,
};
}
getMetadata(userId: string): HubMetadata {
if (typeof userId !== "string" || userId.length === 0) throw invalid("user_id must be non-empty");
const head = this.headSeq();
const projected = this.projectedSeq();
const connected = new Set<string>();
for (const socket of this.ctx.getWebSockets()) {
try {
const attachment = socket.deserializeAttachment() as { device_id?: unknown } | null;
if (typeof attachment?.device_id === "string") connected.add(attachment.device_id);
} catch {}
}
const devices = this.ctx.storage.sql.exec<{
device_id: string;
name: string | null;
last_ack_seq: string;
last_seen: number | null;
}>(
`SELECT device_id, name, last_ack_seq, last_seen
FROM devices
ORDER BY last_seen IS NULL, last_seen DESC, device_idView on GitHub (pinned to d8bc9755e7)