feat: add port configuration for upstreams and update proxy request handling

This commit is contained in:
2026-06-01 09:53:58 +08:00
parent d1e3ce76be
commit 8d90d49afc
6 changed files with 33 additions and 6 deletions
+2
View File
@@ -66,6 +66,7 @@ settings:
upstreams:
service_name:
base_url: https://service.example.com/
port: 8443
timeout_seconds: 15
routes:
@@ -83,6 +84,7 @@ Notes:
- `path` is the public gateway path.
- `upstream_path` overrides forwarded path.
- `strip_prefix` removes a leading path segment before forwarding.
- `port` overrides the upstream URL port without changing the host or scheme.
## Administrative Portal and Dashboard
- Admin UI: `GET /admin/portal`
+2 -1
View File
@@ -12,7 +12,8 @@ settings:
upstreams:
lta_datamall:
base_url: https://api.example.com/
base_url: http://127.0.0.1/
port: 8068
timeout_seconds: 20
routes:
+16
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from pathlib import Path
from typing import Literal
from urllib.parse import urlsplit, urlunsplit
import yaml
from pydantic import AnyHttpUrl, BaseModel, Field, ValidationError, field_validator
@@ -22,8 +23,23 @@ class GatewaySettings(BaseModel):
class UpstreamConfig(BaseModel):
base_url: AnyHttpUrl
port: int | None = Field(default=None, ge=1, le=65535)
timeout_seconds: float = Field(default=15.0, ge=0.1, le=120.0)
def resolved_base_url(self) -> str:
url = urlsplit(str(self.base_url))
if self.port is None:
return str(self.base_url)
host = url.hostname or ""
if url.username:
auth = url.username
if url.password:
auth = f"{auth}:{url.password}"
host = f"{auth}@{host}"
netloc = f"{host}:{self.port}"
return urlunsplit((url.scheme, netloc, url.path, url.query, url.fragment))
class RouteConfig(BaseModel):
path: str
+1 -1
View File
@@ -83,7 +83,7 @@ async def proxy_request(
) -> Response:
upstream = config.upstreams[route.upstream]
target_path = build_target_path(request, route)
target_url = urljoin(str(upstream.base_url), target_path.lstrip("/"))
target_url = urljoin(upstream.resolved_base_url(), target_path.lstrip("/"))
cache = None
cache_key = ""
+4
View File
@@ -14,6 +14,7 @@ settings:
upstreams:
demo:
base_url: https://example.com/
port: 8443
routes:
- path: /api/v1/demo
methods: [GET]
@@ -27,6 +28,7 @@ routes:
assert config.settings.title == "Test Gateway"
assert "demo" in config.upstreams
assert config.routes[0].path == "/api/v1/demo"
assert config.upstreams["demo"].port == 8443
def test_load_gateway_config_unknown_upstream(tmp_path) -> None:
@@ -36,6 +38,7 @@ def test_load_gateway_config_unknown_upstream(tmp_path) -> None:
upstreams:
known:
base_url: https://example.com/
port: 8443
routes:
- path: /api/v1/demo
methods: [GET]
@@ -55,6 +58,7 @@ def test_load_gateway_config_invalid_route_path(tmp_path) -> None:
upstreams:
demo:
base_url: https://example.com/
port: 8443
routes:
- path: api/v1/demo
methods: [GET]
+8 -4
View File
@@ -72,7 +72,9 @@ async def test_proxy_request_forwards_to_upstream() -> None:
)
config = GatewayConfig(
upstreams={"demo": UpstreamConfig(base_url="https://example.com/", timeout_seconds=5)},
upstreams={
"demo": UpstreamConfig(base_url="https://example.com/", port=8443, timeout_seconds=5)
},
routes=[],
)
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo")
@@ -84,7 +86,7 @@ async def test_proxy_request_forwards_to_upstream() -> None:
assert response.status_code == 200
assert response.body == b'{"ok":true}'
assert response.headers.get("connection") is None
assert seen["url"] == "https://example.com/api/v1/bus?a=1"
assert seen["url"] == "https://example.com:8443/api/v1/bus?a=1"
assert seen["x_forwarded_proto"] == "http"
assert seen["x_forwarded_host"] == "gateway.local"
@@ -103,7 +105,9 @@ async def test_proxy_request_cache_hit_skips_second_upstream_call() -> None:
config = GatewayConfig(
settings=GatewaySettings(cache_enabled=True, cache_ttl_seconds=60, cache_max_entries=100),
upstreams={"demo": UpstreamConfig(base_url="https://example.com/", timeout_seconds=5)},
upstreams={
"demo": UpstreamConfig(base_url="https://example.com/", port=8443, timeout_seconds=5)
},
routes=[],
)
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo")
@@ -136,7 +140,7 @@ async def test_proxy_request_returns_502_when_upstream_unreachable() -> None:
raise httpx.ConnectError("connection failed", request=req)
config = GatewayConfig(
upstreams={"demo": UpstreamConfig(base_url="https://example.com/")},
upstreams={"demo": UpstreamConfig(base_url="https://example.com/", port=8443)},
routes=[],
)
route = RouteConfig(path="/api/v1/bus", methods=["GET"], upstream="demo")