go-kratos/kratos · error

%s: nanos out of range %v

Error message

%s: nanos out of range %v

What it means

Form-encoding error while marshaling a google.protobuf.Timestamp: the nanoseconds component is negative or greater than secondsInNanos (999999999). Valid proto timestamps require 0 <= nanos <= 999999999; the codec enforces this before time.Unix formatting, mirroring protojson validation.

Source

Thrown at encoding/form/well_known_types.go:51

	structFieldsFieldNumber protoreflect.FieldNumber = 1

	fieldMaskFullName protoreflect.FullName = "google.protobuf.FieldMask"
)

func marshalTimestamp(m protoreflect.Message) (string, error) {
	fds := m.Descriptor().Fields()
	fdSeconds := fds.ByNumber(timestampSecondsFieldNumber)
	fdNanos := fds.ByNumber(timestampNanosFieldNumber)

	secsVal := m.Get(fdSeconds)
	nanosVal := m.Get(fdNanos)
	secs := secsVal.Int()
	nanos := nanosVal.Int()
	if secs < minTimestampSeconds || secs > maxTimestampSeconds {
		return "", fmt.Errorf("%s: seconds out of range %v", timestampMessageFullname, secs)
	}
	if nanos < 0 || nanos > secondsInNanos {
		return "", fmt.Errorf("%s: nanos out of range %v", timestampMessageFullname, nanos)
	}
	// Uses RFC 3339, where generated output will be Z-normalized and uses 0, 3,
	// 6 or 9 fractional digits.
	t := time.Unix(secs, nanos).Local()
	x := t.Format("2006-01-02T15:04:05.000000000")
	x = strings.TrimSuffix(x, "000")
	x = strings.TrimSuffix(x, "000")
	x = strings.TrimSuffix(x, ".000")
	return x + "Z", nil
}

func marshalDuration(m protoreflect.Message) (string, error) {
	fds := m.Descriptor().Fields()
	fdSeconds := fds.ByNumber(durationSecondsFieldNumber)
	fdNanos := fds.ByNumber(durationNanosFieldNumber)

	secsVal := m.Get(fdSeconds)
	nanosVal := m.Get(fdNanos)

View on GitHub (pinned to 668db92c2c)

Solutions

  1. Always construct with timestamppb.New(time.Time) which normalizes seconds/nanos correctly
  2. When converting manually, normalize: carry nanos >= 1e9 into seconds and flip sign handling for negatives before setting fields
  3. Sanitize inbound timestamps at the trust boundary (validate 0 <= nanos <= 999999999, reject otherwise) before they reach form encoding
  4. For interop with runtimes emitting normalized-negative pairs, recombine into a single seconds value (sec = s; if s < 0 && n > 0 { sec++; n -= 1e9 })

Example fix

// before: un-normalized hand-built timestamp
ts := &timestamppb.Timestamp{Seconds: 10, Nanos: 1500000000}

// after: constructor normalizes automatically
ts := timestamppb.New(time.Unix(11, 500000000))
Defensive patterns

Strategy: validation

Validate before calling

// Validate nanos range before encoding
func validTimestampNanos(nanos int32) bool {
	return nanos >= 0 && nanos <= 999999999
}

Type guard

func validTimestampMsg(ts *timestamppb.Timestamp) bool {
	return ts == nil || (validTimestampSeconds(ts.Seconds) && validTimestampNanos(ts.Nanos))
}

Try / catch

if _, err := form.EncodeField(fd, val); err != nil {
	if strings.Contains(err.Error(), "nanos out of range") {
		ts := val.Message().Interface().(*timestamppb.Timestamp)
		normalized := timestamppb.New(ts.AsTime()) // re-derive from a time.Time
		_ = normalized
	}
}

Prevention

When it happens

Trigger: Encoding a Timestamp constructed by hand with invalid nanos: passing a negative fraction, nanos borrowed across a second boundary incorrectly (e.g. {Seconds:-1, Nanos:500000000} style normalized-negative representations from other runtimes), nanos holding a full second or more (1000000000+), or a millisecond value mistakenly written into nanos scaled wrong (e.g. 1e6*ms overflow patterns).

Common situations: Interoperating with C++/Python protobuf code that normalizes negative durations into {negative seconds, positive nanos} and that pattern leaking into Timestamp; hand-rolled conversions in ETL jobs; copy-paste from Duration math where nanos can exceed a second before normalization.

Related errors


AI-assisted analysis of go-kratos/kratos@668db92c2c (2026-08-16). Data as JSON: /api/errors/90a34b919b46866e. Report an issue: GitHub.