apolloconfig/apollo · error · BadRequestException

Token expires must be in the future

Error message

Token expires must be in the future

What it means

Thrown by UserTokenService.resolveExpires() when the requested expiry date exists (is non-null) but is not strictly after the current time (now). If expires is null, a default is computed from portalConfig.userTokenDefaultExpireDays() and this check passes. The check uses Date.after(), so a date equal to now also triggers it. BadRequestException → HTTP 400.

Source

Thrown at apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/UserTokenService.java:393

    UserInfo userInfo = userService.findByUserId(userId);
    if (userInfo == null) {
      throw BadRequestException.userNotExists(userId);
    }
    if (userInfo.getEnabled() != USER_ENABLED) {
      throw new BadRequestException("User is disabled");
    }
  }

  private Date resolveExpires(Date requestedExpires, Date now) {
    Date expires = requestedExpires;
    if (expires == null) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(now);
      calendar.add(Calendar.DAY_OF_YEAR, portalConfig.userTokenDefaultExpireDays());
      expires = calendar.getTime();
    }
    if (!expires.after(now)) {
      throw new BadRequestException("Token expires must be in the future");
    }

    Calendar maxCalendar = Calendar.getInstance();
    maxCalendar.setTime(now);
    maxCalendar.add(Calendar.DAY_OF_YEAR, portalConfig.userTokenMaxExpireDays());
    if (expires.after(maxCalendar.getTime())) {
      throw new BadRequestException("Token expires exceeds max allowed days:%s",
          portalConfig.userTokenMaxExpireDays());
    }
    return expires;
  }

  private int resolveRateLimit(Integer rateLimit) {
    return rateLimit == null ? 0 : rateLimit;
  }

  private void validateOperator(String operator) {
    if (StringUtils.isBlank(operator)) {

View on GitHub (pinned to d95fc18d11)

Solutions

  1. Set expires to a date clearly in the future (e.g., now + N days).
  2. If you want the default expiry, leave request.setExpires() as null — the service computes now + userTokenDefaultExpireDays.
  3. Ensure client and server clocks are synchronized (NTP) to avoid false past-date rejections.

Example fix

// before
request.setExpires(yesterday);
// after
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 30);
request.setExpires(cal.getTime());
Defensive patterns

Strategy: validation

Validate before calling

Date expires = request.getExpires();
if (expires != null && !expires.after(new Date())) {
    throw new IllegalArgumentException("Expiry must be in the future");
}
userTokenService.createToken(request, operator);

Type guard

static boolean isExpiryInFuture(Date expires) {
    return expires == null || expires.after(new Date());
}

Try / catch

try {
    userTokenService.createToken(request, operator);
} catch (BadRequestException e) {
    if (e.getMessage().contains("must be in the future")) {
        return Response.status(400).entity("Set expiry to a future date or leave it null for default").build();
    }
    throw e;
}

Prevention

When it happens

Trigger: Calling createToken() with request.setExpires() set to a past date, the current instant, or null-but-overridden-after. For example, setting expires to yesterday's timestamp, or a date that has already elapsed by the time the request reaches the server.

Common situations: Client clock skew sends a timestamp that the server considers past. Stale cached request retried after the expiry date has passed. Test or migration script hardcoded a fixed past date.

Related errors


AI-assisted analysis of apolloconfig/apollo@d95fc18d11 (2026-08-14). Data as JSON: /api/errors/3fd574263d79478a. Report an issue: GitHub.