question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Remove all instances of `SystemExit` and start implementing our own custom exceptions where needed

See original GitHub issue

Is your feature request related to a specific problem?

Right now we use a lot of SystemExit, but to maintain best practices for software development we should instead be creating custom exceptions everywhere that we would like to raise an exception.

Describe the solution you’d like

A single exceptions.py file per module (fideslang, fidesctl.core, fidesctl.api, fidesctl.connectors, etc…) that contains the custom exceptions.

Describe alternatives you’ve considered, if any

Not using custom exceptions. Having the exceptions defined in the same place where they are used.

Additional context

N/A

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:6 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
PSalant726commented, May 20, 2022

There are several places in the codebase where the below concept is seen, but the inspiration for this issue came from the check_server function, introduced in #433:

https://github.com/ethyca/fides/blob/ef0f636cf588b3edf2a6ba3ac5913fce198f83a5/src/fidesctl/cli/utils.py#L31-L53

It has four possible exit scenarios...
  1. The fidesctl API server is unreachable, so print some red error text to stdout and raise SystemExit(1) with a stack trace.
  2. The CLI + API versions don't match, so print some [different] red error text to stdout.
  3. Quiet mode is enabled, and the CLI + API versions match, so don't print anything to stdout.
  4. Quiet mode is disabled, and the CLI + API versions match, so print some green success text to stdout.

The check_server function is currently referenced in two places:

https://github.com/ethyca/fides/blob/ef0f636cf588b3edf2a6ba3ac5913fce198f83a5/src/fidesctl/cli/__init__.py#L91-L92

https://github.com/ethyca/fides/blob/ef0f636cf588b3edf2a6ba3ac5913fce198f83a5/src/fidesctl/cli/commands/util.py#L93-L104

In both of the above contexts, the code calling check_server is part of the “main” codepath.

Uh... "main codepath"...? What? Executing any CLI command directly calls the cli() function, and so check_server is called prior to any command handler being executed (except for the status command, which of course calls check_server itself).

As a best practice, all logging and user feedback should occur within the main codepath directly, not within helper functions. To resolve this issue with respect to the above example only, I think an acceptable refactor could look like:

# src/fidesctl/cli/utils.py

def check_server(cli_version: str, server_url: str) -> None:
    """Runs a health check and a version check against the server."""

    healthcheck_url = server_url + API_PREFIX + "/health"
    try:
        health_response = check_response(_api.ping(healthcheck_url))
    except requests.exceptions.ConnectionError:
        raise APIServerUnavailableError(url=healthcheck_url)
    except json.JSONDecodeError as err:
        raise InvalidAPIResponseError(err)

    server_version = health_response.json()["version"]
    if clean_version(server_version) != clean_version(cli_version):
        raise APICLIVersionMismatchError(cli_version, server_version)


def clean_version(raw_version: str) -> str:
    """Removes any cruft on version number strings due to local changes."""

    return raw_version.replace(".dirty", "", 1)
# src/fidesctl/cli/__init__.py

if ctx.invoked_subcommand in SERVER_CHECK_COMMAND_NAMES:
    try:
        check_server(VERSION, config.cli.server_url)
    except Exception as err:
        echo_red(err)
# src/fidesctl/cli/commands/util.py

def status(ctx: click.Context) -> None:
    """
    Sends a request to the Fidesctl API healthcheck endpoint and verifies
    that it responds, and that the CLI and API version numbers match.
    """

    click.echo("Checking server status...")

    config = ctx.obj["CONFIG"]
    try:
        check_server(cli_version=fidesctl.__version__, server_url=config.cli.server_url)
    except Exception as err:
        echo_red(err)
    else:
        echo_green(
            "Server is reachable and the client/server application versions match."
        )

…and APIServerUnavailableError, InvalidAPIResponseError, and APICLIVersionMismatchError should be implemented as custom exceptions with __str__() methods that return useful error messages.

The above code is incomplete, but with such a refactor check_server has exactly two exit scenarios: do nothing, or raise an exception. Then, each “main” codepath handles the possible exceptions while producing log output and/or user feedback itself.


The results are twofold:

  1. For CLI users, implementation details are never leaked in error scenarios
    • CLI users shouldn’t and don’t need to know that fidesctl is implemented with Python. As long as our error messaging is clear and actionable, no value is lost by hiding stack traces, and the user experience improves.
  2. For CLI developers, error messaging and user feedback is easily discoverable, controlled, and modified

This pattern should be repeated throughout the CLI codebase wherever applicable, as our default pattern for exception handling and providing user feedback. Separately, a resolution to #596 can replace echo_red and echo_green with a library that helps maintain output consistency and enables true CLI logging to a local file, for debugging purposes.

0reactions
ThomasLaPianacommented, May 9, 2022

de-prioritizing this as it isn’t critical for the 1.7 release

Read more comments on GitHub >

github_iconTop Results From Across the Web

Is there a way to prevent a SystemExit exception raised from ...
You can call os._exit() to directly exit, without throwing an exception: import os os._exit(1). This bypasses all of the python shutdown ...
Read more >
How Python SystemExit Work with Examples - eduCBA
In Python, SystemExit is an exception that is raised by the sys.exit() method. In general, all exceptions must be derived from BaseException which...
Read more >
Implement Custom Exceptions in Java: Why, When and How
Learn 4 best practices for custom exceptions in Java. Understand when and how to use them in your application.
Read more >
Built-in Exceptions — Python 3.11.1 documentation
In Python, all exceptions must be instances of a class that derives from BaseException. In a try statement with an except clause that...
Read more >
Exit function in Java | System.exit() Method with Example
Exception : It throws a SecurityException. Moving ahead with System.exit method(), let's see some of its practical implementation.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found