lfnovo/open-notebook · error · ValueError

Invalid {kind} name: {value!r}

Error message

Invalid {kind} name: {value!r}

What it means

_ensure_safe_identifier() validates table/relationship names interpolated into SurrealQL and rejects anything not matching ^[a-zA-Z_][a-zA-Z0-9_]*$. It is a SQL/SurrealQL injection guard: repo_relate/repo_upsert/repo_update build query strings from these names, so unsafe values (spaces, punctuation, digits first, non-strings, f-string built names) are refused before reaching the database.

Source

Thrown at open_notebook/database/repository.py:31

# Keep the internal SurrealDB websocket out of any configured HTTP proxy
# (issue #1160). Runs at import time - i.e. before any db_connection() can be
# opened - so it protects every entrypoint (API + worker) that touches the DB.
ensure_internal_no_proxy()

T = TypeVar("T", Dict[str, Any], List[Dict[str, Any]])

# Bare SurrealDB table/relation identifier: no ':', whitespace, or query
# syntax. Used to validate the parts of RELATE/UPSERT/UPDATE that name a
# table or edge-relation and therefore can't be bound as a query parameter
# (SurrealQL only allows binding record/table *values*, not identifiers in
# that position).
_IDENTIFIER_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")


def _ensure_safe_identifier(value: str, kind: str) -> str:
    """Validate a table/relationship name before it is interpolated into a query."""
    if not isinstance(value, str) or not _IDENTIFIER_RE.match(value):
        raise ValueError(f"Invalid {kind} name: {value!r}")
    return value


def _get_env_or_default(name: str, default: str) -> str:
    value = os.getenv(name)
    return value if value else default


def get_database_url() -> str:
    """Get database URL with backward compatibility"""
    surreal_url = os.getenv("SURREAL_URL")
    if surreal_url:
        return surreal_url

    # Fallback to old format - WebSocket URL format
    address = os.getenv("SURREAL_ADDRESS", "localhost")
    port = os.getenv("SURREAL_PORT", "8000")
    return f"ws://{address}/rpc:{port}"

View on GitHub (pinned to a7de90d38a)

Solutions

  1. Fix the caller to pass a plain identifier: letters, digits, underscores, not starting with a digit
  2. If the name comes from user input, whitelist it against known tables/relationships before calling the repo function
  3. Check for accidental RecordID/None being passed where a string name is expected
  4. If you truly need exotic table names, rename the table to a safe identifier at the schema level

Example fix

// before
await repo_relate(src, "linked-to note", tgt)
// after
await repo_relate(src, "linked_to_note", tgt)
Defensive patterns

Strategy: validation

Validate before calling

import re
IDENT = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
def safe_name(n: str) -> bool:
    return isinstance(n, str) and bool(IDENT.match(n))
assert safe_name(relationship) before calling repo_relate

Type guard

def is_safe_identifier(value) -> bool:
    return isinstance(value, str) and bool(_IDENTIFIER_RE.match(value))

Try / catch

try:
    await repo_relate(src, rel, tgt)
except ValueError as e:
    if 'Invalid' in str(e) and 'name' in str(e):
        raise InvalidInputError(str(e))  # 400 to client
    raise

Prevention

When it happens

Trigger: Passing a table or relationship name with a space, hyphen, dot, or leading digit to repo_relate/repo_upsert/repo_update; passing None or a non-string (e.g. a RecordID or enum object) where a name is expected; dynamically building relationship names from untrusted input.

Common situations: Dynamically constructing relationship names from note types or user input; passing a full record id ('note:xyz') instead of a table name; refactors that change a constant into an f-string; SurrealDB records created with quoted/escaped identifiers that don't match the regex.

Related errors


AI-assisted analysis of lfnovo/open-notebook@a7de90d38a (2026-08-27). Data as JSON: /api/errors/0423ec717325de68. Report an issue: GitHub.