{"record":{"id":"8a5df1d2248dbc74","repo":"goharbor/harbor","slug":"nil-stats-body","errorCode":null,"errorMessage":"nil stats body","messagePattern":"nil stats body","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/jobservice/job/models.go","lineNumber":118,"sourceCode":"// StatusChange is designed for reporting the status change via hook.\ntype StatusChange struct {\n\tJobID    string     `json:\"job_id\"`\n\tStatus   string     `json:\"status\"`\n\tCheckIn  string     `json:\"check_in,omitempty\"`\n\tMetadata *StatsInfo `json:\"metadata,omitempty\"`\n}\n\n// SimpleStatusChange only keeps job ID and the target status\ntype SimpleStatusChange struct {\n\tJobID        string `json:\"job_id\"`\n\tTargetStatus string `json:\"target_status\"`\n\tRevision     int64  `json:\"revision\"`\n}\n\n// Validate the job stats\nfunc (st *Stats) Validate() error {\n\tif st.Info == nil {\n\t\treturn errors.New(\"nil stats body\")\n\t}\n\n\tif utils.IsEmptyStr(st.Info.JobID) {\n\t\treturn errors.New(\"missing job ID in job stats\")\n\t}\n\n\tif utils.IsEmptyStr(st.Info.JobName) {\n\t\treturn errors.New(\"missing job name in job stats\")\n\t}\n\n\tif utils.IsEmptyStr(st.Info.JobKind) {\n\t\treturn errors.New(\"missing job name in job stats\")\n\t}\n\n\tif st.Info.JobKind != KindGeneric &&\n\t\tst.Info.JobKind != KindPeriodic &&\n\t\tst.Info.JobKind != KindScheduled {\n\t\treturn errors.Errorf(\"job kind is not supported: %s\", st.Info.JobKind)","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/goharbor/harbor/blob/7b2fd08cc568955cca339afeefab27372840d936/src/jobservice/job/models.go#L100-L136","documentation":"job.Stats.Validate (src/jobservice/job/models.go:118) rejects stats lacking the embedded Info object (JSON 'info'). Stats records are persisted to the worker backend and validated on creation/read; a payload without 'info' means the record was truncated, corrupt, or hand-built incorrectly.","triggerScenarios":"json.Unmarshal of a payload like {} or {\"revision\":1} into job.Stats followed by Validate; a manager reading a Redis/DB stats entry whose info JSON was written by an incompatible Harbor version.","commonSituations":"Harbor version upgrades with stats schema drift; sharing a Redis instance across Harbor versions; manually crafted test fixtures; partially migrated job records.","solutions":["When creating stats, always populate Info with job_id, job_name, and job_kind","For corrupt persisted stats, delete/recreate the job entry rather than bypassing validation","Keep producer and consumer Harbor versions aligned on the job.Stats schema"],"exampleFix":"// before\nst := &job.Stats{}\n// after\nst := &job.Stats{\n    Info: &job.StatsInfo{\n        JobID:   \"ju-1234\",\n        JobName: \"GARBAGE_COLLECTION\",\n        JobKind: job.KindGeneric,\n    },\n}","handlingStrategy":"type-guard","validationCode":"if err := st.Validate(); err != nil {\n    return err // fail before persisting or forwarding stats\n}","typeGuard":"func hasStatsInfo(st *job.Stats) bool {\n    return st != nil && st.Info != nil\n}","tryCatchPattern":null,"preventionTips":["Populate Info (job_id, job_name, job_kind) whenever constructing job.Stats","Call Stats.Validate() before persisting or forwarding stats","Do not share a Redis job namespace across Harbor versions"],"tags":["jobservice","job-stats","validation","serialization","harbor"],"backgroundTag":null,"analyzedSha":"7b2fd08cc568955cca339afeefab27372840d936","analyzedAt":"2026-08-16T00:00:10.961Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}