Skip to content

Commit a971237

Browse files
committed
htag_mode cookie
1 parent e4f853c commit a971237

5 files changed

Lines changed: 96 additions & 0 deletions

File tree

.agent/skills/htag-development/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,21 @@ If you are migrating legacy htag v1 components, be aware of these core framework
401401
**3. Move to Composition & Tailwind**:
402402
- Replace heavy Python wrapper components (like `b.VBox`, `b.Progress()`) with native CSS composition (e.g., `Tag.div(_class="flex flex-col")`, `Tag.div(_class="animate-spin")`) leveraging the `.statics` injection of modern CSS frameworks like Tailwind.
403403

404+
### 15. Forcing Interaction Mode (`htag_mode` cookie)
405+
406+
In some network environments (e.g., restrictive corporate proxies), WebSockets or SSE might be technically "available" but extremely slow to time out, causing a frustrating delay before falling back to a working mode.
407+
408+
htag supports a special cookie `htag_mode` to manually force a specific transport protocol, bypassing the standard auto-detection/timeout logic:
409+
410+
- **`htag_mode=http`**: Forces the application into pure HTTP mode immediately. Both WebSocket and EventSource are mocked to fail instantly.
411+
- **`htag_mode=sse`**: Forces the application to use SSE (Server-Sent Events) by making the initial WebSocket connection fail immediately.
412+
- **(Absent)**: Default behavior (WebSocket -> SSE -> HTTP fallback).
413+
414+
**How to use**:
415+
- Users can set this cookie via browser developer tools (`document.cookie="htag_mode=http;path=/"`).
416+
- It can be used by external load balancers or scripts to pre-configure the best transport for a specific environment.
417+
- The server detects this cookie during the initial `GET /` request and injects a specialized JS bootstrap patch into the page.
418+
404419
## Best Practices
405420

406421
### Layout & Styling

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
**htag** is a Python3 GUI toolkit for building "beautiful" applications for mobile, web, and desktop from a single codebase
2020

21+
2122
Here is a full rewrite of **htag 1.0.0**, using only antigravity and prompts/intentions. It's the future of **htag**.
2223

2324
Currently, it works ...
@@ -120,6 +121,13 @@ To tests them in live (debugging):
120121
- 2/ **sse** : in devtools/console : type `fallback();`
121122
- 3/ **http-pure**: in devtools/console : `fallback_pure_http();` (should always work!)
122123

124+
### Forcing Interaction Mode (htag_mode cookie)
125+
126+
To manually test or force a specific mode (bypassing auto-detection), set the `htag_mode` cookie:
127+
- `document.cookie="htag_mode=http;path=/"` -> Force pure HTTP
128+
- `document.cookie="htag_mode=sse;path=/"` -> Force SSE
129+
- `document.cookie="htag_mode=;Max-Age=0;path=/"` -> Back to default (WS)
130+
123131
## History
124132

