{"record":{"id":"1b7ab58bea556692","repo":"gitbutlerapp/gitbutler","slug":"invalid-token-format","errorCode":null,"errorMessage":"Invalid token format","messagePattern":"Invalid token format","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/shared/src/lib/login/loginService.ts","lineNumber":137,"sourceCode":"\t\t\t},\n\t\t);\n\t}\n\n\tasync confirmPasswordReset(\n\t\ttoken: string,\n\t\tnewPassword: string,\n\t\tpasswordConfirmation: string,\n\t): Promise<LoginResponse<{ message: string; token: string }>> {\n\t\treturn await this.sendPostRequest(\n\t\t\t\"sessions/confirm_new_password\",\n\t\t\t{\n\t\t\t\tpassword_reset_token: token,\n\t\t\t\tpassword: newPassword,\n\t\t\t\tpassword_confirmation: passwordConfirmation,\n\t\t\t},\n\t\t\t(data) => {\n\t\t\t\tif (!isStr(data.message)) throw new Error(\"Invalid message format\");\n\t\t\t\tif (!isStr(data.token)) throw new Error(\"Invalid token format\");\n\t\t\t\treturn { message: data.message, token: data.token };\n\t\t\t},\n\t\t);\n\t}\n\n\tasync resetPassword(email: string): Promise<LoginResponse<{ message: string }>> {\n\t\treturn await this.sendPostRequest(\"sessions/forgot_password\", { email }, (data) => {\n\t\t\tif (!isStr(data.message)) throw new Error(\"Invalid message format\");\n\t\t\treturn { message: data.message };\n\t\t});\n\t}\n\n\tasync loginWithEmail(email: string, password: string): Promise<LoginResponse<string>> {\n\t\treturn await this.sendPostRequest(\"sessions/login_with_email\", { email, password }, (data) => {\n\t\t\tif (!isStr(data.token)) throw new Error(\"Invalid token format\");\n\t\t\treturn data.token;\n\t\t});\n\t}","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/packages/shared/src/lib/login/loginService.ts#L119-L155","documentation":"LoginService.confirmPasswordReset requires the 2xx response of POST sessions/confirm_new_password to carry a string `token` (the post-reset session token). When `data.token` is missing or not a string, the successHandler throws 'Invalid token format'. sendPostRequest converts the throw into an error LoginResponse (errorCode network_error), so the password reset flow reports failure even though the reset may have succeeded server-side.","triggerScenarios":"POST sessions/confirm_new_password returns HTTP 200 with a JSON body that lacks a string `token` field (or has a non-string value), tripping isStr(data.token) at line 137.","commonSituations":"Backend changed to no longer auto-login after reset (no token returned); server returns the token under a different key name; version mismatch between app and API; proxies or mocks that strip or reshape the success payload.","solutions":["Inspect the raw 2xx response body for sessions/confirm_new_password to see whether/where `token` is returned","Check whether the backend still returns a session token on password reset; if it removed that behavior, update the client flow to not expect a token (e.g. require an explicit login afterwards)","Align client/server versions or environments so the response contract matches","If a token can legitimately be absent, treat it as optional and fall back to a login step instead of throwing"],"exampleFix":"// before\nreturn { message: data.message, token: data.token };\n// after\nreturn {\n  message: data.message,\n  token: isStr(data.token) ? data.token : null,\n}; // caller then falls back to loginWithEmail when token is null","handlingStrategy":"try-catch","validationCode":"const res = await fetch(base + \"sessions/confirm_new_password\", { method: \"POST\", ... });\nconst data = await res.json();\nif (res.ok && typeof data?.token !== \"string\") {\n  console.warn(\"no session token in reset confirmation response\", data);\n}","typeGuard":"function hasResetToken(data: unknown): data is { token: string } {\n  return typeof data === \"object\" && data !== null && typeof (data as any).token === \"string\";\n}","tryCatchPattern":"const result = await loginService.confirmPasswordReset(token, newPassword, confirm);\nif (result.type === \"error\" && result.errorMessage === \"Invalid token format\") {\n  // Reset likely succeeded but no session token was returned; fall back to explicit login\n  await loginService.loginWithEmail(email, newPassword);\n  return;\n}","preventionTips":["Confirm with the backend team whether confirm_new_password returns a session token, and encode that in the shared response type","Fall back to an explicit login when no token is present instead of failing the whole flow","Add contract tests covering the reset-confirmation response","Treat the reset token as single-use: on this failure do not retry the same request"],"tags":["api","validation","authentication","response-shape"],"backgroundTag":"unexpected-response-shape","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}