fix(dev): share auth across dev frontend proxy

This commit is contained in:
Codex
2026-05-19 04:41:28 +00:00
parent dfb344ef0c
commit 0c833a7be2
9 changed files with 184 additions and 22 deletions
@@ -16,6 +16,7 @@ const providerHttpTunnelMaxAttempts = 3;
const microserviceForwardRequestHeaders = [
"accept",
"content-type",
"cookie",
"range",
"x-auth",
"x-requested-with",
@@ -28,6 +29,16 @@ const microserviceForwardRequestHeaders = [
"upload-metadata",
"upload-offset",
] as const;
const microserviceForwardResponseHeaders = [
"cache-control",
"content-disposition",
"etag",
"expires",
"last-modified",
"location",
"set-cookie",
"www-authenticate",
] as const;
// ---------------------------------------------------------------------------
// Internal helpers
@@ -140,6 +151,13 @@ function headersFromMicroserviceRequest(requestHeaders: Record<string, JsonValue
return headers;
}
function copyForwardedResponseHeaders(from: Headers, to: Headers): void {
for (const name of microserviceForwardResponseHeaders) {
const value = from.get(name);
if (value !== null && value.length > 0) to.set(name, value);
}
}
function boundedMicroserviceBodyText(
bodyText: string,
contentType: string,
@@ -622,15 +640,16 @@ async function directMicroserviceResponse(
});
const upstreamContentType = upstream.headers.get("content-type") ?? "text/plain; charset=utf-8";
if (upstreamContentType.toLowerCase().includes("text/event-stream")) {
const responseHeaders: Record<string, string> = {
const responseHeaders = new Headers({
"content-type": upstreamContentType,
"cache-control": upstream.headers.get("cache-control") || "no-store, no-transform",
"connection": "keep-alive",
"x-unidesk-proxy-mode": "direct",
"x-unidesk-response-truncated": "false",
};
});
const buffering = upstream.headers.get("x-accel-buffering");
if (buffering !== null) responseHeaders["x-accel-buffering"] = buffering;
if (buffering !== null) responseHeaders.set("x-accel-buffering", buffering);
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
}
const rawBodyText = await upstream.text();
@@ -641,13 +660,15 @@ async function directMicroserviceResponse(
status: upstream.status,
upstreamBodyBytes: rawBodyText.length,
});
const responseHeaders = new Headers({
"content-type": upstreamContentType,
"x-unidesk-proxy-mode": "direct",
"x-unidesk-response-truncated": bounded.truncated ? "true" : "false",
});
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
return new Response(bounded.bodyText, {
status: upstream.status,
headers: {
"content-type": upstreamContentType,
"x-unidesk-proxy-mode": "direct",
"x-unidesk-response-truncated": bounded.truncated ? "true" : "false",
},
headers: responseHeaders,
});
} catch (error) {
return jsonResponse({ ok: false, error: "direct microservice proxy failed", serviceId: service.id, detail: errorToJson(error) }, 502);
@@ -35,6 +35,16 @@ const FORWARD_REQUEST_HEADERS: &[&str] = &[
"upload-metadata",
"upload-offset",
];
const FORWARD_RESPONSE_HEADERS: &[&str] = &[
"cache-control",
"content-disposition",
"etag",
"expires",
"last-modified",
"location",
"set-cookie",
"www-authenticate",
];
fn service_by_id(state: &Arc<AppState>, service_id: &str) -> Option<MicroserviceConfig> {
state
@@ -531,6 +541,44 @@ fn headers_from_microservice_request(request_headers: &Value) -> reqwest::header
headers
}
fn forwarded_response_headers_from_reqwest(
headers: &reqwest::header::HeaderMap,
) -> serde_json::Map<String, Value> {
let mut result = serde_json::Map::new();
for name in FORWARD_RESPONSE_HEADERS {
if let Some(value) = headers
.get(*name)
.and_then(|value| value.to_str().ok())
.filter(|value| !value.is_empty())
{
result.insert((*name).to_string(), Value::String(truncate_text(value, 8192)));
}
}
result
}
fn copy_forwarded_response_headers_from_json(response: &mut Response, result: &Value) {
let Some(headers) = result.get("responseHeaders").and_then(Value::as_object) else {
return;
};
for name in FORWARD_RESPONSE_HEADERS {
if let Some(value) = headers.get(*name).and_then(Value::as_str) {
add_header(response, name, value);
}
}
}
fn copy_forwarded_response_headers_from_reqwest(
response: &mut Response,
headers: &reqwest::header::HeaderMap,
) {
for name in FORWARD_RESPONSE_HEADERS {
if let Some(value) = headers.get(*name).and_then(|value| value.to_str().ok()) {
add_header(response, name, value);
}
}
}
fn content_type_is_json(content_type: &str) -> bool {
content_type.to_ascii_lowercase().contains("json")
}
@@ -644,6 +692,7 @@ fn response_from_provider_microservice_result(result: Value, proxy_mode: &str) -
"false"
},
);
copy_forwarded_response_headers_from_json(&mut response, &result);
response
}
@@ -705,6 +754,7 @@ async fn direct_microservice_response(
}
};
let status = response.status().as_u16();
let response_headers = response.headers().clone();
let content_type = response
.headers()
.get(reqwest::header::CONTENT_TYPE)
@@ -737,6 +787,7 @@ async fn direct_microservice_response(
"x-unidesk-response-truncated",
if truncated { "true" } else { "false" },
);
copy_forwarded_response_headers_from_reqwest(&mut response, &response_headers);
response
}
@@ -5,6 +5,29 @@ server {
# The dev frontend is intentionally reached through the existing backend-core
# microservice proxy so the public port does not need direct access to D601.
# Auth endpoints use the production frontend only to issue/clear the shared
# host-scoped UniDesk session cookie. Application routes and APIs still go to
# frontend-dev.
location = /login {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://frontend:8080$request_uri;
}
location = /logout {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://frontend:8080$request_uri;
}
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -95,12 +95,29 @@ const nativeServiceFailures = new Map<string, number>();
const nativeServiceEndpointCache = new Map<string, { endpoint: NativeServiceEndpoint; expiresAt: number }>();
const nativeServiceTunnels = new Map<string, NativeServiceTunnel>();
logWriter?.prune();
const forwardedResponseHeaderNames = [
"cache-control",
"content-disposition",
"etag",
"expires",
"last-modified",
"location",
"set-cookie",
"www-authenticate",
] as const;
function envString(name: string, fallback: string): string {
const value = process.env[name];
return value === undefined || value.length === 0 ? fallback : value;
}
function copyForwardedResponseHeaders(from: Headers, to: Headers): void {
for (const name of forwardedResponseHeaderNames) {
const value = from.get(name);
if (value !== null && value.length > 0) to.set(name, value);
}
}
function envNumber(name: string, fallback: number): number {
const raw = process.env[name];
if (raw === undefined || raw.trim().length === 0) return fallback;
@@ -636,17 +653,19 @@ async function fetchNativeServiceUrl(
signal: controller.signal,
});
const body = await boundedText(upstream, 8 * 1024 * 1024);
const responseHeaders = new Headers({
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
"x-unidesk-proxy-mode": "kubernetes-native-service",
"x-unidesk-k3s-service": service.id,
"x-unidesk-k3s-service-url": upstreamUrl.origin,
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
...extraHeaders,
});
copyForwardedResponseHeaders(upstream.headers, responseHeaders);
return new Response(body.text, {
status: upstream.status,
headers: {
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
"x-unidesk-proxy-mode": "kubernetes-native-service",
"x-unidesk-k3s-service": service.id,
"x-unidesk-k3s-service-url": upstreamUrl.origin,
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
...extraHeaders,
},
headers: responseHeaders,
});
} finally {
clearTimeout(timer);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@unidesk/provider-gateway",
"version": "0.2.24",
"version": "0.2.25",
"private": true,
"type": "module",
"scripts": {
@@ -130,6 +130,7 @@ const microserviceHttpMaxBodyTextLength = 8 * 1024 * 1024;
const microserviceForwardRequestHeaders = [
"accept",
"content-type",
"cookie",
"range",
"x-auth",
"x-requested-with",
@@ -142,6 +143,16 @@ const microserviceForwardRequestHeaders = [
"upload-metadata",
"upload-offset",
] as const;
const microserviceForwardResponseHeaders = [
"cache-control",
"content-disposition",
"etag",
"expires",
"last-modified",
"location",
"set-cookie",
"www-authenticate",
] as const;
function readGatewayMetadataFile(path: string): { name: string; version: string } | null {
try {
@@ -2009,6 +2020,15 @@ function headersFromMicroserviceRequest(requestHeaders: Record<string, JsonValue
return headers;
}
function forwardedMicroserviceResponseHeaders(response: Response): Record<string, JsonValue> {
const headers: Record<string, JsonValue> = {};
for (const name of microserviceForwardResponseHeaders) {
const value = response.headers.get(name);
if (value !== null && value.length > 0) headers[name] = value.slice(0, 8192);
}
return headers;
}
async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<JsonValue> {
const rawMethod = String(payload.method || "GET").toUpperCase();
const allowedMethods = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"]);
@@ -2081,6 +2101,7 @@ async function runMicroserviceHttp(payload: Record<string, JsonValue>): Promise<
returnedBodyBytes: bounded.bodyText.length,
responseBodyLimitBytes: microserviceHttpMaxBodyTextLength,
truncated: bounded.truncated,
responseHeaders: forwardedMicroserviceResponseHeaders(response),
transform: transformed.transform,
upstreamDurationMs: Date.now() - requestStartedAt,
proxyMode: "provider-gateway-http-fetch",