125133
At the beginning, there was [guy](https://github.com/manatlan/guy), which was/is the same concept as [python-eel](https://github.com/ChrisKnott/Eel), but more advanced.

docs/runners.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ if __name__ == "__main__":
6666

6767
This ensures your application remains functional even behind the strictest corporate firewalls or unstable mobile connections.
6868

69+
## Forcing Protocol via Cookie (`htag_mode`)
70+
71+
In some restricted network environments (e.g., corporate proxies), WebSockets or SSE might be technically "available" but extremely slow to time out, causing a long delay before falling back to a working mode.
72+
73+
`htag` supports forcing a specific protocol via the `htag_mode` cookie, bypassing the standard auto-detection:
74+
75+
- **`htag_mode=http`**: Forces the application into pure HTTP mode (Level 3) immediately.
76+
- **`htag_mode=sse`**: Forces the application into SSE mode (Level 2).
77+
- **(Absent)**: Default behavior (WebSocket -> SSE -> HTTP).
78+
79+
**Usage**:
80+
You can set this cookie manually in the browser console:
81+
`document.cookie="htag_mode=http;path=/"`
82+
Refresh the page to apply the change.
83+
6984
## Session & Cookie Path
7085

7186
When using `WebApp`, `htag` manages sessions using a `htag_sid` cookie.

htag/runner.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .core import GTag, App as BaseApp
2020
from .tag import Tag
2121
from .utils import _obf_dumps, _obf_loads
22+
from .context import current_request
2223
from .client_js import CLIENT_JS
2324

2425
logger = logging.getLogger("htag")
@@ -178,6 +179,16 @@ def _render_page(self) -> str:
178179
self.sent_statics.update(all_statics)
179180
statics_html = "".join(all_statics)
180181

182+
# Force protocol via cookie if present
183+
protocol_patch = ""
184+
request = current_request.get()
185+
if request and hasattr(request, "cookies"):
186+
mode = request.cookies.get("htag_mode")
187+
if mode == "http":
188+
protocol_patch = "window.WebSocket=window.EventSource=function(){var self=this;setTimeout(function(){if(self.onerror)self.onerror(new Error('Forced HTTP mode'))},0)};"
189+
elif mode == "sse":
190+
protocol_patch = "window.WebSocket=function(){var self=this;setTimeout(function(){if(self.onerror)self.onerror(new Error('Forced SSE mode'))},0)};"
191+
181192
html_content = f"""
182193
<!DOCTYPE html>
183194
<html>
@@ -186,6 +197,7 @@ def _render_page(self) -> str:
186197
<title>{title}</title>
187198
<meta name="viewport" content="width=device-width, initial-scale=1">
188199
<link rel="icon" href="/logo.png">
200+
{f"<script>{protocol_patch}</script>" if protocol_patch else ""}
189201
<script>{CLIENT_JS}</script>
190202
<script>
191203
window.HTAG_RELOAD = {"true" if getattr(self, "_reload", False) else "false"};

tests/test_htag_mode.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from starlette.testclient import TestClient
2+
from htag import App
3+
from htag.web import WebApp
4+
5+
class MyApp(App):
6+
def init(self):
7+
self += "Hello World"
8+
9+
def test_htag_mode_default():
10+
"""Verify that by default, no protocol mocking is injected."""
11+
webapp = WebApp(MyApp)
12+
client = TestClient(webapp.app)
13+
response = client.get("/")
14+
assert response.status_code == 200
15+
assert "window.WebSocket=window.EventSource" not in response.text
16+
assert "Hello World" in response.text
17+
18+
def test_htag_mode_http():
19+
"""Verify protocol mocking for 'http' mode."""
20+
webapp = WebApp(MyApp)
21+
client = TestClient(webapp.app)
22+
response = client.get("/", cookies={"htag_mode": "http"})
23+
assert response.status_code == 200
24+
# Both WebSocket and EventSource should be mocked
25+
assert "window.WebSocket=window.EventSource=function()" in response.text
26+
assert "Forced HTTP mode" in response.text
27+
28+
def test_htag_mode_sse():
29+
"""Verify protocol mocking for 'sse' mode."""
30+
webapp = WebApp(MyApp)
31+
client = TestClient(webapp.app)
32+
response = client.get("/", cookies={"htag_mode": "sse"})
33+
assert response.status_code == 200
34+
# Only WebSocket should be mocked
35+
assert "window.WebSocket=function()" in response.text
36+
assert "window.EventSource=function()" not in response.text
37+
assert "Forced SSE mode" in response.text
38+
39+
def test_htag_mode_invalid():
40+
"""Verify that invalid cookie values are ignored."""
41+
webapp = WebApp(MyApp)
42+
client = TestClient(webapp.app)
43+
response = client.get("/", cookies={"htag_mode": "invalid"})
44+
assert response.status_code == 200
45+
assert "window.WebSocket=function()" not in response.text
46+
assert "window.EventSource=function()" not in response.text

0 commit comments

Comments
 (0)