From 8d90d49afcd92227d92199aecc2449a24c839f1f Mon Sep 17 00:00:00 2001 From: furyhawk Date: Mon, 1 Jun 2026 09:53:58 +0800 Subject: [PATCH] feat: add port configuration for upstreams and update proxy request handling --- README.md | 2 ++ config/gateway.yaml | 3 ++- src/gateway_framework/config.py | 16 ++++++++++++++++ src/gateway_framework/proxy.py | 2 +- tests/test_config.py | 4 ++++ tests/test_proxy.py | 12 ++++++++---- 6 files changed, 33 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a3b9805..32fea8a 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/config/gateway.yaml b/config/gateway.yaml index ede509e..6426254 100644 --- a/config/gateway.yaml +++ b/config/gateway.yaml @@ -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: diff --git a/src/gateway_framework/config.py b/src/gateway_framework/config.py index 855c00c..bf90e18 100644 --- a/src/gateway_framework/config.py +++ b/src/gateway_framework/config.py @@ -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 diff --git a/src/gateway_framework/proxy.py b/src/gateway_framework/proxy.py index bf690e6..d390048 100644 --- a/src/gateway_framework/proxy.py +++ b/src/gateway_framework/proxy.py @@ -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 = "" diff --git a/tests/test_config.py b/tests/test_config.py index 295308e..63a5043 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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] diff --git a/tests/test_proxy.py b/tests/test_proxy.py index d453ffc..995e424 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -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")