Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions src/notebooklm_tools/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

from . import constants
from .data_types import ConversationTurn
from .errors import ClientAuthenticationError as AuthenticationError
from .errors import ClientAuthenticationError as AuthenticationError, TransientBackendError
from .errors import ResourceExhaustedError, RPCDriftError, RPCError
from .retry import (
DEFAULT_BASE_DELAY,
Expand Down Expand Up @@ -144,6 +144,29 @@ def _extract_user_message(detail_data: Any, _depth: int = 0) -> str:
SOURCE_ADD_TIMEOUT = 120.0 # Extended timeout for all source operations


def _is_unreachable_failure(exc: Exception | None) -> bool:
"""Return True when the evidence shows an unreachable backend, not expiry.

A refresh can fail because the saved cookies are genuinely rejected, or
because the homepage fetch never completed. Only the first case justifies
telling the user to log in again.
"""
if exc is None:
return False
if isinstance(exc, (httpx.TransportError, httpx.TimeoutException)):
return True
if isinstance(exc, OSError):
return True
text = str(exc).lower()
if 'accounts.google.com' in text or 'expired' in text:
return False
return any(
marker in text
for marker in ('could not reach', 'network', 'timed out', 'timeout',
'connection', 'temporarily unavailable', 'dns')
)


class BaseClient:
"""Base client providing HTTP/RPC infrastructure for NotebookLM API.

Expand Down Expand Up @@ -1047,23 +1070,40 @@ def _call_rpc(
# -- Auth recovery (reached only for 401/403 HTTP or RPC Error 16) --

# Layer 1: Refresh CSRF/session tokens (first retry only)
refresh_failure: Exception | None = None
if not _retry:
try:
self._refresh_auth_tokens()
with self._state_lock:
self._client = None
return self._call_rpc(rpc_id, params, path, timeout, _retry=True)
except ValueError:
# CSRF refresh failed (cookies expired) - continue to layer 2
pass
except ValueError as exc:
# Refresh failed. This is NOT proof of expiry: the same path is
# reached when the homepage fetch never completed. Preserve the
# cause so the final message can stay truthful.
refresh_failure = exc
except (httpx.HTTPError, OSError) as exc:
# Transport failure during refresh. Never report this as expiry.
refresh_failure = exc

# Layer 2 & 3: Reload from disk or run headless auth (deep retry)
if not _deep_retry and self._try_reload_or_headless_auth():
with self._state_lock:
self._client = None
return self._call_rpc(rpc_id, params, path, timeout, _retry=True, _deep_retry=True)

# All recovery attempts failed
# All recovery attempts failed.
# Distinguish 'credentials are rejected' from 'the backend was unreachable'.
# Reporting an unreachable backend as an expired session sends users to a
# re-login that cannot fix the problem, and makes automated monitors raise
# false credential alerts.
if _is_unreachable_failure(refresh_failure):
raise TransientBackendError(
"Could not reach NotebookLM while verifying the session "
f"({type(refresh_failure).__name__}). Saved credentials may still be "
"valid. Check connectivity and retry; only run 'nlm login' if this "
"persists after the backend is reachable again."
) from refresh_failure
msg = (
"Authentication expired. Run 'nlm login' in your terminal to re-authenticate. "
"MCP users: the server should auto-detect the new credentials; "
Expand Down
10 changes: 10 additions & 0 deletions src/notebooklm_tools/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ def __init__(self, artifact_id: str, artifact_type: str = "artifact"):
self.artifact_type = artifact_type


class TransientBackendError(Exception):
"""The backend could not be reached while verifying or refreshing a session.

This is deliberately NOT an authentication error. Credentials may still be
valid; the request simply never reached a verdict. Callers and monitors must
not treat this as expiry, because re-authentication cannot fix a transport
failure and a false expiry signal causes unnecessary interactive logins.
"""


class ClientAuthenticationError(Exception):
"""Raised when authentication fails (HTTP 401/403 or RPC Error 16).

Expand Down