kataras/iris · error
ISO8601: invalid timezone offset sign: %c
Error message
ISO8601: invalid timezone offset sign: %c
What it means
The 6+-character offset tail does not begin with '+' or '-'; the first byte is something else, so it is not a signed UTC offset. parseOffsetToSeconds checks the sign character explicitly before parsing hours/minutes. This usually means the LastIndexFunc match found a '+'/'-' earlier in the string and the actual tail is not an offset, or the string is malformed.
Source
Thrown at x/jsonx/iso8601.go:197
return time.Time{}, err
}
loc, ok := fixedEastUTCLocations[secondsEastUTC]
if !ok {
loc = time.FixedZone("", secondsEastUTC)
}
return time.ParseInLocation(ISO8601ZUTCOffsetLayoutWithoutMicroseconds, s, loc)
}
func parseOffsetToSeconds(offsetText string) (int, error) {
if len(offsetText) < 6 {
return 0, fmt.Errorf("ISO8601: invalid timezone offset length: %s", offsetText)
}
sign := offsetText[0]
if sign != '-' && sign != '+' {
return 0, fmt.Errorf("ISO8601: invalid timezone offset sign: %c", sign)
}
hours, err := strconv.Atoi(offsetText[1:3])
if err != nil {
return 0, fmt.Errorf("ISO8601: %w", err)
}
minutes, err := strconv.Atoi(offsetText[4:6])
if err != nil {
return 0, fmt.Errorf("ISO8601: %w", err)
}
secondsEastUTC := (hours*60 + minutes) * 60
if sign == '-' {
secondsEastUTC = -secondsEastUTC
}
return secondsEastUTC, nilView on GitHub (pinned to 7bedaf55a0)
Solutions
- Ensure the offset starts with an explicit sign: '+03:00' or '-05:00', not '03:00'.
- For UTC use 'Z' so the parser takes the ISO8601LayoutWithTimezone branch instead of the offset path.
- Validate input with a regex like ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([+-]\d{2}:\d{2}|Z)?$ before parsing and reject bad values with a clear app-level message.
- Trace where the string was built; a replace/trim step is likely mangling the sign character.
Example fix
// before
tt, err := jsonx.ParseISO8601("2024-01-02T15:04:05 03:00") // invalid timezone offset sign: ' '
// after
tt, err := jsonx.ParseISO8601("2024-01-02T15:04:05+03:00") // ok Defensive patterns
Strategy: validation
Validate before calling
var signedOffsetRe = regexp.MustCompile(`^[+-]\d{2}:\d{2}$`)
func offsetIsSigned(s string) bool {
i := strings.LastIndexAny(s, "+-")
return i != -1 && signedOffsetRe.MatchString(s[i:])
} Type guard
func tailStartsWithSign(s string) bool {
i := strings.LastIndexAny(s, "+-")
return i != -1 && i == len(s)-6
} Try / catch
tt, err := jsonx.ParseISO8601(s)
if err != nil {
if strings.Contains(err.Error(), "invalid timezone offset sign") {
return fmt.Errorf("timestamp %q: offset must start with '+' or '-' (or use 'Z' for UTC)", s)
}
return err
} Prevention
- Use 'Z' for UTC instead of an offset without a sign
- Reject locale-formatted datetimes at input boundaries; accept only ISO8601
- Avoid manual string surgery on timestamps that can drop the sign character
- Validate the tail matches ^[+-]\d{2}:\d{2}$ before calling the parser
When it happens
Trigger: parseWithOffset passes a tail like 'T15:04:05' (no sign at position 0), e.g. input '2024-01-02T15:04:05' where LastIndexFunc found the '-' inside the date when offsets are searched incorrectly, or garbage tails like '...+x3:00' where offsetText[0] is not reached — more precisely any offsetText whose byte 0 is neither '+' nor '-': e.g. ' 03:00' or 'Z03:00'.
Common situations: Strings containing a dash in the date combined with odd slicing by upstream code; timestamps where 'Z' was replaced by a space ('2024-01-02T15:04:05 03:00'); locale-formatted datetimes sneaking into an ISO8601 field; corrupted strings after manual string manipulation.
Related errors
- ISO8601: invalid timezone offset length: %s
- ISO8601: %w
- ISO8601: invalid timezone format: %s
- ISO8601: missing timezone offset
- invalid offset format: %s
AI-assisted analysis of kataras/iris@7bedaf55a0 (2026-08-30).
Data as JSON: /api/errors/22bb0361b9f94435.
Report an issue: GitHub.