kataras/iris · error

invalid offset hours

Error message

invalid offset hours: %d: %s

What it means

After parsing the hours of an unconventional '+HH:MM:SS' offset, adjustForUnconventionalOffset rejects hour values greater than 24, since a UTC offset hour component can never exceed 24. The parsed hours and the full offset string are included in the message.

Solutions

  1. Correct the offset so its hour component is within 0–24 (e.g. '+03:00:00').
  2. Verify the producer is not swapping fields (e.g. putting a year or day value into the offset hours slot).
  3. Validate offsets at ingestion with a regex like ^[+-](0?[0-9]|1[0-9]|2[0-4]):[0-5]?[0-9]:[0-5]?[0-9]$ before parsing.

Example fix

// before
ParseISO8601("2024-01-02T15:04:05+30:00:00") // invalid offset hours: 30
// after
ParseISO8601("2024-01-02T15:04:05+03:00:00")
Defensive patterns

Strategy: validation

Validate before calling

func offsetHoursInRange(ts string) bool {
	i := strings.LastIndexAny(ts, "+-")
	if i < 0 { return false }
	parts := strings.Split(ts[i+1:], ":")
	if len(parts) != 3 { return false }
	h, err := strconv.Atoi(parts[0])
	return err == nil && h >= 0 && h <= 24
}

Try / catch

tt, err := jsonx.ParseISO8601(s)
if err != nil {
	if strings.Contains(err.Error(), "invalid offset hours") {
		return fmt.Errorf("timestamp %q has out-of-range UTC offset", s)
	}
	return err
}

Prevention

When it happens

Trigger: ParseISO8601 on a timestamp with an offset like '+30:00:00' or '+99:00:00' — digits parse fine but exceed the 24-hour bound.

Common situations: Malformed timestamps produced by buggy serializers (e.g. concatenating wrong fields); hand-built offset strings; tests with deliberately bogus offsets.

Related errors


AI-assisted analysis of kataras/iris@7bedaf55a0 (2026-08-30). Data as JSON: /api/errors/24f50d5cab1753ca. Report an issue: GitHub.

Appendix: source

Thrown at x/jsonx/iso8601.go:241

func adjustForUnconventionalOffset(t time.Time, offset string) (time.Time, error) {
	sign := 1
	if offset[0] == '-' {
		sign = -1
	}
	offset = offset[1:]

	offsetParts := strings.Split(offset, ":")
	if len(offsetParts) != 3 {
		return time.Time{}, fmt.Errorf("invalid offset format: %s", offset)
	}

	hours, err := strconv.Atoi(offsetParts[0])
	if err != nil {
		return time.Time{}, fmt.Errorf("error parsing offset hours: %s: %w", offset, err)
	}

	if hours > 24 {
		return time.Time{}, fmt.Errorf("invalid offset hours: %d: %s", hours, offset)
	}

	minutes, err := strconv.Atoi(offsetParts[1])
	if err != nil {
		return time.Time{}, fmt.Errorf("error parsing offset minutes: %s: %w", offset, err)
	}
	if minutes > 60 {
		return time.Time{}, fmt.Errorf("invalid offset minutes: %d: %s", minutes, offset)
	}

	seconds, err := strconv.Atoi(offsetParts[2])
	if err != nil {
		return time.Time{}, fmt.Errorf("error parsing offset seconds: %s: %w", offset, err)
	}

	if seconds > 60 {
		return time.Time{}, fmt.Errorf("invalid offset seconds: %d: %s", seconds, offset)
	}

View on GitHub (pinned to 7bedaf55a0)