YunaiV/yudao-cloud · error · NullPointerException
TenantContextHolder 不存在租户编号!可参考文档:https://doc.iocoder.cn
Error message
TenantContextHolder 不存在租户编号!可参考文档:https://doc.iocoder.cn
What it means
TenantContextHolder stores the tenant id in a ThreadLocal (set by the tenant filter/interceptor from the 'tenant-id' header). getRequiredTenantId() throws NullPointerException when the ThreadLocal is empty — meaning the executing thread never went through tenant context setup. The message links to the framework's tenant documentation.
Source
Thrown at yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/context/TenantContextHolder.java:41
/**
* 获得租户编号
*
* @return 租户编号
*/
public static Long getTenantId() {
return TENANT_ID.get();
}
/**
* 获得租户编号。如果不存在,则抛出 NullPointerException 异常
*
* @return 租户编号
*/
public static Long getRequiredTenantId() {
Long tenantId = getTenantId();
if (tenantId == null) {
throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"
+ DocumentEnum.TENANT.getUrl());
}
return tenantId;
}
public static void setTenantId(Long tenantId) {
TENANT_ID.set(tenantId);
}
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
/**
* 当前是否忽略租户
*
* @return 是否忽略
*/View on GitHub (pinned to 477be9dd49)
Solutions
- Send the tenant-id header (or login first so the token carries tenant) for HTTP calls.
- For background/async code, propagate context: TenantUtils.execute(tenantId, () -> ...) or capture and setTenantId(...) in the worker thread (and clear it after).
- Add the endpoint to tenant ignore-urls only if it is genuinely tenant-free — otherwise fix context propagation, not the ignore list.
- Verify filter order: TenantContextFilter must wrap any component that reads the context.
Example fix
// before
@Async
public void notify(Long userId) {
Long tenantId = TenantContextHolder.getRequiredTenantId(); // NPE: no context
}
// after
@Async
public void notify(Long tenantId, Long userId) {
TenantUtils.execute(tenantId, () -> {
// tenant-aware work
});
} Defensive patterns
Strategy: validation
Validate before calling
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
tenantId = resolveTenantFromRequestOrJob(); // or throw a descriptive error
}
// only then call getRequiredTenantId()-equivalent logic Try / catch
try {
Long tenantId = TenantContextHolder.getRequiredTenantId();
} catch (NullPointerException e) {
throw new IllegalStateException("No tenant context on thread " + Thread.currentThread().getName()
+ " — wrap work in TenantUtils.execute(tenantId, ...)");
} Prevention
- Wrap async/job bodies in TenantUtils.execute(tenantId, runnable) and clear context after
- Always send the tenant-id header (or use token-based tenant) in API clients
- Check TenantContextHolder.getTenantId() for null before any required access in non-web threads
When it happens
Trigger: Calling getRequiredTenantId() from a thread without tenant context: @Async methods, scheduled jobs, new Thread(...), message listeners, or a request that bypassed TenantContextFilter (missing tenant-id header while the URL is not in the ignore-urls list).
Common situations: Adding @Async to a service that reads tenant; Quartz/XXL-Job handlers touching tenant-aware mappers; websocket/SimpV2 threads; forgetting the tenant-id header in API tests; security framework ordering that puts auth before tenant filter incorrectly.
Related errors
AI-assisted analysis of YunaiV/yudao-cloud@477be9dd49 (2026-08-14).
Data as JSON: /api/errors/e32b65630b36543e.
Report an issue: GitHub.