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

  1. Pass the authenticated user id (from JWT sub or session) to getMetadata
  2. Validate the id is non-empty before constructing the DO stub
  3. 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

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


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_id

View on GitHub (pinned to d8bc9755e7)