mirror of
https://github.com/furyhawk/api_gateway.git
synced 2026-07-21 02:06:50 +00:00
feat: add port configuration for upstreams and update proxy request handling
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user