Skip to content
Open
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
30 changes: 24 additions & 6 deletions safety/config/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def _should_build_proxy_config(
if not host or not host.strip():
if any(v is not None for v in other_values):
raise ValueError(
f"Proxy host must be provided when using other proxy options in {source}."
f"Proxy host must be provided when using other proxy options "
f"(from {source}). Please specify --proxy-host or configure the "
f"host in your Safety config file."
)
return False

Expand All @@ -83,7 +85,10 @@ def _build_proxy_config(
) -> ProxyConfig:
if not host or not host.strip():
logger.error(PROXY_HOST_EMPTY, extra={"source": source})
raise ValueError("Proxy host must not be empty")
raise ValueError(
f"Proxy host must not be empty (from {source}). "
f"Please provide a valid proxy host."
)

host = host.strip()

Expand All @@ -92,7 +97,10 @@ def _build_proxy_config(
logger.error(
PROXY_PROTOCOL_INVALID, extra={"protocol": scheme, "source": source}
)
raise ValueError(f"Invalid proxy protocol: {scheme!r}")
raise ValueError(
f"Invalid proxy protocol '{scheme}' (from {source}). "
f"Allowed protocols are: http, https."
)

port = port or DEFAULT_PROXY_PORT

Expand Down Expand Up @@ -139,7 +147,11 @@ def _proxy_from_config_ini(config_path: Path) -> Optional[ProxyConfig]:
return None

if not host_raw:
raise ValueError("Proxy host must not be empty")
raise ValueError(
"Proxy host must not be empty in config file. "
"Please set a valid 'host' value under the [proxy] section "
"in your Safety config file."
)

return _build_proxy_config(
host=host_raw,
Expand Down Expand Up @@ -170,15 +182,21 @@ def _proxy_from_cli_options(
return None

if not host:
raise ValueError("Proxy host must not be empty")
raise ValueError(
"Proxy host must not be empty. "
"Please provide a valid host using --proxy-host."
)

port_val = None

if port:
try:
port_val = int(port)
except ValueError:
raise ValueError("Proxy port must be an integer")
raise ValueError(
f"Proxy port must be an integer, got '{port}'. "
f"Please provide a valid port number using --proxy-port."
)

return _build_proxy_config(
host=host,
Expand Down
107 changes: 84 additions & 23 deletions safety/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
"""
Custom exception and error classes used by the Safety CLI.

Hierarchy:
SafetyException (base, exit code 1)
└── SafetyError (generic error base)
├── MalformedDatabase
├── DatabaseFetchError
│ ├── InvalidCredentialError
│ ├── TooManyRequestsError
│ ├── RequestTimeoutError
│ └── ServerError
├── DatabaseFileNotFoundError
├── InvalidProvidedReportError
├── InvalidRequirementError
├── NotVerifiedEmailError
├── NetworkConnectionError
│ └── SSLCertificateError
├── EnrollmentError
│ └── EnrollmentTransientFailure
└── MachineIdUnavailableError

Each class defines a ``get_exit_code()`` method returning a dedicated
exit code from ``safety.constants`` so that callers can distinguish
error types in scripts and CI pipelines.
"""
from typing import Optional

from safety.constants import (
Expand Down Expand Up @@ -26,7 +52,9 @@ class SafetyException(Exception):
"""

def __init__(
self, message: str = "Unhandled exception happened: {info}", info: str = ""
self,
message: str = "An unexpected Safety CLI error occurred: {info}",
info: str = "",
):
self.message = message.format(info=info)
super().__init__(self.message)
Expand All @@ -52,7 +80,7 @@ class SafetyError(Exception):

def __init__(
self,
message: str = "Unhandled Safety generic error",
message: str = "An unexpected Safety CLI error occurred.",
error_code: Optional[int] = None,
):
self.message = message
Expand Down Expand Up @@ -83,11 +111,10 @@ def __init__(
self,
reason: Optional[str] = None,
fetched_from: str = "server",
message: str = "Sorry, something went wrong.\n"
"Safety CLI cannot read the data fetched from {fetched_from} because it is malformed.\n",
message: str = "The vulnerability database fetched from {fetched_from} is malformed "
"and cannot be read by Safety CLI.\n",
):
info = f"Reason, {reason}" if reason else ""
info = "Reason, {reason}".format(reason=reason)
info = f"Reason: {reason}" if reason else ""
self.message = message.format(fetched_from=fetched_from) + (
info if reason else ""
)
Expand All @@ -111,7 +138,10 @@ class DatabaseFetchError(SafetyError):
message (str): The error message.
"""

def __init__(self, message: str = "Unable to load vulnerability database"):
def __init__(
self, message: str = "Unable to fetch the vulnerability database. "
"Please check your network connection and try again."
):
self.message = message
super().__init__(self.message)

Expand Down Expand Up @@ -161,7 +191,10 @@ class InvalidRequirementError(SafetyError):
"""

def __init__(
self, message: str = "Unable to parse the requirement: {line}", line: str = ""
self,
message: str = "Unable to parse the package requirement: '{line}'. "
"Please ensure the requirement format is valid.",
line: str = "",
):
self.message = message.format(line=line)
super().__init__(self.message)
Expand All @@ -188,7 +221,8 @@ class DatabaseFileNotFoundError(DatabaseFetchError):
def __init__(
self,
db: Optional[str] = None,
message: str = "Unable to find vulnerability database in {db}",
message: str = "Unable to find the vulnerability database file at: {db}. "
"Please verify the file exists and the path is correct.",
):
self.db = db
self.message = message.format(db=db)
Expand Down Expand Up @@ -217,17 +251,16 @@ class InvalidCredentialError(DatabaseFetchError):
def __init__(
self,
credential: Optional[str] = None,
message: str = "Your authentication credential{credential}is invalid. See {link}.",
message: str = "Your authentication credential is invalid. See {link}.",
reason: Optional[str] = None,
):
self.credential = credential
self.link = (
"https://docs.safetycli.com/safety-docs/support/invalid-api-key-error"
)
credential_info = f" Credential: '{credential}'" if credential else ""
self.message = (
message.format(credential=f" '{self.credential}' ", link=self.link)
if self.credential
else message.format(credential=" ", link=self.link)
message.format(link=self.link) + credential_info
)
info = f" Reason: {reason}"
self.message = self.message + (info if reason else "")
Expand All @@ -251,7 +284,11 @@ class NotVerifiedEmailError(SafetyError):
message (str): The error message.
"""

def __init__(self, message: str = "email is not verified"):
def __init__(
self,
message: str = "Your Safety account email is not verified. "
"Please check your inbox and verify your email address to continue.",
):
self.message = message
super().__init__(self.message)

Expand All @@ -275,7 +312,10 @@ class TooManyRequestsError(DatabaseFetchError):
"""

def __init__(
self, reason: Optional[str] = None, message: str = "Too many requests."
self,
reason: Optional[str] = None,
message: str = "Too many requests sent to the Safety server. "
"Please wait and try again later.",
):
info = f" Reason: {reason}"
self.message = message + (info if reason else "")
Expand All @@ -301,7 +341,8 @@ class NetworkConnectionError(SafetyError):

def __init__(
self,
message: str = "Check your network connection, unable to reach the server.",
message: str = "Unable to reach the Safety server. "
"Please check your network connection and try again.",
):
self.message = message
super().__init__(self.message)
Expand All @@ -315,7 +356,12 @@ class SSLCertificateError(NetworkConnectionError):
message (str): The error message.
"""

def __init__(self, message: str = "There is a SSL certificate issue."):
def __init__(
self,
message: str = "SSL certificate verification failed when connecting to the Safety server. "
"This may be caused by a proxy, corporate firewall, or outdated root certificates. "
"See https://docs.safetycli.com for TLS configuration options.",
):
self.message = message
super().__init__(self.message)

Expand All @@ -329,7 +375,9 @@ class RequestTimeoutError(DatabaseFetchError):
"""

def __init__(
self, message: str = "Check your network connection, the request timed out."
self,
message: str = "Request to the Safety server timed out. "
"Please check your network connection and try again.",
):
self.message = message
super().__init__(self.message)
Expand All @@ -347,8 +395,8 @@ class ServerError(DatabaseFetchError):
def __init__(
self,
reason: Optional[str] = None,
message: str = "Sorry, something went wrong.\n"
"Our engineers are working quickly to resolve the issue.",
message: str = "The Safety server encountered an error. "
"Our engineers are working to resolve the issue. Please try again later.",
):
info = f" Reason: {reason}"
self.message = message + (info if reason else "")
Expand All @@ -363,7 +411,11 @@ class EnrollmentError(SafetyError):
message (str): The error message.
"""

def __init__(self, message: str = "Enrollment failed"):
def __init__(
self,
message: str = "Machine enrollment failed. "
"Please check your enrollment key and try again.",
):
self.message = message
super().__init__(self.message)

Expand All @@ -388,7 +440,11 @@ class EnrollmentTransientFailure(EnrollmentError):
message (str): The error message.
"""

def __init__(self, message: str = "Enrollment failed (transient error)"):
def __init__(
self,
message: str = "Machine enrollment failed due to a transient error. "
"This is usually temporary. Please try again.",
):
self.message = message
super().__init__(self.message)

Expand All @@ -410,7 +466,12 @@ class MachineIdUnavailableError(SafetyError):
message (str): The error message.
"""

def __init__(self, message: str = "Unable to determine system identity"):
def __init__(
self,
message: str = "Unable to determine the unique identity of this machine. "
"This is required for enrollment. "
"Ensure the machine has a unique hostname and the required system files are accessible.",
):
self.message = message
super().__init__(self.message)

Expand Down
25 changes: 20 additions & 5 deletions safety/output_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1430,12 +1430,21 @@ def build_primary_announcement(

for line in lines:
if "words" not in line:
raise ValueError("Missing words keyword")
raise ValueError(
"Missing 'words' keyword in announcement JSON. "
"Each announcement line must include a 'words' field."
)
if len(line["words"]) <= 0:
raise ValueError("No words in this line")
raise ValueError(
"No words found in announcement line. "
"Each 'words' field must contain at least one word."
)
for word in line["words"]:
if "value" not in word or not word["value"]:
raise ValueError("Empty word or without value")
raise ValueError(
"Empty word or missing 'value' in announcement JSON. "
"Each word in the 'words' array must have a non-empty 'value' field."
)

message = style_lines(lines, columns, start_line="", end_line="")

Expand Down Expand Up @@ -1577,7 +1586,10 @@ def print_service(
formats = ["text", "screen"]

if out_format not in formats:
raise ValueError(f"Print is only allowed for {', '.join(formats)}")
raise ValueError(
f"Output format '{out_format}' is not supported for printing. "
f"Print is only allowed for: {', '.join(formats)}."
)

if not format_text:
format_text = {
Expand Down Expand Up @@ -1620,7 +1632,10 @@ def prompt_service(
formats = ["text", "screen"]

if out_format not in formats:
raise ValueError(f"Prompt is only allowed for {', '.join(formats)}")
raise ValueError(
f"Output format '{out_format}' is not supported for prompting. "
f"Prompt is only allowed for: {', '.join(formats)}."
)

if not format_text:
format_text = {
Expand Down
5 changes: 4 additions & 1 deletion safety/platform/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,10 @@ def _create_http_client(self) -> httpx.Client:
**client_kwargs,
)
else:
raise ValueError(f"Unexpected auth_type: {self._auth_type}")
raise ValueError(
f"Unexpected authentication type '{self._auth_type}'. "
f"Supported types: api_key, token, machine_token."
)

def _initialize_with_tls_fallback(self) -> None:
"""Initialize the client by testing TLS with a lightweight HEAD probe.
Expand Down