XINCGer/Unity3DTraining · error · InvalidOperationException
Duration was not a valid normalized duration
Error message
Duration was not a valid normalized duration
What it means
Duration.ToTimeSpan converts the duration only if it is normalized: nanos within [-999999999, 999999999] and seconds/nanos sign-consistent, within valid ranges. If IsNormalized fails it throws InvalidOperationException. Google.Protobuf deliberately rejects non-normalized values (e.g. 1s + 1_500_000_000ns) rather than guessing an interpretation.
Solutions
- Normalize before converting: fold Nanos into Seconds (carry 1_000_000_000 ns into 1 second, adjust signs) then call ToTimeSpan().
- Fix the producer to emit normalized durations per the proto spec (canonical form).
- Or use Duration.FromSeconds/Nanos helpers on the C# side to build durations instead of hand-assembling them.
Example fix
// before var ts = duration.ToTimeSpan(); // throws when nanos unnormalized // after long totalNanos = duration.Seconds * 1_000_000_000L + duration.Nanos; var normalized = Duration.FromTimeSpan(TimeSpan.FromTicks(totalNanos / 100)); var ts = normalized.ToTimeSpan();
Defensive patterns
Strategy: try-catch
Validate before calling
bool IsNormalizedDuration(Duration d) =>
d.Nanos >= -999999999 && d.Nanos <= 999999999 &&
(d.Seconds > 0 || d.Nanos >= 0) && (d.Seconds < 0 || d.Nanos <= 0); Type guard
bool IsValidDuration(Duration d) => d.Nanos >= -999999999 && d.Nanos <= 999999999 && Math.Sign(d.Seconds == 0 ? d.Nanos : d.Seconds) == Math.Sign(d.Nanos == 0 ? d.Seconds : d.Nanos);
Try / catch
try { return duration.ToTimeSpan(); } catch (InvalidOperationException ex) when (ex.Message.Contains("normalized")) { long n = duration.Seconds * 1_000_000_000L + duration.Nanos; return TimeSpan.FromTicks(n / 100); } Prevention
- Normalize durations at ingestion: carry nanos beyond ±999,999,999 into seconds.
- Build Duration values with Duration.FromSeconds/FromTimeSpan instead of setting fields manually.
- Validate producer output for spec-compliant (canonical) durations in contract tests.
When it happens
Trigger: Calling duration.ToTimeSpan() on a Duration whose Nanos are out of the -999999999..999999999 range, whose Seconds/Nanos signs disagree, or which was deserialized from a producer that emitted unnormalized durations (including negative values outside bounds).
Common situations: Receiving durations from non-C# services or hand-rolled protobuf encoders that don't normalize; computing durations by adding nanos without carrying into seconds.
Understand the failure class
Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.
Related errors
- Unexpected token type
- Expected string value for Duration
- Invalid Duration value
- Full type name for is ; Any message's type url is
- Non-normalized duration value
AI-assisted analysis of XINCGer/Unity3DTraining@016f98412e (2026-09-12).
Data as JSON: /api/errors/c10f42037299dda8.
Report an issue: GitHub.
Appendix: source
Thrown at NetWorkAndResources/Socket_Protobuff/Assets/Plugins/Google.Protobuf/WellKnownTypes/DurationPartial.cs:91
// We only have a problem is one is strictly negative and the other is
// strictly positive.
return Math.Sign(seconds) * Math.Sign(nanoseconds) != -1;
}
/// <summary>
/// Converts this <see cref="Duration"/> to a <see cref="TimeSpan"/>.
/// </summary>
/// <remarks>If the duration is not a precise number of ticks, it is truncated towards 0.</remarks>
/// <returns>The value of this duration, as a <c>TimeSpan</c>.</returns>
/// <exception cref="InvalidOperationException">This value isn't a valid normalized duration, as
/// described in the documentation.</exception>
public TimeSpan ToTimeSpan()
{
checked
{
if (!IsNormalized(Seconds, Nanos))
{
throw new InvalidOperationException("Duration was not a valid normalized duration");
}
long ticks = Seconds * TimeSpan.TicksPerSecond + Nanos / NanosecondsPerTick;
return TimeSpan.FromTicks(ticks);
}
}
/// <summary>
/// Converts the given <see cref="TimeSpan"/> to a <see cref="Duration"/>.
/// </summary>
/// <param name="timeSpan">The <c>TimeSpan</c> to convert.</param>
/// <returns>The value of the given <c>TimeSpan</c>, as a <c>Duration</c>.</returns>
public static Duration FromTimeSpan(TimeSpan timeSpan)
{
checked
{
long ticks = timeSpan.Ticks;
long seconds = ticks / TimeSpan.TicksPerSecond;
int nanos = (int) (ticks % TimeSpan.TicksPerSecond) * NanosecondsPerTick;View on GitHub (pinned to 016f98412e)