pulse-framework 0.1.39__tar.gz → 0.1.40__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/PKG-INFO +1 -1
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/pyproject.toml +1 -1
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/app.py +21 -31
- pulse_framework-0.1.40/src/pulse/proxy.py +98 -0
- pulse_framework-0.1.39/src/pulse/proxy.py +0 -195
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/README.md +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/channel.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/cmd.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/dependencies.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/folder_lock.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/helpers.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/models.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/packages.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/processes.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/secrets.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cli/uvicorn_log_config.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/codegen.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/imports.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/js.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/templates/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/templates/layout.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/templates/route.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/templates/routes_ts.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/codegen/utils.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/components/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/components/for_.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/components/if_.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/components/react_router.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/context.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/cookies.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/css.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/decorators.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/env.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/form.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/helpers.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/core.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/effects.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/runtime.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/setup.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/stable.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/hooks/states.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/elements.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/events.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/props.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/svg.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/tags.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/html/tags.pyi +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/messages.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/middleware.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/plugin.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/py.typed +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/query.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/react_component.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/reactive.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/reactive_extensions.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/render_session.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/renderer.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/request.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/routing.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/serializer.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/state.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/types/__init__.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/types/event_handler.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/user_session.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/vdom.py +0 -0
- {pulse_framework-0.1.39 → pulse_framework-0.1.40}/src/pulse/version.py +0 -0
|
@@ -71,7 +71,7 @@ from pulse.middleware import (
|
|
|
71
71
|
Redirect,
|
|
72
72
|
)
|
|
73
73
|
from pulse.plugin import Plugin
|
|
74
|
-
from pulse.proxy import
|
|
74
|
+
from pulse.proxy import ReactProxyHandler
|
|
75
75
|
from pulse.react_component import ReactComponent, registered_react_components
|
|
76
76
|
from pulse.render_session import RenderSession
|
|
77
77
|
from pulse.request import PulseRequest
|
|
@@ -335,11 +335,6 @@ class App:
|
|
|
335
335
|
self.setup(server_address)
|
|
336
336
|
self.status = AppStatus.running
|
|
337
337
|
|
|
338
|
-
# In single-server mode, the Pulse server acts as reverse proxy to the React server
|
|
339
|
-
if self.mode == "single-server":
|
|
340
|
-
return PulseProxy(
|
|
341
|
-
self.asgi, lambda: envvars.react_server_address, self.api_prefix
|
|
342
|
-
)
|
|
343
338
|
return self.asgi
|
|
344
339
|
|
|
345
340
|
def run(
|
|
@@ -379,28 +374,6 @@ class App:
|
|
|
379
374
|
**cors_config,
|
|
380
375
|
)
|
|
381
376
|
|
|
382
|
-
# Debug middleware to log CORS-related request details
|
|
383
|
-
# @self.fastapi.middleware("http")
|
|
384
|
-
# async def cors_debug_middleware(
|
|
385
|
-
# request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
386
|
-
# ):
|
|
387
|
-
# origin = request.headers.get("origin")
|
|
388
|
-
# method = request.method
|
|
389
|
-
# path = request.url.path
|
|
390
|
-
# print(
|
|
391
|
-
# f"[CORS Debug] {method} {path} | Origin: {origin} | "
|
|
392
|
-
# + f"Mode: {self.mode} | Server: {self.server_address}"
|
|
393
|
-
# )
|
|
394
|
-
# response = await call_next(request)
|
|
395
|
-
# allow_origin = response.headers.get("access-control-allow-origin")
|
|
396
|
-
# if allow_origin:
|
|
397
|
-
# print(f"[CORS Debug] Response allows origin: {allow_origin}")
|
|
398
|
-
# elif origin:
|
|
399
|
-
# logger.warning(
|
|
400
|
-
# f"[CORS Debug] Origin {origin} present but no Access-Control-Allow-Origin header set"
|
|
401
|
-
# )
|
|
402
|
-
# return response
|
|
403
|
-
|
|
404
377
|
# Mount PulseContext for all FastAPI routes (no route info). Other API
|
|
405
378
|
# routes / middleware should be added at the module-level, which means
|
|
406
379
|
# this middleware will wrap all of them.
|
|
@@ -419,8 +392,6 @@ class App:
|
|
|
419
392
|
)
|
|
420
393
|
render_id = request.headers.get("x-pulse-render-id")
|
|
421
394
|
render = self._get_render_for_session(render_id, session)
|
|
422
|
-
if render:
|
|
423
|
-
print(f"Reusing render session {render_id}")
|
|
424
395
|
with PulseContext.update(session=session, render=render):
|
|
425
396
|
res: Response = await call_next(request)
|
|
426
397
|
session.handle_response(res)
|
|
@@ -568,6 +539,26 @@ class App:
|
|
|
568
539
|
for plugin in self.plugins:
|
|
569
540
|
plugin.on_setup(self)
|
|
570
541
|
|
|
542
|
+
# In single-server mode, add catch-all route to proxy unmatched requests to React server
|
|
543
|
+
# This route must be registered last so FastAPI tries all specific routes first
|
|
544
|
+
# FastAPI will match specific routes before this catch-all, but we add an explicit check
|
|
545
|
+
# as a safety measure to ensure API routes are never proxied
|
|
546
|
+
if self.mode == "single-server":
|
|
547
|
+
proxy_handler = ReactProxyHandler(lambda: envvars.react_server_address)
|
|
548
|
+
|
|
549
|
+
@self.fastapi.api_route(
|
|
550
|
+
"/{path:path}",
|
|
551
|
+
methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
552
|
+
include_in_schema=False,
|
|
553
|
+
)
|
|
554
|
+
async def proxy_catch_all(request: Request, path: str): # pyright: ignore[reportUnusedFunction]
|
|
555
|
+
# Skip WebSocket upgrades (handled by Socket.IO)
|
|
556
|
+
if request.headers.get("upgrade", "").lower() == "websocket":
|
|
557
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
558
|
+
|
|
559
|
+
# Proxy all unmatched HTTP requests to React Router
|
|
560
|
+
return await proxy_handler(request)
|
|
561
|
+
|
|
571
562
|
@self.sio.event
|
|
572
563
|
async def connect( # pyright: ignore[reportUnusedFunction]
|
|
573
564
|
sid: str, environ: dict[str, Any], auth: dict[str, str] | None
|
|
@@ -902,7 +893,6 @@ class App:
|
|
|
902
893
|
self._cancel_render_cleanup(rid)
|
|
903
894
|
|
|
904
895
|
# Close all render sessions
|
|
905
|
-
print("Closing app")
|
|
906
896
|
for rid in list(self.render_sessions.keys()):
|
|
907
897
|
self.close_render(rid)
|
|
908
898
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proxy handler for forwarding requests to React Router server in single-server mode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from fastapi.responses import StreamingResponse
|
|
10
|
+
from starlette.background import BackgroundTask
|
|
11
|
+
from starlette.requests import Request
|
|
12
|
+
from starlette.responses import PlainTextResponse, Response
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReactProxyHandler:
|
|
18
|
+
"""
|
|
19
|
+
Handles proxying HTTP requests to React Router server.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
get_react_server_address: Callable[[], str | None]
|
|
23
|
+
_client: httpx.AsyncClient | None
|
|
24
|
+
|
|
25
|
+
def __init__(self, get_react_server_address: Callable[[], str | None]):
|
|
26
|
+
"""
|
|
27
|
+
Args:
|
|
28
|
+
get_react_server_address: Callable that returns the React Router server full URL (or None if not started)
|
|
29
|
+
"""
|
|
30
|
+
self.get_react_server_address = get_react_server_address
|
|
31
|
+
self._client = None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def client(self) -> httpx.AsyncClient:
|
|
35
|
+
"""Lazy initialization of HTTP client."""
|
|
36
|
+
if self._client is None:
|
|
37
|
+
self._client = httpx.AsyncClient(
|
|
38
|
+
timeout=httpx.Timeout(30.0),
|
|
39
|
+
follow_redirects=False,
|
|
40
|
+
)
|
|
41
|
+
return self._client
|
|
42
|
+
|
|
43
|
+
async def __call__(self, request: Request) -> Response:
|
|
44
|
+
"""
|
|
45
|
+
Forward HTTP request to React Router server and stream response back.
|
|
46
|
+
"""
|
|
47
|
+
# Get the React server address
|
|
48
|
+
react_server_address = self.get_react_server_address()
|
|
49
|
+
if react_server_address is None:
|
|
50
|
+
# React server not started yet, return error
|
|
51
|
+
return PlainTextResponse(
|
|
52
|
+
"Service Unavailable: React server not ready", status_code=503
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Build target URL
|
|
56
|
+
url = react_server_address.rstrip("/") + request.url.path
|
|
57
|
+
if request.url.query:
|
|
58
|
+
url += "?" + request.url.query
|
|
59
|
+
|
|
60
|
+
# Extract headers, skip host header (will be set by httpx)
|
|
61
|
+
headers = {k: v for k, v in request.headers.items() if k.lower() != "host"}
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Build request
|
|
65
|
+
req = self.client.build_request(
|
|
66
|
+
method=request.method,
|
|
67
|
+
url=url,
|
|
68
|
+
headers=headers,
|
|
69
|
+
content=request.stream(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Send request with streaming
|
|
73
|
+
r = await self.client.send(req, stream=True)
|
|
74
|
+
|
|
75
|
+
# Filter out headers that shouldn't be present in streaming responses
|
|
76
|
+
response_headers = {
|
|
77
|
+
k: v
|
|
78
|
+
for k, v in r.headers.items()
|
|
79
|
+
# if k.lower() not in ("content-length", "transfer-encoding")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return StreamingResponse(
|
|
83
|
+
r.aiter_raw(),
|
|
84
|
+
background=BackgroundTask(r.aclose),
|
|
85
|
+
status_code=r.status_code,
|
|
86
|
+
headers=response_headers,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except httpx.RequestError as e:
|
|
90
|
+
logger.error(f"Proxy request failed: {e}")
|
|
91
|
+
return PlainTextResponse(
|
|
92
|
+
"Bad Gateway: Could not reach React Router server", status_code=502
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def close(self):
|
|
96
|
+
"""Close the HTTP client."""
|
|
97
|
+
if self._client is not None:
|
|
98
|
+
await self._client.aclose()
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Proxy ASGI app for forwarding requests to React Router server in single-server mode.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from typing import Callable, cast
|
|
8
|
-
|
|
9
|
-
import httpx
|
|
10
|
-
from starlette.datastructures import Headers
|
|
11
|
-
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PulseProxy:
|
|
17
|
-
"""
|
|
18
|
-
ASGI app that proxies non-API requests to React Router server.
|
|
19
|
-
|
|
20
|
-
In single-server mode, Python FastAPI handles /_pulse/* routes and
|
|
21
|
-
proxies everything else to the React Router server running on an internal port.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
app: ASGIApp,
|
|
27
|
-
get_react_server_address: Callable[[], str | None],
|
|
28
|
-
api_prefix: str = "/_pulse",
|
|
29
|
-
):
|
|
30
|
-
"""
|
|
31
|
-
Initialize proxy ASGI app.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
app: The ASGI application to wrap (socketio.ASGIApp)
|
|
35
|
-
get_react_server_address: Callable that returns the React Router server full URL (or None if not started)
|
|
36
|
-
api_prefix: Prefix for API routes that should NOT be proxied (default: "/_pulse")
|
|
37
|
-
"""
|
|
38
|
-
self.app: ASGIApp = app
|
|
39
|
-
self.get_react_server_address: Callable[[], str | None] = (
|
|
40
|
-
get_react_server_address
|
|
41
|
-
)
|
|
42
|
-
self.api_prefix: str = api_prefix
|
|
43
|
-
self._client: httpx.AsyncClient | None = None
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
def client(self) -> httpx.AsyncClient:
|
|
47
|
-
"""Lazy initialization of HTTP client."""
|
|
48
|
-
if self._client is None:
|
|
49
|
-
self._client = httpx.AsyncClient(
|
|
50
|
-
timeout=httpx.Timeout(30.0),
|
|
51
|
-
follow_redirects=False,
|
|
52
|
-
)
|
|
53
|
-
return self._client
|
|
54
|
-
|
|
55
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
56
|
-
"""
|
|
57
|
-
ASGI application handler.
|
|
58
|
-
|
|
59
|
-
Routes starting with api_prefix or WebSocket connections go to FastAPI.
|
|
60
|
-
Everything else is proxied to React Router.
|
|
61
|
-
"""
|
|
62
|
-
if scope["type"] != "http":
|
|
63
|
-
# Pass through non-HTTP requests (WebSocket, lifespan, etc.)
|
|
64
|
-
await self.app(scope, receive, send)
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
path = scope["path"]
|
|
68
|
-
|
|
69
|
-
# Check if path starts with API prefix or is a WebSocket upgrade
|
|
70
|
-
if path.startswith(self.api_prefix):
|
|
71
|
-
# This is an API route, pass through to FastAPI
|
|
72
|
-
await self.app(scope, receive, send)
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
# Check if this is a WebSocket upgrade request (even if not prefixed)
|
|
76
|
-
headers = Headers(scope=scope)
|
|
77
|
-
if headers.get("upgrade", "").lower() == "websocket":
|
|
78
|
-
# WebSocket request, pass through to FastAPI
|
|
79
|
-
await self.app(scope, receive, send)
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
# Proxy to React Router server
|
|
83
|
-
await self._proxy_request(scope, receive, send)
|
|
84
|
-
|
|
85
|
-
async def _proxy_request(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
86
|
-
"""
|
|
87
|
-
Forward HTTP request to React Router server and stream response back.
|
|
88
|
-
"""
|
|
89
|
-
# Get the React server address
|
|
90
|
-
react_server_address = self.get_react_server_address()
|
|
91
|
-
if react_server_address is None:
|
|
92
|
-
# React server not started yet, return error
|
|
93
|
-
await send(
|
|
94
|
-
{
|
|
95
|
-
"type": "http.response.start",
|
|
96
|
-
"status": 503,
|
|
97
|
-
"headers": [(b"content-type", b"text/plain")],
|
|
98
|
-
}
|
|
99
|
-
)
|
|
100
|
-
await send(
|
|
101
|
-
{
|
|
102
|
-
"type": "http.response.body",
|
|
103
|
-
"body": b"Service Unavailable: React server not ready",
|
|
104
|
-
}
|
|
105
|
-
)
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
# Build target URL
|
|
109
|
-
path = scope["path"]
|
|
110
|
-
query_string = scope.get("query_string", b"").decode("utf-8")
|
|
111
|
-
# Ensure react_server_address doesn't end with /
|
|
112
|
-
base_url = react_server_address.rstrip("/")
|
|
113
|
-
target_path = f"{base_url}{path}"
|
|
114
|
-
if query_string:
|
|
115
|
-
target_path += f"?{query_string}"
|
|
116
|
-
|
|
117
|
-
# Extract headers
|
|
118
|
-
headers: dict[str, str] = {}
|
|
119
|
-
for name, value in cast(Iterable[tuple[bytes, bytes]], scope["headers"]):
|
|
120
|
-
name = name.decode("latin1")
|
|
121
|
-
value = value.decode("latin1")
|
|
122
|
-
|
|
123
|
-
# Skip host header (will be set by httpx)
|
|
124
|
-
if name.lower() == "host":
|
|
125
|
-
continue
|
|
126
|
-
|
|
127
|
-
# Collect headers (handle multiple values)
|
|
128
|
-
existing = headers.get(name)
|
|
129
|
-
if existing:
|
|
130
|
-
headers[name] = f"{existing},{value}"
|
|
131
|
-
else:
|
|
132
|
-
headers[name] = value
|
|
133
|
-
|
|
134
|
-
# Read request body
|
|
135
|
-
body_parts: list[bytes] = []
|
|
136
|
-
while True:
|
|
137
|
-
message = await receive()
|
|
138
|
-
if message["type"] == "http.request":
|
|
139
|
-
body_parts.append(message.get("body", b""))
|
|
140
|
-
if not message.get("more_body", False):
|
|
141
|
-
break
|
|
142
|
-
body = b"".join(body_parts)
|
|
143
|
-
|
|
144
|
-
try:
|
|
145
|
-
# Forward request to React Router
|
|
146
|
-
method = scope["method"]
|
|
147
|
-
response = await self.client.request(
|
|
148
|
-
method=method,
|
|
149
|
-
url=target_path,
|
|
150
|
-
headers=headers,
|
|
151
|
-
content=body,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
# Send response status
|
|
155
|
-
await send(
|
|
156
|
-
{
|
|
157
|
-
"type": "http.response.start",
|
|
158
|
-
"status": response.status_code,
|
|
159
|
-
"headers": [
|
|
160
|
-
(name.encode("latin1"), value.encode("latin1"))
|
|
161
|
-
for name, value in response.headers.items()
|
|
162
|
-
],
|
|
163
|
-
}
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
# Stream response body
|
|
167
|
-
await send(
|
|
168
|
-
{
|
|
169
|
-
"type": "http.response.body",
|
|
170
|
-
"body": response.content,
|
|
171
|
-
}
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
except httpx.RequestError as e:
|
|
175
|
-
logger.error(f"Proxy request failed: {e}")
|
|
176
|
-
|
|
177
|
-
# Send error response
|
|
178
|
-
await send(
|
|
179
|
-
{
|
|
180
|
-
"type": "http.response.start",
|
|
181
|
-
"status": 502,
|
|
182
|
-
"headers": [(b"content-type", b"text/plain")],
|
|
183
|
-
}
|
|
184
|
-
)
|
|
185
|
-
await send(
|
|
186
|
-
{
|
|
187
|
-
"type": "http.response.body",
|
|
188
|
-
"body": b"Bad Gateway: Could not reach React Router server",
|
|
189
|
-
}
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
async def close(self):
|
|
193
|
-
"""Close the HTTP client."""
|
|
194
|
-
if self._client is not None:
|
|
195
|
-
await self._client.aclose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|