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
+6 -6
View File
@@ -101,11 +101,11 @@ export async function createSessionAndUpdateCookie(command: {
return { session: response.session as Session, sessionCookie, challenges: createdSession.challenges };
} else {
throw "could not get session or session does not have loginName";
throw new Error("could not get session or session does not have loginName");
}
});
} else {
throw "Could not create session";
throw new Error("Could not create session");
}
}
@@ -154,7 +154,7 @@ export async function createSessionForIdpAndUpdateCookie({
});
if (!createdSession) {
throw "Could not create session";
throw new Error("Could not create session");
}
const { session } = await getSession({
@@ -164,7 +164,7 @@ export async function createSessionForIdpAndUpdateCookie({
});
if (!session || !session.factors?.user?.loginName) {
throw "Could not retrieve session";
throw new Error("Could not retrieve session");
}
const sessionCookie: CustomCookieData = {
@@ -235,7 +235,7 @@ export async function setSessionAndUpdateCookie(command: {
return getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }).then(
async (response) => {
if (!response?.session || !response.session.factors?.user?.loginName) {
throw "could not get session or session does not have loginName";
throw new Error("could not get session or session does not have loginName");
}
const { session } = response;
@@ -267,7 +267,7 @@ export async function setSessionAndUpdateCookie(command: {
},
);
} else {
throw "Session not be set";
throw new Error("Session could not be set");
}
})
.catch(passwordAttemptsHandler);