{"record":{"id":"8ee0af86490bb30d","repo":"siyuan-note/siyuan","slug":"wait-for-oidc-login-transaction-failed-w","errorCode":null,"errorMessage":"wait for OIDC login transaction failed: %w","messagePattern":"wait for OIDC login transaction failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/oidc.go","lineNumber":712,"sourceCode":"\t\treturn nil, false, errors.New(\"OIDC configuration changed during login\")\n\t}\n\tif !(allowDesktopWithoutBinding && (transaction.Flow == oidcFlowDesktop || transaction.Flow == oidcFlowValidate)) &&\n\t\t(binding == \"\" || binding != transaction.Binding) {\n\t\toidcTransactions.Unlock()\n\t\treturn nil, false, errors.New(\"OIDC login binding does not match\")\n\t}\n\tif !transaction.Claimed {\n\t\ttransaction.Claimed = true\n\t\tcopy := *transaction\n\t\toidcTransactions.Unlock()\n\t\treturn &copy, false, nil\n\t}\n\tdone := transaction.Done\n\toidcTransactions.Unlock()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn nil, false, fmt.Errorf(\"wait for OIDC login transaction failed: %w\", ctx.Err())\n\tcase <-done:\n\t}\n\n\toidcTransactions.Lock()\n\tdefer oidcTransactions.Unlock()\n\ttransaction = oidcTransactions.byState[state]\n\tif transaction == nil || !transaction.Completed {\n\t\treturn nil, false, errors.New(\"OIDC login transaction was not found or has expired\")\n\t}\n\tcopy := *transaction\n\treturn &copy, true, nil\n}\n\nfunc completeOIDCTransaction(state string, success bool, message string) {\n\toidcTransactions.Lock()\n\tdefer oidcTransactions.Unlock()\n\ttransaction := oidcTransactions.byState[state]\n\tif transaction == nil {","sourceCodeStart":694,"sourceCodeEnd":730,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/oidc.go#L694-L730","documentation":"When a transaction has already been claimed (e.g. the desktop app polling via poll token and the callback both claiming it), the second claimant waits on transaction.Done for the flow to complete. If the caller's context is cancelled or times out first, this error wraps the context error via %w. It signals that the wait was aborted, not that the login itself failed.","triggerScenarios":"A second concurrent claim of the same state (poll endpoint vs callback) whose request context is cancelled — HTTP client disconnect, request timeout, or explicit context cancellation before the other claimant completes the transaction.","commonSituations":"Desktop poll request aborted by the user closing the window; frontend fetch timeout shorter than the login takes; user refreshing/cancelling while another tab waits.","solutions":["Retry the claim/wait with a fresh context if the login is still in progress","Increase the client-side request timeout so the poll waits at least as long as oidcTransactionTimeout","Ensure the UI keeps the connection alive until the transaction completes or expires"],"exampleFix":"// before\nctx := context.Background() // or a short-lived request ctx\n// after: give the wait enough headroom\nctx, cancel := context.WithTimeout(context.Background(), oidcTransactionTimeout)\ndefer cancel()","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"tx, done, err := claimOIDCTransaction(ctx, state, binding, false)\nif err != nil {\n    var ctxErr error\n    if errors.As(err, &ctxErr) || strings.Contains(err.Error(), \"context\") {\n        // wait aborted: retry with a longer-lived context if login may still complete\n    }\n}","preventionTips":["Set client request timeouts >= oidcTransactionTimeout for poll requests","Keep the polling connection open until completion","Avoid cancelling or refreshing the waiting tab during login"],"tags":["oidc","context-cancelled","timeout"],"backgroundTag":"request-timeout","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}