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

  1. Ensure the client registers a device and sends a non-empty X-Device-Id header on every push
  2. Generate and persist a device id (e.g. crypto.randomUUID()) at app init before any sync
  3. 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

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


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)