Skip to content

Nullability issue in OtelSpan.error() when Throwable.getMessage() returns null #1320

@roharaiev

Description

@roharaiev

The OtelSpan.error(Throwable throwable) method in micrometer-tracing-bridge-otel causes a NullPointerException when the throwable's message is null, which violates the OpenTelemetry API contract for non-nullable parameters.

The current implementation in io.micrometer.tracing.otel.bridge.OtelSpan (lines 172-177):

@Override
public Span error(Throwable throwable) {
    this.delegate.recordException(throwable);
    this.delegate.setStatus(StatusCode.ERROR, throwable.getMessage());
    return this;
}

Calls throwable.getMessage() which can return null, but passes it to setStatus(StatusCode, String description) where the description parameter is specified as non-null in the OpenTelemetry API signature:

Span setStatus(StatusCode statusCode, String description);

Impact:

This causes runtime failures when:

  1. Working with exceptions that have null messages (e.g., ReadTimeoutException, custom exceptions without messages)
  2. Using Kotlin implementations of the OpenTelemetry Span interface, which enforce null-safety at runtime
  3. Custom span wrappers that delegate to multiple spans

Example Failure:

java.lang.NullPointerException: Parameter specified as non-null is null: method com.test.observability.CompositeSpan.setStatus, parameter description
    at com.test.observability.CompositeSpan.setStatus(CompositeSpan.kt)
    at io.micrometer.tracing.otel.bridge.OtelSpan.error(OtelSpan.java:175)
    at io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler.onError(PropagatingSenderTracingObservationHandler.java:94)

This occurs when handling exceptions like io.netty.handler.timeout.ReadTimeoutException, which has a null message.

Proposed Fix:

Use a fallback value when throwable.getMessage() is null:

@Override
public Span error(Throwable throwable) {
    this.delegate.recordException(throwable);
    this.delegate.setStatus(StatusCode.ERROR,
        throwable.getMessage() != null ? throwable.getMessage() : throwable.getClass().getName());
    return this;
}

Alternative Solutions:

  1. Use empty string as fallback: ""
  2. Use exception class simple name: throwable.getClass().getSimpleName()

Using the exception class name seems most informative for observability purposes.

Reproducibility:

Create any exception without a message and pass it to the error() method when using a Kotlin-based Span implementation or any implementation that validates non-null parameters.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions