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
+15 -26
View File
@@ -1,5 +1,4 @@
services:
devcontainer:
image: mcr.microsoft.com/devcontainers/typescript-node:22-bookworm
volumes:
@@ -15,7 +14,6 @@ services:
DEVCONTAINER_DB_HOST: db
DEVCONTAINER_API_HOST: api
DEVCONTAINER_LOGIN_HOST: login
DEVCONTAINER_LOGIN_API_MOCK_HOST: login-api-mock
DEVCONTAINER_DB_API_INTEGRATION_HOST: db-api-integration
DEVCONTAINER_DB_FUNCTIONAL_UI_HOST: db-functional-ui
ports:
@@ -31,58 +29,49 @@ services:
volumes:
- db-data-dev:/var/lib/postgresql
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-U", "postgres" ]
test: ["CMD-SHELL", "pg_isready", "-U", "postgres"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: "20s"
ports:
- 5432:5432
- "5432:5432"
db-api-integration:
image: 'postgres:18'
image: "postgres:18"
env_file:
- ./.env.db
environment:
PGPORT: 5433
command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all -c shared_buffers=1GB -c work_mem=16MB -c effective_io_concurrency=100 -c wal_level=minimal -c archive_mode=off -c max_wal_senders=0
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-U", "postgres" ]
interval: '10s'
timeout: '30s'
test: ["CMD-SHELL", "pg_isready", "-U", "postgres"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: '20s'
start_period: "20s"
ports:
- 5433:5433
- "5433:5433"
cache-api-integration:
image: 'redis:8'
image: "redis:8"
ports:
- 6379:6379
- "6379:6379"
db-functional-ui:
image: 'postgres:18'
image: "postgres:18"
env_file:
- ./.env.db
environment:
PGPORT: 5434
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-U", "postgres" ]
interval: '10s'
timeout: '30s'
test: ["CMD-SHELL", "pg_isready", "-U", "postgres"]
interval: "10s"
timeout: "30s"
retries: 5
start_period: '20s'
start_period: "20s"
ports:
- "5434:5434"
login-api-mock:
build:
context: ../apps/login/integration/api-mock
additional_contexts:
zitadel-protos: ../proto
ports:
- 22220:22220
- 22222:22222
volumes:
db-data-dev:
@@ -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
+2
View File
@@ -52,6 +52,8 @@
"server-only": "^0.0.1",
"tinycolor2": "^1.6.0",
"uuid": "^13.0.0",
"@connectrpc/connect-node": "^2.1.1",
"@connectrpc/connect": "^2.1.1",
"winston": "^3.17.0"
},
"devDependencies": {
+113 -129
View File
@@ -1,7 +1,11 @@
import { Code, ConnectError, createRouterTransport } from "@connectrpc/connect";
import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { createClientFor } from "@zitadel/client";
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
import { otelGrpcInterceptor } from "./otel";
@@ -18,31 +22,37 @@ describe("otelGrpcInterceptor", () => {
beforeEach(() => exporter.reset());
it("injects traceparent header into gRPC requests", async () => {
const capturedHeaders: Record<string, string> = {};
let capturedHeaders: Headers | undefined;
await otelGrpcInterceptor(async () => ({ success: true }))({
service: { typeName: "zitadel.session.v2.SessionService" },
method: { name: "CreateSession" },
url: "https://api.zitadel.example.com/zitadel.session.v2/CreateSession",
header: {
set: (key: string, value: string) => {
capturedHeaders[key.toLowerCase()] = value;
},
const mockTransport = createRouterTransport(() => {}, {
transport: {
interceptors: [
otelGrpcInterceptor,
(next) => (req) => {
capturedHeaders = req.header;
return next(req);
},
],
},
});
expect(capturedHeaders["traceparent"]).toMatch(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/);
const client = createClientFor(SessionService)(mockTransport);
await expect(client.createSession({})).rejects.toThrow();
expect(capturedHeaders!.get("traceparent")!).toMatch(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/);
});
it("creates client spans with RPC attributes", async () => {
await otelGrpcInterceptor(async () => ({ user: { id: "123" } }))({
service: { typeName: "zitadel.user.v2.UserService" },
method: { name: "GetUserByID" },
url: "https://api.zitadel.example.com/zitadel.user.v2/GetUserByID",
header: { set: () => {} },
const mockTransport = createRouterTransport(() => {}, {
transport: {
interceptors: [otelGrpcInterceptor],
},
});
const client = createClientFor(UserService)(mockTransport);
await expect(client.getUserByID({})).rejects.toThrow();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0]).toMatchObject({
name: "zitadel.user.v2.UserService/GetUserByID",
@@ -51,151 +61,125 @@ describe("otelGrpcInterceptor", () => {
"rpc.system": "grpc",
"rpc.service": "zitadel.user.v2.UserService",
"rpc.method": "GetUserByID",
"server.address": "api.zitadel.example.com",
"server.address": "in-memory",
},
});
});
it("records error spans with gRPC status codes", async () => {
const error = Object.assign(new Error("Session not found"), { code: 5 });
const mockTransport = createRouterTransport(
({ service }) => {
service(SessionService, {
deleteSession: () => {
throw new ConnectError("Session not found", Code.NotFound);
},
});
},
{
transport: {
interceptors: [otelGrpcInterceptor],
},
},
);
await expect(
otelGrpcInterceptor(async () => {
throw error;
})({
service: { typeName: "zitadel.session.v2.SessionService" },
method: { name: "DeleteSession" },
url: "https://api.zitadel.example.com",
header: { set: () => {} },
}),
).rejects.toThrow("Session not found");
const client = createClientFor(SessionService)(mockTransport);
await expect(client.deleteSession({})).rejects.toThrow();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0]).toMatchObject({
name: "zitadel.session.v2.SessionService/DeleteSession",
status: { code: SpanStatusCode.ERROR, message: "Session not found" },
status: {
code: SpanStatusCode.ERROR,
// https://github.com/connectrpc/connect-es/blob/beb1864a45786f01c8ecb76c93a08ac14483885a/packages/connect/src/connect-error.ts#L32
// code prefix gets added by connect
message: "[not_found] Session not found",
},
attributes: { "rpc.grpc.status_code": 5 },
});
expect(spans[0].events).toHaveLength(1);
expect(spans[0].events[0].name).toBe("exception");
});
it("handles missing service/method gracefully", async () => {
await otelGrpcInterceptor(async () => ({ ok: true }))({
url: "https://api.zitadel.example.com",
header: { set: () => {} },
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0]).toMatchObject({
name: "unknown/unknown",
attributes: { "rpc.service": "unknown", "rpc.method": "unknown" },
});
});
it("returns the response from the next handler", async () => {
const expectedResponse = { data: "test-data", items: [1, 2, 3] };
const expectedResponse = {
details: {},
sessionId: "test-session-id",
sessionToken: "test-token",
};
const result = await otelGrpcInterceptor(async () => expectedResponse)({
service: { typeName: "TestService" },
method: { name: "TestMethod" },
url: "https://test.example.com",
header: { set: () => {} },
});
const mockTransport = createRouterTransport(
({ service }) => {
service(SessionService, {
createSession: () => expectedResponse,
});
},
{
transport: {
interceptors: [otelGrpcInterceptor],
},
},
);
expect(result).toEqual(expectedResponse);
const client = createClientFor(SessionService)(mockTransport);
const result = await client.createSession({});
expect(result).toMatchObject(expectedResponse);
});
it("records error spans without gRPC status code", async () => {
const error = new Error("Network failure");
it("handles in-memory transport URL gracefully", async () => {
const mockTransport = createRouterTransport(() => {}, {
transport: {
interceptors: [otelGrpcInterceptor],
},
});
await expect(
otelGrpcInterceptor(async () => {
throw error;
})({
service: { typeName: "zitadel.user.v2.UserService" },
method: { name: "ListUsers" },
url: "https://api.zitadel.example.com",
header: { set: () => {} },
}),
).rejects.toThrow("Network failure");
const client = createClientFor(SessionService)(mockTransport);
await expect(client.createSession({})).rejects.toThrow();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].attributes["server.address"]).toBeDefined();
});
it("sets correct service and method names from protobuf definitions", async () => {
const mockTransport = createRouterTransport(() => {}, {
transport: {
interceptors: [otelGrpcInterceptor],
},
});
const client = createClientFor(SessionService)(mockTransport);
await expect(client.deleteSession({})).rejects.toThrow();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0]).toMatchObject({
name: "zitadel.user.v2.UserService/ListUsers",
status: { code: SpanStatusCode.ERROR, message: "Network failure" },
});
expect(spans[0].attributes).not.toHaveProperty("rpc.grpc.status_code");
});
it("handles malformed URL gracefully", async () => {
await otelGrpcInterceptor(async () => ({ ok: true }))({
service: { typeName: "TestService" },
method: { name: "TestMethod" },
url: "not-a-valid-url",
header: { set: () => {} },
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].attributes["server.address"]).toBe("unknown");
});
it("handles missing URL gracefully", async () => {
await otelGrpcInterceptor(async () => ({ ok: true }))({
service: { typeName: "TestService" },
method: { name: "TestMethod" },
header: { set: () => {} },
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].attributes["server.address"]).toBe("unknown");
});
it("handles empty string URL gracefully", async () => {
await otelGrpcInterceptor(async () => ({ ok: true }))({
service: { typeName: "TestService" },
method: { name: "TestMethod" },
url: "",
header: { set: () => {} },
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].attributes["server.address"]).toBe("unknown");
});
it("uses empty strings as-is for service/method names", async () => {
await otelGrpcInterceptor(async () => ({ ok: true }))({
service: { typeName: "" },
method: { name: "" },
url: "https://api.example.com",
header: { set: () => {} },
});
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
expect(spans[0]).toMatchObject({
name: "/",
attributes: { "rpc.service": "", "rpc.method": "" },
name: "zitadel.session.v2.SessionService/DeleteSession",
attributes: {
"rpc.service": "zitadel.session.v2.SessionService",
"rpc.method": "DeleteSession",
},
});
});
it("handles non-Error exceptions", async () => {
await expect(
otelGrpcInterceptor(async () => {
throw "string error";
})({
service: { typeName: "TestService" },
method: { name: "TestMethod" },
url: "https://api.example.com",
header: { set: () => {} },
}),
).rejects.toBe("string error");
const mockTransport = createRouterTransport(
({ service }) => {
service(SessionService, {
createSession: () => {
throw "string error";
},
});
},
{
transport: {
interceptors: [otelGrpcInterceptor],
},
},
);
const client = createClientFor(SessionService)(mockTransport);
await expect(client.createSession({})).rejects.toThrow();
const spans = exporter.getFinishedSpans();
expect(spans).toHaveLength(1);
+8 -12
View File
@@ -19,8 +19,8 @@
* ```
*/
import { ConnectError, Interceptor } from "@connectrpc/connect";
import { context, propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
import { GrpcError, Interceptor } from "./types";
const TRACER_NAME = "zitadel-login-grpc" as const;
@@ -28,10 +28,7 @@ const TRACER_NAME = "zitadel-login-grpc" as const;
* Extracts the hostname from a URL string. Returns "unknown" if the URL is
* undefined or cannot be parsed.
*/
function parseHostname(url: string | undefined): string {
if (!url) {
return "unknown";
}
function parseHostname(url: string): string {
try {
return new URL(url).hostname;
} catch {
@@ -44,8 +41,8 @@ function parseHostname(url: string | undefined): string {
* client span, injects trace context headers for distributed tracing, and
* records any errors that occur during the call.
*/
export function otelGrpcInterceptor(next: Parameters<Interceptor>[0]): ReturnType<Interceptor> {
return async function tracedCall(req) {
export const otelGrpcInterceptor: Interceptor = (next) =>
async function tracedCall(req) {
const serviceName: string = req.service?.typeName ?? "unknown";
const methodName: string = req.method?.name ?? "unknown";
@@ -70,12 +67,12 @@ export function otelGrpcInterceptor(next: Parameters<Interceptor>[0]): ReturnTyp
const response = await next(req);
span.setStatus({ code: SpanStatusCode.OK });
return response;
} catch (err: unknown) {
} catch (err) {
const exception = err instanceof Error ? err : new Error(String(err));
span.recordException(exception);
const grpcError = err as GrpcError;
if (grpcError.code !== undefined) {
span.setAttribute("rpc.grpc.status_code", grpcError.code);
if (exception instanceof ConnectError) {
span.setAttribute("rpc.grpc.status_code", exception.code);
}
span.setStatus({
code: SpanStatusCode.ERROR,
@@ -88,4 +85,3 @@ export function otelGrpcInterceptor(next: Parameters<Interceptor>[0]): ReturnTyp
},
);
};
}
@@ -1,16 +0,0 @@
export interface GrpcRequestHeader {
readonly set: (key: string, value: string) => void;
}
export interface GrpcRequest {
readonly service?: { readonly typeName: string };
readonly method?: { readonly name: string };
readonly url?: string;
readonly header: GrpcRequestHeader;
}
export interface GrpcError extends Error {
readonly code?: number;
}
export type Interceptor<T = unknown> = (next: (req: GrpcRequest) => Promise<T>) => (req: GrpcRequest) => Promise<T>;
-1
View File
@@ -529,7 +529,6 @@ export async function sendLoginname(command: SendLoginnameCommand) {
// user not found, check if IDPs are available when local auth is not allowed
if (!effectiveLoginSettings?.allowLocalAuthentication) {
console.log("redirecting to IDP (register allowed, password not allowed)");
logger.debug("redirecting to IDP (register allowed, password not allowed)");
const resp = await redirectUserToIDP(undefined, discoveredOrganization);
if (resp) {
+1 -1
View File
@@ -2,7 +2,6 @@ import crypto from "crypto";
import { cookies } from "next/headers";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getFingerprintIdCookie } from "./fingerprint";
import { shouldEnforceMFA } from "./verify-helper";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import {
@@ -11,6 +10,7 @@ import {
checkMFAFactors,
checkPasswordChangeRequired,
checkUserVerification,
shouldEnforceMFA,
} from "./verify-helper";
// Mock function to create timestamps - following the same pattern as session.test.ts
+39 -38
View File
@@ -1,5 +1,5 @@
import { createConnectTransport } from "@connectrpc/connect-node";
import { Client, create, Duration } from "@zitadel/client";
import { createServerTransport as libCreateServerTransport } from "@zitadel/client/node";
import { makeReqCtx } from "@zitadel/client/v2";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OrganizationSchema, TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
@@ -32,9 +32,10 @@ import {
import { getTranslations } from "next-intl/server";
import { unstable_cacheLife as cacheLife } from "next/cache";
import { getUserAgent } from "./fingerprint";
import { otelGrpcInterceptor } from "./grpc/interceptors/otel";
import { createLogger } from "./logger";
import { otelGrpcInterceptor } from "@/lib/grpc/interceptors/otel";
import { Interceptor } from "@connectrpc/connect";
import { createServiceForHost } from "./service";
const logger = createLogger("zitadel");
@@ -1243,46 +1244,46 @@ export type WithServiceConfig<T = {}> = T & {
};
export function createServerTransport(token: string, serviceConfig: ServiceConfig) {
// Build interceptors list - OTEL interceptor is always included for trace propagation
const interceptors: any[] = [otelGrpcInterceptor];
const authorizationInterceptor: Interceptor = (next) => (req) => {
if (!req.header.get("Authorization")) {
req.header.set("Authorization", `Bearer ${token}`);
}
return next(req);
};
// Add custom headers interceptor if needed
if (process.env.CUSTOM_REQUEST_HEADERS || serviceConfig.instanceHost || serviceConfig.publicHost) {
interceptors.push((next: any) => {
return (req: any) => {
// Apply headers from serviceConfig
if (serviceConfig.instanceHost) {
req.header.set("x-zitadel-instance-host", serviceConfig.instanceHost);
}
if (serviceConfig.publicHost) {
req.header.set("x-zitadel-public-host", serviceConfig.publicHost);
const headerInterceptor: Interceptor = (next) => (req) => {
// Apply headers from serviceConfig
if (serviceConfig.instanceHost) {
req.header.set("x-zitadel-instance-host", serviceConfig.instanceHost);
}
if (serviceConfig.publicHost) {
req.header.set("x-zitadel-public-host", serviceConfig.publicHost);
}
// Apply headers from CUSTOM_REQUEST_HEADERS environment variable
if (process.env.CUSTOM_REQUEST_HEADERS) {
process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach((header) => {
const kv = header.indexOf(":");
if (kv > 0) {
const key = header.slice(0, kv).trim();
const value = header.slice(kv + 1).trim();
if (value) {
req.header.set(key, value);
} else {
req.header.delete(key);
}
} else {
logger.warn("Skipping malformed header", { header });
}
});
}
// Apply headers from CUSTOM_REQUEST_HEADERS environment variable
if (process.env.CUSTOM_REQUEST_HEADERS) {
process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach((header) => {
const kv = header.indexOf(":");
if (kv > 0) {
const key = header.slice(0, kv).trim();
const value = header.slice(kv + 1).trim();
if (value) {
req.header.set(key, value);
} else {
req.header.delete(key);
}
} else {
logger.warn("Skipping malformed header", { header });
}
});
}
return next(req);
};
return next(req);
};
});
}
return libCreateServerTransport(token, {
return createConnectTransport({
httpVersion: "1.1",
baseUrl: serviceConfig.baseUrl,
interceptors,
interceptors: [otelGrpcInterceptor, authorizationInterceptor, headerInterceptor],
});
}
+6
View File
@@ -190,6 +190,12 @@ importers:
apps/login:
dependencies:
'@connectrpc/connect':
specifier: ^2.1.1
version: 2.1.1(@bufbuild/[email protected])
'@connectrpc/connect-node':
specifier: ^2.1.1
version: 2.1.1(@bufbuild/[email protected])(@connectrpc/[email protected](@bufbuild/[email protected]))
'@headlessui/react':
specifier: ^2.2.9
version: 2.2.9([email protected]([email protected]))([email protected])