dvalle08 commited on
Commit
3a5c3c8
·
1 Parent(s): 38bbab9

Add error handling for session bootstrap failures: Implement a client-safe error response payload in session_bootstrap.py and update Streamlit routes to use this new error handling.

Browse files
src/api/session_bootstrap.py CHANGED
@@ -25,6 +25,7 @@ RETRYABLE_TWIRP_CODES = {
25
  livekit_api.TwirpErrorCode.UNAVAILABLE,
26
  livekit_api.TwirpErrorCode.UNKNOWN,
27
  }
 
28
 
29
 
30
  @dataclass(frozen=True)
@@ -110,6 +111,14 @@ def _is_retryable_bootstrap_error(exc: Exception) -> bool:
110
  return False
111
 
112
 
 
 
 
 
 
 
 
 
113
  class _SessionBootstrapHandler(BaseHTTPRequestHandler):
114
  server_version = "OpenVoiceAgentBootstrap/1.0"
115
  protocol_version = "HTTP/1.1"
@@ -124,7 +133,7 @@ class _SessionBootstrapHandler(BaseHTTPRequestHandler):
124
  except Exception as exc: # pragma: no cover - exercised by integration flow
125
  logger.exception("Failed to create session bootstrap payload: %s", exc)
126
  self._write_json(
127
- {"error": "bootstrap_failed", "message": str(exc)},
128
  status=HTTPStatus.INTERNAL_SERVER_ERROR,
129
  )
130
  return
 
25
  livekit_api.TwirpErrorCode.UNAVAILABLE,
26
  livekit_api.TwirpErrorCode.UNKNOWN,
27
  }
28
+ CLIENT_BOOTSTRAP_ERROR_MESSAGE = "Could not initialize voice session. Please try again."
29
 
30
 
31
  @dataclass(frozen=True)
 
111
  return False
112
 
113
 
114
+ def build_bootstrap_error_payload(_: Exception | None = None) -> dict[str, str]:
115
+ """Build a client-safe error response payload without internal details."""
116
+ return {
117
+ "error": "bootstrap_failed",
118
+ "message": CLIENT_BOOTSTRAP_ERROR_MESSAGE,
119
+ }
120
+
121
+
122
  class _SessionBootstrapHandler(BaseHTTPRequestHandler):
123
  server_version = "OpenVoiceAgentBootstrap/1.0"
124
  protocol_version = "HTTP/1.1"
 
133
  except Exception as exc: # pragma: no cover - exercised by integration flow
134
  logger.exception("Failed to create session bootstrap payload: %s", exc)
135
  self._write_json(
136
+ build_bootstrap_error_payload(exc),
137
  status=HTTPStatus.INTERNAL_SERVER_ERROR,
138
  )
139
  return
src/api/streamlit_routes.py CHANGED
@@ -12,7 +12,10 @@ from streamlit.web.server.server_util import make_url_path_regex
12
  from tornado.routing import PathMatches, Rule
13
  from tornado.web import Application, RequestHandler
14
 
15
- from src.api.session_bootstrap import build_session_bootstrap_payload
 
 
 
16
  from src.core.logger import logger
17
 
18
  SESSION_BOOTSTRAP_ENDPOINT = "session/bootstrap"
@@ -46,7 +49,7 @@ class SessionBootstrapRequestHandler(RequestHandler):
46
  except Exception as exc: # pragma: no cover - integration behavior
47
  logger.exception("Failed to create session bootstrap payload: %s", exc)
48
  self._write_json(
49
- {"error": "bootstrap_failed", "message": str(exc)},
50
  status=HTTPStatus.INTERNAL_SERVER_ERROR,
51
  )
52
  return
 
12
  from tornado.routing import PathMatches, Rule
13
  from tornado.web import Application, RequestHandler
14
 
15
+ from src.api.session_bootstrap import (
16
+ build_bootstrap_error_payload,
17
+ build_session_bootstrap_payload,
18
+ )
19
  from src.core.logger import logger
20
 
21
  SESSION_BOOTSTRAP_ENDPOINT = "session/bootstrap"
 
