jaegertracing/jaeger · error

%w: the bound %q on %q was never read as a duration

Error message

%w: the bound %q on %q was never read as a duration

What it means

A duration bound on span.duration must carry a *expression.DurationValue. When the constant is untyped (*expression.AnyValue), it means the constant was never resolved as a duration at all — expression.ResolveConstants reads the span.duration field's declared type and rejects unparseable spellings on the way in, so an AnyValue here indicates the constant bypassed that resolution. Because the value is invalid rather than merely unsupported, this uses ErrFilterInvalid and reports the raw constant text plus the field name.

Source

Thrown at internal/storage/v2/api/tracestore/shape.go:279

// untyped one, which is what an unqualified tag equality has always been. A constant of any other
// type asks for a match on a type these fields cannot name.
func textConstant(name string, value expression.Expression) (string, error) {
	switch constant := value.(type) {
	case *expression.StringValue:
		return constant.Value, nil
	case *expression.AnyValue:
		return constant.Value, nil
	default:
		return "", fmt.Errorf("%w: it compares %q against a string constant only", ErrFilterUnsupported, name)
	}
}

// errNotADuration refuses a bound that is not a length of time. An untyped constant reaching here
// was never read as a duration, which expression.ResolveConstants does on the way in, so the
// refusal says that rather than blaming the spelling the caller wrote.
func errNotADuration(name string, value expression.Expression) error {
	if constant, ok := value.(*expression.AnyValue); ok {
		return fmt.Errorf("%w: the bound %q on %q was never read as a duration",
			ErrFilterInvalid, constant.Value, name)
	}
	return fmt.Errorf(`%w: it compares %q against a duration such as "2s" only`,
		ErrFilterUnsupported, name)
}

func errUnsupportedOperator(op expression.Operator) error {
	return fmt.Errorf("%w: it does not support the operator %q", ErrFilterUnsupported, op)
}

func errUnsupportedOperatorOn(op expression.Operator, name string) error {
	return fmt.Errorf("%w: it does not support the operator %q on %q", ErrFilterUnsupported, op, name)
}

func errRepeatedPredicateOn(name string) error {
	return fmt.Errorf("%w: it can carry only one predicate on %q", ErrFilterUnsupported, name)
}

View on GitHub (pinned to 806f444784)

Solutions

  1. Run the filter expression through expression.ResolveConstants before passing it to ToLegacyShape / FindTraces.
  2. Build the value as *expression.DurationValue{Value: parsedDuration} explicitly (e.g. time.ParseDuration("2s")).
  3. Validate the constant parses as a duration before constructing the predicate.

Example fix

// before
&expression.Call{Op: expression.OpGte, Args: []expression.Expression{durField, &expression.AnyValue{Value: "2s"}}}
// after
resolved := expression.ResolveConstants(filter) // AnyValue under span.duration becomes DurationValue
_ = resolved
Defensive patterns

Strategy: validation

Validate before calling

func ensureDurationResolved(f *expression.Call) error {
  for _, p := range conjuncts(f) {
    if ref, ok := p.Args[0].(*expression.FieldRef); ok && ref.Name == expression.SpanFieldDuration {
      if _, ok := p.Args[1].(*expression.DurationValue); !ok {
        return fmt.Errorf("span.duration bound must be a DurationValue, got %T", p.Args[1])
      }
    }
  }
  return nil
}

Type guard

func isDurationValue(v expression.Expression) bool { _, ok := v.(*expression.DurationValue); return ok }

Try / catch

legacy, err := params.ToLegacyShape()
if errors.Is(err, tracestore.ErrFilterInvalid) {
  return fmt.Errorf("duration constant was not resolved; run expression.ResolveConstants first: %w", err)
}

Prevention

When it happens

Trigger: applyDurationBound (via ToLegacyShape) receives a span.duration >=/<= predicate whose value is *expression.AnyValue, i.e. a raw untyped constant string like '2s' that never went through expression.ResolveConstants.

Common situations: Hand-constructed *expression.Call filter trees that skip constant resolution; an interceptor layer that passes raw user text into the value slot; version drift between the filter builder and the resolver step.

Related errors


AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01). Data as JSON: /api/errors/98b2df2027b24970. Report an issue: GitHub.