Jelajahi Sumber

Add a link to backend in connection error (#3044)

* 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 🫨
Masen Furer 10 bulan lalu
induk
melakukan
8eb834f816
4 mengubah file dengan 107 tambahan dan 4 penghapusan
  1. 10 2
      reflex/app.py
  2. 2 2
      reflex/components/next/image.pyi
  3. 1 0
      reflex/constants/event.py
  4. 94 0
      reflex/utils/codespaces.py

+ 10 - 2
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."""

+ 2 - 2
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,

+ 1 - 0
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.

+ 94 - 0
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(
+        """
+    <html>
+        <head>
+            <title>Reflex Github Codespace Forward Successfully Authenticated</title>
+        </head>
+        <body>
+            <center>
+                <h2>Successfully Authenticated</h2>
+            </center>
+            <script language="javascript">
+                %s
+            </script>
+        </body>
+    </html>
+    """
+        % redirect_script
+    )