TryGhost/Ghost · error · Error

Invalid filter date: ${date}

Error message

Invalid filter date: ${date}

What it means

getDayBoundsInUtc converts a calendar date plus a timezone into UTC instant bounds (start of day 00:00:00.000, end of day 23:59:59.999). It calls Temporal.PlainDate.from(date) which throws if the string is not a valid ISO 8601 date (YYYY-MM-DD). The catch re-throws with a descriptive message including the offending date value.

Source

Thrown at apps/admin/src/shared/filters/filter-normalization.ts:32

            : value;

        return Temporal.Instant.from(instantValue).toZonedDateTimeISO(timezone).toPlainDate().toString();
    } catch {
        return null;
    }
}

export function getTodayInTimezone(timezone: string): string {
    return Temporal.Now.zonedDateTimeISO(timezone).toPlainDate().toString();
}

export function getDayBoundsInUtc(date: string, timezone: string): {start: string; end: string} {
    let plainDate: Temporal.PlainDate;

    try {
        plainDate = Temporal.PlainDate.from(date);
    } catch {
        throw new Error(`Invalid filter date: ${date}`);
    }

    try {
        const start = plainDate.toPlainDateTime(Temporal.PlainTime.from('00:00:00')).toZonedDateTime(timezone).toInstant();
        const end = plainDate.toPlainDateTime(Temporal.PlainTime.from('23:59:59.999')).toZonedDateTime(timezone).toInstant();

        return {
            start: start.toString({fractionalSecondDigits: 3}),
            end: end.toString({fractionalSecondDigits: 3})
        };
    } catch {
        throw new Error(`Invalid timezone: ${timezone}`);
    }
}

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Normalize the date to ISO 8601 YYYY-MM-DD before calling getDayBoundsInUtc; use Temporal.PlainDate.from(date) in a try/catch upstream or validate with /^\d{4}-\d{2}-\d{2}$/.
  2. If the input may be a datetime, extract the date portion first: value.slice(0, 10) for ISO datetime strings, or use formatDateInTimezone which accepts both date-only and legacy UTC datetime formats.
  3. Validate at the form/API boundary and reject non-ISO dates with a user-facing message before they reach this function.
  4. Use getTodayInTimezone(timezone) when you need 'today' rather than constructing a date string manually.

Example fix

// before (throws — non-padded month)
getDayBoundsInUtc('2024-1-05', 'America/New_York')
// after
getDayBoundsInUtc('2024-01-05', 'America/New_York')
Defensive patterns

Strategy: validation

Validate before calling

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function isValidFilterDate(date: string): boolean {
  if (!ISO_DATE.test(date)) return false;
  try {
    Temporal.PlainDate.from(date);
    return true;
  } catch {
    return false;
  }
}
// call: if (!isValidFilterDate(date)) { /* reject */ }

Type guard

function isIsoDateString(value: unknown): value is string {
  return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value);
}

Try / catch

try {
  const bounds = getDayBoundsInUtc(date, timezone);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid filter date')) {
    // return a 400 to the caller with the bad date
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getDayBoundsInUtc with a date string that is not strict ISO 8601 calendar date format. Examples: '2024-1-5' (non-zero-padded), '2024/01/05' (slashes), '01-05-2024' (date-first), '2024-01-05T00:00:00' (datetime, not date-only), empty string, or a locale-formatted date like 'Jan 5, 2024'.

Common situations: A date picker component emits a locale-specific or non-padded format. A backend returns a datetime string where a bare date was expected. A URL query parameter carries a user-typed date that was not normalized. Date-fns or moment formatting with a non-ISO token produces a string PlainDate.from rejects.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/6179381d92826ba3. Report an issue: GitHub.