thedotmack/claude-mem · error · Error
deviceId must be non-empty
Error message
deviceId must be non-empty
What it means
Validation guard in SyncHubDO.pushOps: the authenticated deviceId must be a non-empty string. pushOps appends canonical operations to the per-user sync hub, and every op is attributed to this device, so an empty/absent device identity cannot be accepted. The error is wrapped as an invalid outcome and returned as a refusal rather than crashing the DO.
Solutions
- Ensure the client registers a device and sends a non-empty X-Device-Id header on every push
- Generate and persist a device id (e.g. crypto.randomUUID()) at app init before any sync
- Return a clear 4xx to the client prompting re-registration instead of retrying the same call
Example fix
// before
await hub.push('', ops);
// after
if (!deviceId) deviceId = crypto.randomUUID();
await hub.push(deviceId, ops); Defensive patterns
Strategy: validation
Validate before calling
function assertDeviceId(id: unknown): string {
if (typeof id !== 'string' || id.length === 0) throw new Error('deviceId must be non-empty');
return id;
} Type guard
const isValidDeviceId = (id: unknown): id is string => typeof id === 'string' && id.length > 0;
Try / catch
try {
return await hub.push(deviceId, ops);
} catch (e) {
if (String(e).includes('deviceId must be non-empty')) return registerDeviceAndRetry();
throw e;
} Prevention
- Generate and persist a device id at first launch before any sync
- Use one canonical header name (X-Device-Id) across client code
- Never push before device registration completes
When it happens
Trigger: Calling pushOps with deviceId = '' or null coerced to string; HTTP POST /ops missing or blank X-Device-Id header; client that never persisted its device id before first sync.
Common situations: Fresh install where device registration was skipped, header name typo (x-deviceid vs x-device-id), client-side state wipe clearing the stored device id.
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
- name must be 1-80 characters
- ops must be an array
- canonical content
- device_id must be 1-128 characters
- ops[ ]
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/a7179bd47aa78226.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:376
const attachment = ws.deserializeAttachment() as { device_id?: unknown } | null;
attachedDevice = typeof attachment?.device_id === "string" ? attachment.device_id : null;
} catch {}
if (attachedDevice === originDeviceId) continue;
try { ws.send(frame); } catch {}
}
} catch (error) {
console.error("sync-hub fan-out failed (advisory; push unaffected):", error);
}
}
// ---------------------------------------------------------------------
// Canonical append path and client cursor reads.
// ---------------------------------------------------------------------
async pushOps(deviceId: string, ops: PushOp[], deviceName: string | null = null): Promise<PushOutcome> {
let rows: ValidatedOp[];
try {
if (typeof deviceId !== "string" || deviceId.length === 0) throw invalid("deviceId must be non-empty");
if (!Array.isArray(ops)) throw invalid("ops must be an array");
rows = await Promise.all(ops.map(async (op, index) => {
try {
const parsed = await parseCanonicalOperation(op);
if (parsed.body.origin_device_id !== deviceId) {
throw new Error("origin_device_id does not match authenticated X-Device-Id");
}
return parsed;
} catch (error) {
throw invalid(`ops[${index}] ${error instanceof Error ? error.message : String(error)}`);
}
}));
} catch (error) {
if (error instanceof Error && error.message.startsWith(INVALID_OPS_PREFIX)) {
return { refused: true, error: error.message };
}
if (isDeviceLimitError(error)) return { refused: true, error: DEVICE_LIMIT_ERROR };
throw error;View on GitHub (pinned to d8bc9755e7)