fix(login): http 2 memory leak's (#11830)

# Which Problems Are Solved
All http 2 based connect-es clients caused memory leaks in our testing
using the login v2 application as the host.

# How the Problems Are Solved
Switch to connect-rpc based http 1.1 transport for the login v2

# Additional Changes
Improved the otel interceptor implementation and tests.

# Additional Context
- connect-rpc is only enabled for v2 API's so we have to make sure to
not use any v1 apis in the login but this should be the case anyways.
- Discussion #10562

---------

Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Ramon
2026-03-17 14:29:21 +00:00
committed by GitHub
co-authored by Max Peintner
parent c1be72765c
commit fca2589fc3
11 changed files with 1294 additions and 845 deletions
@@ -1,10 +1,7 @@
/**
* Mock Zitadel Connect/gRPC server for testing trace propagation.
* Mock Zitadel ConnectRPC server for testing trace propagation.
* Captures incoming headers and writes them to a file for test assertions.
*
* Uses HTTP/2 with cleartext (h2c) to support gRPC protocol.
*/
const http2 = require("http2");
const http = require("http");
const fs = require("fs");
const path = require("path");
@@ -34,21 +31,18 @@ function safeJsonStringify(value) {
// Store captured requests
const capturedRequests = [];
// Handle HTTP/2 request
function handleRequest(stream, headers, isHttp2 = true) {
const method = isHttp2 ? headers[":method"] : headers.method;
const url = isHttp2 ? headers[":path"] : headers.url;
const server = http.createServer((req, res) => {
const { method, url } = req;
// Capture trace headers
const traceHeaders = {
timestamp: new Date().toISOString(),
method: method,
url: url,
traceparent: headers["traceparent"] || null,
tracestate: headers["tracestate"] || null,
baggage: headers["baggage"] || null,
grpcAcceptEncoding: headers["grpc-accept-encoding"] || null,
contentType: headers["content-type"] || null,
method,
url,
traceparent: req.headers["traceparent"] || null,
tracestate: req.headers["tracestate"] || null,
baggage: req.headers["baggage"] || null,
contentType: req.headers["content-type"] || null,
};
capturedRequests.push(traceHeaders);
@@ -60,24 +54,9 @@ function handleRequest(stream, headers, isHttp2 = true) {
console.log(`[${traceHeaders.timestamp}] ${method} ${url}`);
console.log(` traceparent: ${traceHeaders.traceparent || "(none)"}`);
// Helper to send response
const sendResponse = (statusCode, contentType, body) => {
if (isHttp2) {
stream.respond({
":status": statusCode,
"content-type": contentType,
"grpc-status": "0",
"grpc-message": "OK",
});
stream.end(body);
} else {
stream.writeHead(statusCode, {
"Content-Type": contentType,
"grpc-status": "0",
"grpc-message": "OK",
});
stream.end(body);
}
res.writeHead(statusCode, { "Content-Type": contentType });
res.end(body);
};
// Health check
@@ -92,7 +71,7 @@ function handleRequest(stream, headers, isHttp2 = true) {
return;
}
// Mock Connect/gRPC responses
// Mock ConnectRPC responses
// These are minimal responses to prevent the login app from crashing
if (url.includes("GetLoginSettings") || url.includes("SettingsService")) {
sendResponse(200, "application/json", safeJsonStringify({ settings: {} }));
@@ -109,47 +88,20 @@ function handleRequest(stream, headers, isHttp2 = true) {
} else {
sendResponse(200, "application/json", safeJsonStringify({}));
}
}
// Create HTTP/2 server (cleartext for testing)
const http2Server = http2.createServer();
http2Server.on("stream", (stream, headers) => {
handleRequest(stream, headers, true);
});
http2Server.on("error", (err) => console.error("HTTP/2 error:", err));
server.on("error", (err) => console.error("Server error:", err));
// Also create HTTP/1.1 server for health checks from wget
const httpServer = http.createServer((req, res) => {
// Create a headers object compatible with handleRequest
const headers = {
method: req.method,
url: req.url,
...req.headers,
};
handleRequest(res, headers, false);
});
// Start HTTP/2 server on main port
http2Server.listen(PORT, "0.0.0.0", () => {
console.log(`Mock Zitadel HTTP/2 server listening on port ${PORT}`);
server.listen(PORT, "0.0.0.0", () => {
console.log(`Mock Zitadel server listening on port ${PORT}`);
console.log(`Captured headers will be written to: ${HEADERS_FILE}`);
});
// Start HTTP/1.1 server on a different port for health checks
const HEALTH_PORT = 7432;
httpServer.listen(HEALTH_PORT, "0.0.0.0", () => {
console.log(`Mock Zitadel HTTP/1.1 health server listening on port ${HEALTH_PORT}`);
});
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("Shutting down mock servers...");
http2Server.close(() => {
httpServer.close(() => {
console.log("Mock servers stopped");
process.exit(0);
});
console.log("Shutting down mock server...");
server.close(() => {
console.log("Mock server stopped");
process.exit(0);
});
});
File diff suppressed because it is too large Load Diff