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

  1. Send the tenant-id header (or login first so the token carries tenant) for HTTP calls.
  2. For background/async code, propagate context: TenantUtils.execute(tenantId, () -> ...) or capture and setTenantId(...) in the worker thread (and clear it after).
  3. Add the endpoint to tenant ignore-urls only if it is genuinely tenant-free — otherwise fix context propagation, not the ignore list.
  4. 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

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.