{"record":{"id":"5e0ba046af45fdee","repo":"siyuan-note/siyuan","slug":"oidc-login-transaction-capacity-reached","errorCode":null,"errorMessage":"OIDC login transaction capacity reached","messagePattern":"OIDC login transaction capacity reached","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/oidc.go","lineNumber":711,"sourceCode":"\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn &oidcTransaction{State: state, Nonce: nonce, CodeVerifier: verifier, PollToken: pollToken, Binding: binding,\n\t\tClientIP: clientIP,\n\t\tFlow:     input.Flow, RedirectURL: redirectURL, To: input.To, ConfigVersion: oidcConfigurationVersion(Conf.GetOIDC()), RememberMe: input.RememberMe,\n\t\tExpiresAt: time.Now().Add(oidcTransactionTimeout), Done: make(chan struct{})}, nil\n}\n\nfunc storeOIDCTransaction(transaction *oidcTransaction) error {\n\toidcTransactions.Lock()\n\tdefer oidcTransactions.Unlock()\n\tcleanupOIDCTransactionsLocked()\n\tif transaction.Done == nil {\n\t\ttransaction.Done = make(chan struct{})\n\t}\n\tif len(oidcTransactions.byState) >= oidcTransactionMax {\n\t\treturn errors.New(\"OIDC login transaction capacity reached\")\n\t}\n\tperIP, perBinding := 0, 0\n\tfor _, candidate := range oidcTransactions.byState {\n\t\tif candidate.Completed {\n\t\t\tcontinue\n\t\t}\n\t\tif transaction.ClientIP != \"\" && candidate.ClientIP == transaction.ClientIP {\n\t\t\tperIP++\n\t\t}\n\t\tif transaction.Binding != \"\" && candidate.Binding == transaction.Binding {\n\t\t\tperBinding++\n\t\t}\n\t}\n\tif perIP >= oidcTransactionPerIP || perBinding >= oidcTransactionPerBind {\n\t\treturn errors.New(\"too many pending OIDC login transactions\")\n\t}\n\toidcTransactions.byState[transaction.State] = transaction\n\tif transaction.PollToken != \"\" {","sourceCodeStart":693,"sourceCodeEnd":729,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/oidc.go#L693-L729","documentation":"storeOIDCTransaction refuses new entries once the in-memory store holds oidcTransactionMax (512) transactions server-wide. This bounds memory; expired entries are only reaped lazily on the next store/claim call via cleanupOIDCTransactionsLocked.","triggerScenarios":"A burst of /api/system/oidc/start calls (legitimate or abusive) that outpaces the 10-minute transaction expiry reap; clients that start login but never complete the IdP callback.","commonSituations":"Bot or scanner hitting the start endpoint; many users behind one NAT starting flows simultaneously; abandoned browser tabs each holding a transaction.","solutions":["Wait roughly 10 minutes for expiry reap to free slots.","Restart the kernel to clear the in-memory transaction store if urgent.","Investigate source IPs/bindings - if a single source dominates, it is likely abuse and should be rate-limited at the edge."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// On capacity errors, back off and retry; the lazy reaper frees slots as transactions expire.\nif err := storeOIDCTransaction(tx); err != nil {\n    if strings.Contains(err.Error(), \"capacity reached\") {\n        time.Sleep(retryBackoff)\n        // retry start, or surface 'service busy, retry shortly'\n    }\n}","preventionTips":["Rate-limit /api/system/oidc/start at the edge per IP.","Ensure clients complete the IdP callback promptly rather than abandoning flows.","Monitor pending transaction count and alert well below 512."],"tags":["oidc","capacity","dos-protection","concurrency"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}