JuliusBrussee/caveman · error
%s %s/%s: verified_at must be RFC3339
Error message
%s %s/%s: verified_at must be RFC3339
What it means
Thrown by the catalog YAML loader when an entry's verified_at timestamp cannot be parsed as RFC3339, or parses to the zero time. Each price row must carry a machine-parseable verification timestamp so consumers can judge staleness; free-text or wrongly formatted dates are rejected at load.
Source
Thrown at shared/platform/catalog/catalog.go:274
}
if len(decoded) == 0 {
return nil, fmt.Errorf("catalog is empty")
}
seen := make(map[string]struct{}, len(decoded))
for i, entry := range decoded {
label := fmt.Sprintf("entry %d", i)
if strings.TrimSpace(entry.Provider) == "" || strings.TrimSpace(entry.Model) == "" || strings.TrimSpace(entry.Region) == "" {
return nil, fmt.Errorf("%s: provider, model, and region are required", label)
}
if entry.Currency != "USD" {
return nil, fmt.Errorf("%s %s/%s: unsupported currency %q", label, entry.Provider, entry.Model, entry.Currency)
}
if !cost.ValidPrice(entry.Pricing) {
return nil, fmt.Errorf("%s %s/%s: invalid pricing", label, entry.Provider, entry.Model)
}
verified, err := time.Parse(time.RFC3339, entry.VerifiedAt)
if err != nil || verified.IsZero() {
return nil, fmt.Errorf("%s %s/%s: verified_at must be RFC3339", label, entry.Provider, entry.Model)
}
if verified.After(time.Now().UTC().Add(24 * time.Hour)) {
return nil, fmt.Errorf("%s %s/%s: verified_at is in the future", label, entry.Provider, entry.Model)
}
if entry.CapabilitiesVerifiedAt != "" {
capVerified, err := time.Parse(time.RFC3339, entry.CapabilitiesVerifiedAt)
if err != nil || capVerified.IsZero() {
return nil, fmt.Errorf("%s %s/%s: capabilities_verified_at must be RFC3339", label, entry.Provider, entry.Model)
}
if capVerified.After(time.Now().UTC().Add(24 * time.Hour)) {
return nil, fmt.Errorf("%s %s/%s: capabilities_verified_at is in the future", label, entry.Provider, entry.Model)
}
}
if len(entry.Sources) == 0 {
return nil, fmt.Errorf("%s %s/%s: at least one source is required", label, entry.Provider, entry.Model)
}
for _, rawURL := range entry.Sources {
u, err := url.ParseRequestURI(rawURL)View on GitHub (pinned to 27d5a3981a)
Solutions
- Use a full RFC3339 timestamp with timezone, e.g. verified_at: "2026-01-05T10:00:00Z".
- Generate timestamps programmatically (time.Now().UTC().Format(time.RFC3339)) instead of typing them.
- Remember the same format is enforced for capabilities_verified_at when that optional field is present.
Example fix
# before verified_at: 2026-01-05 # after verified_at: 2026-01-05T10:00:00Z
Defensive patterns
Strategy: validation
Validate before calling
const rfc3339RE = `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`
func validRFC3339(s string) bool {
t, err := time.Parse(time.RFC3339, s)
return err == nil && !t.IsZero()
} Try / catch
Fail catalog load at startup; the timestamp must be corrected in the YAML.
Prevention
- Generate timestamps with date -u +%Y-%m-%dT%H:%M:%SZ, never type them.
- Always include the timezone (Z or +HH:MM).
- Lint both verified_at and capabilities_verified_at with time.Parse(time.RFC3339, ...) in CI.
When it happens
Trigger: verified_at values like "2026-01-05" (date only), "Jan 5, 2026", "2026-01-05T10:00:00" (no timezone), or a missing field (empty string) — all fail time.Parse(time.RFC3339, ...) or produce a zero time.
Common situations: Hand-entering human-style dates; omitting the timezone offset; a scraper writing ISO dates without the 'T' or the +00:00 suffix.
Related errors
- %s %s/%s: capabilities_verified_at must be RFC3339
- %s %s/%s: verified_at is in the future
- %s %s/%s: capabilities_verified_at is in the future
- ${label}:${lineNo}: "${text}" is not a finite number
- ${label}: row is missing a string "${field}"
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/6b3a454914e78bc2.
Report an issue: GitHub.