fix(login): improve error classification (#11926)

Closes #11923

# Which Problems Are Solved

Client-side gRPC errors (e.g. NotFound, InvalidArgument,
PermissionDenied) were being surfaced as HTTP 500 Internal Server
Errors, causing false SRE alerts and poor UI feedback.

# How the Problems Are Solved

- New transport interceptor (`error-classification.ts`): Automatically
enriches every ConnectError with httpStatus and isUserError metadata via
a `ClassifiedConnectError` wrapper
- Route handler protection (`route.ts`, `flow-initiation.ts`): Catches
classified errors from `getAuthRequest` / `getSAMLRequest` and returns
correct HTTP status codes instead of 500
- Type safety: Replaced all magic number error.code === 9 checks with
typed Code.FailedPrecondition + instanceof ConnectError across
`oidc.ts`, `saml.ts`, `password.ts`, `zitadel.ts`
- Classification-aware logging (`session.ts`): Client/user errors log at
warn level, server errors at error level
- Observability (`otel.ts`): Spans now include error.is_user_error and
http.status_code attributes for alert filtering

# Additional Changes

Gitignore `next-env.d.ts`: This file is auto-generated by Next.js on
every next dev and next build invocation. The two modes write slightly
different import paths (.next/dev/types/ vs .next/types/), which causes
git diff --exit-code to fail in CI whenever a developer runs next dev
locally before committing. Since Next.js regenerates it automatically,
there's no need to track it

---------

Co-authored-by: Ramon <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Max Peintner
2026-03-31 11:20:56 +02:00
committed by GitHub
co-authored by Ramon Copilot
parent 8e41d288ff
commit ff4aae7b3f
18 changed files with 332 additions and 36 deletions
+4 -3
View File
@@ -34,8 +34,9 @@ import { getTranslations } from "next-intl/server";
import { getUserAgent } from "./fingerprint";
import { createLogger } from "./logger";
import { errorClassificationInterceptor } from "@/lib/grpc/interceptors/error-classification";
import { otelGrpcInterceptor } from "@/lib/grpc/interceptors/otel";
import { Interceptor } from "@connectrpc/connect";
import { Code, ConnectError, Interceptor } from "@connectrpc/connect";
import { createServiceForHost } from "./service";
const logger = createLogger("zitadel");
@@ -1154,7 +1155,7 @@ export async function setUserPassword({
return userService.setPassword(payload, {}).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
if (error instanceof ConnectError && error.code === Code.FailedPrecondition && error.message) {
return { error: error.message };
} else {
throw error;
@@ -1378,6 +1379,6 @@ export function createServerTransport(token: string, serviceConfig: ServiceConfi
return createConnectTransport({
httpVersion: "1.1",
baseUrl: serviceConfig.baseUrl,
interceptors: [otelGrpcInterceptor, authorizationInterceptor, headerInterceptor],
interceptors: [otelGrpcInterceptor, errorClassificationInterceptor, authorizationInterceptor, headerInterceptor],
});
}