ruvnet/ruflo · error
roomId has invalid characters
Error message
roomId has invalid characters
What it means
validateRoomId enforces a strict allow-list pattern (ROOM_ID_RE) on room IDs before any federation merge or sync touches them. The 'roomId has invalid characters' error means the supplied ID matched the length/required checks but contained characters outside the allowed set, preventing injection or malformed room keys from entering the federation layer.
Solutions
- Inspect the failing roomId and remove/rename characters not matching ROOM_ID_RE (stick to the allow-list, e.g. alphanumerics, '-', '_', '/', '.').
- Sanitize or re-encode the room ID at its source of creation so all rooms are created with valid IDs.
- Normalize input (trim whitespace, percent-decode URL fragments) before passing it to validateRoomId.
- Add a test with the exact rejected ID to confirm which characters violate the pattern.
Example fix
// before
const roomId = `rooms/${topic}:${channel}`;
validateRoomId(roomId); // ':' rejected -> 'roomId has invalid characters'
// after
const roomId = `rooms/${topic}-${channel.replace(/[^a-zA-Z0-9._/-]/g, '')}`;
validateRoomId(roomId); Defensive patterns
Strategy: validation
Validate before calling
const ROOM_ID_SAFE = /^[a-zA-Z0-9._/-]{1,128}$/;
if (!ROOM_ID_SAFE.test(roomId)) throw new Error(`invalid roomId: ${JSON.stringify(roomId)}`); Type guard
function isValidRoomId(v: unknown): v is string {
return typeof v === 'string' && /^[a-zA-Z0-9._/-]{1,128}$/.test(v);
} Try / catch
try {
validateRoomId(roomId);
} catch (e) {
if (e.message === 'roomId has invalid characters') {
console.error(`Rejected roomId ${JSON.stringify(roomId)}: allowed chars only`);
return { ok: false, reason: 'invalid-room-id' };
}
throw e;
} Prevention
- Generate room IDs from a constrained alphabet (alphanumerics, '-', '_', '/', '.')
- Trim and normalize user input before constructing a roomId
- Unit-test ID generation with unicode/symbol inputs
- Validate IDs at the ingress boundary (HTTP handler, peer message) not just at call time
When it happens
Trigger: Calling mergeEnvelopes, syncRoomFromPeer, or starting the server with a roomId that fails ROOM_ID_RE — e.g. one containing spaces, unicode, control characters, or symbols not permitted by the regex.
Common situations: Room IDs built by concatenating user input or URL fragments without sanitizing; IDs copied from logs with trailing whitespace; programmatic IDs generated with characters like ':' or '#' that the allow-list rejects.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- roomId exceeds 128 chars
- roomId is required
- Validation failed
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15).
Data as JSON: /api/errors/d21554d169cfd212.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-federation.ts:321
}
export function readEnvelopes(basePath: string, roomId: string): SignedEnvelope[] {
const p = roomLogPath(basePath, roomId);
if (!existsSync(p)) return [];
const out: SignedEnvelope[] = [];
for (const line of readFileSync(p, 'utf-8').split(/\r?\n/)) {
if (!line.trim()) continue;
try { out.push(JSON.parse(line)); } catch { /* skip malformed */ }
}
return out;
}
export function validateRoomId(roomId: string): string {
if (!roomId || typeof roomId !== 'string') throw new Error('roomId is required');
if (roomId.length > 128) throw new Error('roomId exceeds 128 chars');
// Also blocks path traversal: `.` is allowed but `/` segments cannot form
// `..` without tripping the explicit check below.
if (!ROOM_ID_RE.test(roomId)) throw new Error('roomId has invalid characters');
if (roomId.includes('..')) throw new Error('roomId must not contain ..');
return roomId;
}
export interface MergeResult {
merged: number;
skippedDuplicate: number;
skippedUnverified: number;
skippedOversize: number;
skippedHopLimit: number;
}
/**
* Union-merge verified envelopes from a peer into the local room log.
*
* Idempotent: `envelopeId` is the merge key, so replaying the same batch is a
* no-op. Anything that fails verification is dropped and counted rather than
* quarantined — a receiver has no use for an envelope it cannot attribute.View on GitHub (pinned to 2602b642d9)