Skip to content

🐛 Fix FASTAPI_CLOUD_TOKEN always overrides user token#180

Merged
YuriiMotov merged 18 commits intomainfrom
login-and-token-2
Apr 15, 2026
Merged

🐛 Fix FASTAPI_CLOUD_TOKEN always overrides user token#180
YuriiMotov merged 18 commits intomainfrom
login-and-token-2

Conversation

@YuriiMotov
Copy link
Copy Markdown
Contributor

@YuriiMotov YuriiMotov commented Apr 12, 2026

This PR implements alternative approach to solve the same issue as #174

For now, if you have FASTAPI_CLOUD_TOKEN set and try to use any command, it will always use deploy token (see sources)

This PR turns Identity into a just storage, and moves token selection logic to APIClient. Now we can parameterize APIClient to use user token or deployment token depending on the context (command used).

Now if user runs fastapi login and FASTAPI_CLOUD_TOKEN is set, it will show a warning:

fastapi login
image

And on fastapi deploy command, we also notify that deployment token is being used:

fastapi deploy
image


As a side effect I updated the output of whoami command.
Now it checks both user token and deployment token:

Details

Not logged in, but deployment token is set:

No credentials found. Use `fastapi login` to login.
⚡ Using API token from environment variable for `fastapi deploy` command.

Logged in, and deployment token is set:

⚡ somebody@example.com
⚡ Using API token from environment variable for `fastapi deploy` command.

Without deployment token:

⚡ somebody@example.com

Additionally added a unset_env_vars fixture that ensures FASTAPI_CLOUD_TOKEN set on host machine doesn't break tests (currently on master tests will fail if you have FASTAPI_CLOUD_TOKEN set)

@YuriiMotov YuriiMotov added the bug Something isn't working label Apr 12, 2026
@@ -557,7 +572,7 @@ def _send_waitlist_form(
) -> None:
with toolkit.progress("Sending your request...") as progress:
with APIClient() as client:
Copy link
Copy Markdown
Contributor Author

@YuriiMotov YuriiMotov Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instantiate APIClient here instead of accepting it as a parameter because at the moment this function is called there is no APIClient instance yet in deploy. We can off course instantiate APIClient earlier in deploy, but it would increase indentation

app_config = get_app_config(path_to_deploy)

if app_config and provided_app_id and app_config.app_id != provided_app_id:
if use_deploy:
Copy link
Copy Markdown
Contributor Author

@YuriiMotov YuriiMotov Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way GitHub shows diff of deploy command starting from this line is total mess..
Changes are not that big actually.
It's just:

  • printing info message in case deployment token is used
  • increasing the indentation caused by with APIClient()
  • using client.handle_http_errors instead of handle_http_errors
  • passing client parameter to some functions
  • passing arguments by names instead of positional arguments

It looks much better in VSCode

POLL_MAX_RETRIES = 5


def _handle_unauthorized(auth_mode: AuthMode) -> str:
Copy link
Copy Markdown
Contributor Author

@YuriiMotov YuriiMotov Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_handle_unauthorized and handle_http_error were just moved from cli module with the following diff:

Details
-def handle_unauthorized() -> str:
+def _handle_unauthorized(auth_mode: AuthMode) -> str:
    message = "The specified token is not valid. "
 
-    identity = Identity()
-
-    if identity.auth_mode == "user":
+    if auth_mode == "user":
         delete_auth_config()
 
        message += "Use `fastapi login` to generate a new token."
    else:
        message += "Make sure to use a valid token."

    return message
 
 
-def handle_http_error(error: HTTPError, default_message: str | None = None) -> str:
+def handle_http_error(
+    error: httpx.HTTPError,
+    default_message: str | None = None,
+    auth_mode: AuthMode = "user",
+) -> str:
     message: str | None = None
 
-    if isinstance(error, HTTPStatusError):
+    if isinstance(error, httpx.HTTPStatusError):
         status_code = error.response.status_code
 
         # Handle validation errors from Pydantic models, this should make it easier to debug :)
        if status_code == 422:
            logger.debug(error.response.json())  # pragma: no cover

         elif status_code == 401:
-            message = handle_unauthorized()
+            message = _handle_unauthorized(auth_mode=auth_mode)
 
         elif status_code == 403:
             message = "You don't have permissions for this resource"


    if not message:
        message = (
            default_message
            or f"Something went wrong while contacting the FastAPI Cloud server. Please try again later. \n\n{error}"
        )

    return message

)

@contextmanager
def handle_http_errors(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_http_errors was just moved from cli module with the following diff (omitted indentation for clearer diff):

Details
-@contextlib.contextmanager
+@contextmanager
 def handle_http_errors(
+    self,
     progress: Progress,
     default_message: str | None = None,
 ) -> Generator[None, None, None]:
     try:
         yield
-    except ReadTimeout as e:
+    except httpx.ReadTimeout as e:
         logger.debug(e)
 
         progress.set_error(
-            "The request to the FastAPI Cloud server timed out. Please try again later."
+            "The request to the FastAPI Cloud server timed out."
+            " Please try again later."
         )
 
         raise typer.Exit(1) from None
-    except HTTPError as e:
+    except httpx.HTTPError as e:
         logger.debug(e)
 
-        message = handle_http_error(e, default_message)
+        message = handle_http_error(e, default_message, auth_mode=self.auth_mode)
 
         progress.set_error(message)

        raise typer.Exit(1) from None

return _is_jwt_expired(self.token)
return _is_jwt_expired(self._user_token)

def is_logged_in(self) -> bool:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, is_logged_in meant any of user token or deploy token exist. I think it was not very good naming (is_authenticated would be better).
Now is_logged_in is only about the user token (obtained as a result of login procedure).
Hope it will not be confusing (for me it's not).

Comment thread tests/conftest.py
Comment on lines +17 to +21
@pytest.fixture(autouse=True)
def unset_env_vars(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]:
"""Fixture to unset environment variables that might interfere with tests."""
monkeypatch.delenv("FASTAPI_CLOUD_TOKEN", raising=False)
yield
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was very annoying that tests stopped working when I had FASTAPI_CLOUD_TOKEN set on my machine.. Took me time to understand what is happening..

Comment thread tests/test_sentry.py
Comment on lines +20 to +31
def test_init_sentry_when_deployment_token(
logged_out_cli: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("FASTAPI_CLOUD_TOKEN", "deployment-token")
with patch("fastapi_cloud_cli.utils.sentry.sentry_sdk.init") as mock_init:
init_sentry()

mock_init.assert_called_once_with(
dsn=SENTRY_DSN,
integrations=[ANY], # TyperIntegration instance
send_default_pii=False,
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary for this PR, I just noticed that this use case wasn't covered by tests

@YuriiMotov YuriiMotov marked this pull request as ready for review April 13, 2026 22:57
Copy link
Copy Markdown
Contributor

@buurro buurro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing this!

Copy link
Copy Markdown
Collaborator

@patrick91 patrick91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I've left a couple of nit comments 😊

Comment thread src/fastapi_cloud_cli/commands/deploy.py Outdated
Comment thread src/fastapi_cloud_cli/commands/deploy.py Outdated
Comment thread src/fastapi_cloud_cli/utils/auth.py Outdated
Comment thread src/fastapi_cloud_cli/utils/auth.py Outdated
YuriiMotov and others added 3 commits April 15, 2026 14:12
Comment thread src/fastapi_cloud_cli/commands/deploy.py
@YuriiMotov YuriiMotov merged commit f467ac4 into main Apr 15, 2026
18 checks passed
@YuriiMotov YuriiMotov deleted the login-and-token-2 branch April 15, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants