{"record":{"id":"2c809c54e9199c70","repo":"dromara/Sa-Token","slug":"10002-2c809c","errorCode":"10002","errorMessage":"SaTokenContext 上下文尚未初始化","messagePattern":"SaTokenContext 上下文尚未初始化","errorType":"exception","errorClass":"SaTokenContextException","httpStatus":null,"severity":"error","filePath":"sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java","lineNumber":73,"sourceCode":"\t\tmodelBoxThreadLocal.remove();\n\t}\n\n\t/**\n\t * 获取当前线程的 [ Box 存储器 ]\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBoxOrNull() {\n\t\treturn modelBoxThreadLocal.get();\n\t}\n\t\n\t/**\n\t * 获取当前线程的 [ Box 存储器 ], 如果为空则抛出异常\n\t * @return /\n\t */\n\tpublic static SaTokenContextModelBox getModelBox() {\n\t\tSaTokenContextModelBox box = modelBoxThreadLocal.get();\n\t\tif(box ==  null) {\n\t\t\tthrow new SaTokenContextException(\"SaTokenContext 上下文尚未初始化\").setCode(SaErrorCode.CODE_10002);\n\t\t}\n\t\treturn box;\n\t}\n\n\t/**\n\t * 在当前线程的 SaRequest 包装对象\n\t * \n\t * @return /\n\t */\n\tpublic static SaRequest getRequest() {\n\t\treturn getModelBox().getRequest();\n\t}\n\n\t/**\n\t * 在当前线程的 SaResponse 包装对象\n\t * \n\t * @return /\n\t */","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-core/src/main/java/cn/dev33/satoken/context/SaTokenContextForThreadLocalStaff.java#L55-L91","documentation":"SaTokenContextForThreadLocalStaff keeps the current request's model box in a ThreadLocal. getModelBox() throws code 10002 when the ThreadLocal is empty for the calling thread, i.e. setContext() was never called on this thread. Unlike the CODE_10001 errors (no integration at all), this one means the integration exists but the current thread is outside any bound request context.","triggerScenarios":"Calling SaHolder.getRequest()/getStorage() or StpUtil APIs from @Scheduled jobs, @Async methods, CompletableFuture.supplyAsync, newly spawned threads, or message consumers — threads on which the web filter never ran setContext().","commonSituations":"Background token cleanup or push jobs that reuse service methods containing StpUtil calls; async processing detached from the HTTP request; unit tests calling StpUtil without a mock context; timer threads in a servlet app.","solutions":["Move the sa-token-dependent logic back into the request thread (pass loginId as a method argument into the async code instead of calling StpUtil there)","If the async code must use sa-token, capture what you need (loginId, token) before leaving the request thread","In tests or carefully controlled non-web code, set a mock context for the thread before calling SaHolder/StpUtil and clear it after"],"exampleFix":"// before\nCompletableFuture.runAsync(() -> {\n    Object id = StpUtil.getLoginId(); // CODE_10002: no context on pool thread\n});\n\n// after: capture in the request thread\nObject id = StpUtil.getLoginId();\nCompletableFuture.runAsync(() -> {\n    process(id);\n});","handlingStrategy":"try-catch","validationCode":"// check for an active context before using SaHolder on arbitrary threads\nif (SaTokenContextForThreadLocalStaff.getModelBoxOrNull() == null) {\n    // not inside a request: capture loginId before going async instead\n}","typeGuard":null,"tryCatchPattern":"try {\n    Object id = StpUtil.getLoginId();\n} catch (SaTokenContextException e) {\n    if (SaErrorCode.CODE_10002 == e.getCode()) {\n        // thread has no bound context: fall back to a passed-in principal\n    }\n}","preventionTips":["Pass loginId/token values into async lambdas as parameters","Do not call StpUtil/SaHolder inside @Async/@Scheduled/new Thread blocks","Wrap test calls with a mock context and clear it in finally"],"tags":["sa-token","thread-local","async","context","background-job"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}