From 8eb834f81637e92a731478101f8ca5b2958849ab Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 27 Jun 2024 12:20:03 -0700 Subject: [PATCH] Add a link to backend in connection error (#3044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Automatic authentication for backend on Github Codespaces When running reflex on Github codespaces, the port forwarding mechanism requires authentication, which happens automatically when first accessing the port via HTTPS; however since the backend connects over the WSS protocol instead, it gets an access error with no way to redirect to Github's authentication servers to get the port open. This PR adds an automatic redirection mechanism to a backend route when there is a connection error accessing the frontend. After the backend route loads, it redirects back to the frontend, but now it can connect to the backend via websocket because the port forward is authenticated. * manually update .pyi file 🫨 --- reflex/app.py | 12 +++- reflex/components/next/image.pyi | 4 +- reflex/constants/event.py | 1 + reflex/utils/codespaces.py | 94 ++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 reflex/utils/codespaces.py diff --git a/reflex/app.py b/reflex/app.py index bb3e1d403..e06956b20 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -80,7 +80,7 @@ from reflex.state import ( _substate_key, code_uses_state_contexts, ) -from reflex.utils import console, exceptions, format, prerequisites, types +from reflex.utils import codespaces, console, exceptions, format, prerequisites, types from reflex.utils.exec import is_testing_env, should_skip_compile from reflex.utils.imports import ImportVar @@ -95,7 +95,11 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ - return Fragment.create(connection_pulser(), connection_toaster()) + return Fragment.create( + connection_pulser(), + connection_toaster(), + *codespaces.codespaces_auto_redirect(), + ) class OverlayFragment(Fragment): @@ -346,6 +350,10 @@ class App(LifespanMixin, Base): StaticFiles(directory=get_upload_dir()), name="uploaded_files", ) + if codespaces.is_running_in_codespaces(): + self.api.get(str(constants.Endpoint.AUTH_CODESPACE))( + codespaces.auth_codespace + ) def _add_cors(self): """Add CORS middleware to the app.""" diff --git a/reflex/components/next/image.pyi b/reflex/components/next/image.pyi index 41484e2fe..c8abc57bd 100644 --- a/reflex/components/next/image.pyi +++ b/reflex/components/next/image.pyi @@ -19,8 +19,8 @@ class Image(NextComponent): def create( # type: ignore cls, *children, - width: Optional[Union[str, int]] = None, - height: Optional[Union[str, int]] = None, + width: Optional[Union[int, str]] = None, + height: Optional[Union[int, str]] = None, src: Optional[Union[Var[Any], Any]] = None, alt: Optional[Union[Var[str], str]] = None, loader: Optional[Union[Var[Any], Any]] = None, diff --git a/reflex/constants/event.py b/reflex/constants/event.py index fcfe9cdfc..16a2c6a5c 100644 --- a/reflex/constants/event.py +++ b/reflex/constants/event.py @@ -10,6 +10,7 @@ class Endpoint(Enum): PING = "ping" EVENT = "_event" UPLOAD = "_upload" + AUTH_CODESPACE = "auth-codespace" def __str__(self) -> str: """Get the string representation of the endpoint. diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py new file mode 100644 index 000000000..7ff686129 --- /dev/null +++ b/reflex/utils/codespaces.py @@ -0,0 +1,94 @@ +"""Utilities for working with Github Codespaces.""" + +from __future__ import annotations + +import os + +from fastapi.responses import HTMLResponse + +from reflex.components.base.script import Script +from reflex.components.component import Component +from reflex.components.core.banner import has_connection_errors +from reflex.components.core.cond import cond +from reflex.constants import Endpoint + +redirect_script = """ +const thisUrl = new URL(window.location.href); +const params = new URLSearchParams(thisUrl.search) + +function doRedirect(url) { + if (!window.sessionStorage.getItem("authenticated_github_codespaces")) { + const a = document.createElement("a"); + if (params.has("redirect_to")) { + a.href = params.get("redirect_to") + } else if (!window.location.href.startsWith(url)) { + a.href = url + `?redirect_to=${window.location.href}` + } else { + return + } + a.hidden = true; + a.click(); + a.remove(); + window.sessionStorage.setItem("authenticated_github_codespaces", "true") + } +} +doRedirect("%s") +""" % Endpoint.AUTH_CODESPACE.get_url() + + +def codespaces_port_forwarding_domain() -> str | None: + """Get the domain for port forwarding in Github Codespaces. + + Returns: + The domain for port forwarding in Github Codespaces, or None if not running in Codespaces. + """ + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv( + "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" + ) + return GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN + + +def is_running_in_codespaces() -> bool: + """Check if the app is running in Github Codespaces. + + Returns: + True if running in Github Codespaces, False otherwise. + """ + return codespaces_port_forwarding_domain() is not None + + +def codespaces_auto_redirect() -> list[Component]: + """Get the components for automatically redirecting back to the app after authenticating a codespace port forward. + + Returns: + A list containing the conditional redirect component, or empty list. + """ + if is_running_in_codespaces(): + return [cond(has_connection_errors, Script.create(redirect_script))] + return [] + + +async def auth_codespace() -> HTMLResponse: + """Page automatically redirecting back to the app after authenticating a codespace port forward. + + Returns: + An HTML response with an embedded script to redirect back to the app. + """ + return HTMLResponse( + """ + + + Reflex Github Codespace Forward Successfully Authenticated + + +
+

Successfully Authenticated

+
+ + + + """ + % redirect_script + )