49
  except Exception as exc: # pragma: no cover - integration behavior
50
  logger.exception("Failed to create session bootstrap payload: %s", exc)
51
  self._write_json(
52
+ build_bootstrap_error_payload(exc),
53
  status=HTTPStatus.INTERNAL_SERVER_ERROR,
54
  )
55
  return
src/core/settings.py CHANGED
@@ -14,21 +14,32 @@ ENV_FILE = BASE_DIR / ".env"
14
  load_dotenv(ENV_FILE, override=True)
15
  logger.info(f"Loaded environment from: {ENV_FILE}")
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  def mask_sensitive_data(data: dict) -> dict:
19
  masked = {}
20
- sensitive_keys = ["key", "token", "secret", "password"]
21
 
22
  for key, value in data.items():
 
 
 
 
23
  if isinstance(value, dict):
24
  masked[key] = mask_sensitive_data(value)
25
- elif isinstance(value, str) and any(s in key.lower() for s in sensitive_keys):
26
- if not value:
27
- masked[key] = "<not set>"
28
- elif len(value) <= 4:
29
- masked[key] = "***"
30
- else:
31
- masked[key] = f"{value[:4]}...{value[-4:]}"
32
  else:
33
  masked[key] = value
34
 
@@ -227,5 +238,13 @@ try:
227
  logger.info(f"Settings loaded: {json.dumps(masked_settings, indent=2)}")
228
 
229
  except ValidationError as e:
230
- logger.exception(f"Error validating settings: {e.json()}")
 
 
 
 
 
 
 
 
231
  raise
 
14
  load_dotenv(ENV_FILE, override=True)
15
  logger.info(f"Loaded environment from: {ENV_FILE}")
16
 
17
+ SENSITIVE_KEY_MARKERS = ("key", "token", "secret", "password")
18
+
19
+
20
+ def _is_sensitive_key(key: str) -> bool:
21
+ key_lower = key.lower()
22
+ return any(marker in key_lower for marker in SENSITIVE_KEY_MARKERS)
23
+
24
+
25
+ def _redact_sensitive_value(value: object) -> str:
26
+ if value is None:
27
+ return "<not set>"
28
+ if isinstance(value, str) and not value:
29
+ return "<not set>"
30
+ return "<redacted>"
31
+
32
 
33
  def mask_sensitive_data(data: dict) -> dict:
34
  masked = {}
 
35
 
36
  for key, value in data.items():
37
+ if _is_sensitive_key(key):
38
+ masked[key] = _redact_sensitive_value(value)
39
+ continue
40
+
41
  if isinstance(value, dict):
42
  masked[key] = mask_sensitive_data(value)
 
 
 
 
 
 
 
43
  else:
44
  masked[key] = value
45
 
 
238
  logger.info(f"Settings loaded: {json.dumps(masked_settings, indent=2)}")
239
 
240
  except ValidationError as e:
241
+ safe_errors = e.errors(
242
+ include_url=False,
243
+ include_context=False,
244
+ include_input=False,
245
+ )
246
+ logger.exception(
247
+ "Error validating settings: %s",
248
+ json.dumps(safe_errors),
249
+ )
250
  raise
tests/test_session_bootstrap.py CHANGED
@@ -173,3 +173,13 @@ def test_build_session_bootstrap_payload_does_not_retry_non_retryable_twirp_erro
173
 
174
  assert attempts == 1
175
  assert sleep_calls == []
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  assert attempts == 1
175
  assert sleep_calls == []
176
+
177
+
178
+ def test_build_bootstrap_error_payload_hides_exception_details() -> None:
179
+ payload = session_bootstrap.build_bootstrap_error_payload(
180
+ RuntimeError("NVIDIA_API_KEY=super-secret")
181
+ )
182
+
183
+ assert payload["error"] == "bootstrap_failed"
184
+ assert payload["message"] == session_bootstrap.CLIENT_BOOTSTRAP_ERROR_MESSAGE
185
+ assert "super-secret" not in payload["message"]