{"record":{"id":"f7ad36a6ccd69996","repo":"siyuan-note/siyuan","slug":"encrypted-notebook-operation-scope-is-closed","errorCode":null,"errorMessage":"encrypted notebook operation scope is closed","messagePattern":"encrypted notebook operation scope is closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto_lifecycle.go","lineNumber":205,"sourceCode":"\t\tlifecycle.condition.Wait()\n\t}\n\tlifecycle.lock.Unlock()\n}\n\ntype encryptedBoxOperationScope struct {\n\tlock     sync.Mutex\n\tboxIDs   []string\n\tboxIDSet map[string]struct{}\n\tclosed   bool\n}\n\ntype encryptedBoxOperationScopeKey struct{}\n\nvar (\n\t// ErrEncryptedBoxNotUnlocked 表示加密笔记本当前未解锁。\n\tErrEncryptedBoxNotUnlocked = errors.New(\"encrypted notebook is not unlocked\")\n\t// ErrEncryptedBoxOperationScopeClosed 表示响应级操作作用域已经关闭。\n\tErrEncryptedBoxOperationScopeClosed = errors.New(\"encrypted notebook operation scope is closed\")\n)\n\n// WithEncryptedBoxOperationScope 创建覆盖整个外层响应过程的租约作用域。\nfunc WithEncryptedBoxOperationScope(ctx context.Context) (context.Context, func()) {\n\tscope := &encryptedBoxOperationScope{boxIDSet: map[string]struct{}{}}\n\tscopedContext := context.WithValue(ctx, encryptedBoxOperationScopeKey{}, scope)\n\treturn scopedContext, scope.release\n}\n\n// AcquireEncryptedBoxOperations 按固定顺序取得多个笔记本的响应级租约。\nfunc AcquireEncryptedBoxOperations(ctx context.Context, boxIDs []string) (release func(), err error) {\n\tunique := map[string]struct{}{}\n\tfor _, boxID := range boxIDs {\n\t\tif boxID != \"\" && IsEncryptedBox(boxID) {\n\t\t\tunique[boxID] = struct{}{}\n\t\t}\n\t}\n\tsortedBoxIDs := make([]string, 0, len(unique))","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto_lifecycle.go#L187-L223","documentation":"`ErrEncryptedBoxOperationScopeClosed` is returned when code tries to acquire leases from a response-scoped operation scope that has already been closed (its close function was called at the end of the outer request). Scopes exist to tie all box leases to one request lifecycle, so any acquisition after close is a programming error.","triggerScenarios":"Calling AcquireEncryptedBoxOperations with a context whose scope was closed by the deferred close function from WithEncryptedBoxOperationScope; also asserted by the test TestAcquireEncryptedBoxOperationsReportsClosedScope.","commonSituations":"Launching a goroutine that outlives the HTTP request and tries to take leases after the request handler returned; forgetting that the scope close is deferred at the start of the response flow.","solutions":["Move lease acquisition into the synchronous part of the request handler, before the deferred scope close runs.","If background work is needed, acquire leases before spawning the goroutine or use a context that is not tied to the closed scope.","Structure the handler so the scope close func runs only after all lease-holding work completes.","Check for double-invocation of the scope's close function in wrapper middleware."],"exampleFix":"// before\nscopeCtx, closeScope := model.WithEncryptedBoxOperationScope(ctx)\ngo func() { model.AcquireEncryptedBoxOperations(scopeCtx, ids) }() // scope may close first\ncloseScope()\n// after\nscopeCtx, closeScope := model.WithEncryptedBoxOperationScope(ctx)\n_, err := model.AcquireEncryptedBoxOperations(scopeCtx, ids) // acquire synchronously\nif err != nil { return err }\ncloseScope()","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"_, err := model.AcquireEncryptedBoxOperations(scopeCtx, boxIDs)\nif errors.Is(err, model.ErrEncryptedBoxOperationScopeClosed) {\n    // scope already closed: move this work into the request's synchronous path\n    return fmt.Errorf(\"lease requested after request scope closed: %w\", err)\n}","preventionTips":["Acquire all box leases synchronously before returning from or deferring close of the scope.","Never pass the scoped context to goroutines that may outlive the handler.","Keep the scope close deferred only after all lease-holding work completes."],"tags":["go","encryption","lifecycle","context"],"backgroundTag":"invalid-state-transition","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}