Skip to content

Harmonize @Nullable annotations in okhttp instrumentation - #7375

Merged
jonatan-ivanov merged 1 commit into
micrometer-metrics:1.16.xfrom
jonatan-ivanov:okhttp-vs-jspecify
Apr 7, 2026
Merged

jonatan-ivanov merged 1 commit into
micrometer-metrics:1.16.xfrom
jonatan-ivanov:okhttp-vs-jspecify

Conversation

@jonatan-ivanov

Copy link
Copy Markdown
Member

This is a just firefighting and not a proper fix.
The proper fix would be non-null request and nullable response but fixing that seems to be a very deep rabbit hole and also involves breaking changes.

Closes gh-7373

This is a just firefighting and not a proper fix.
The proper fix would be non-null request and nullable response but
fixing that seems to be a very deep rabbit hole and also involves
breaking changes.

Closes micrometer-metricsgh-7373

@shakuzen shakuzen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the right fix to me. I'm not sure why we didn't do this to begin with and why it wasn't caught by errorprone. I don't think we can know the Request is not null unless we check because it is marked @Nullable on the CallState, and the person who reported the issue apparently ran into it being null in some case.

@jckoenen jckoenen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jonatan-ivanov Thanks a lot for the quick reply and the PR! I reviewed it as best as I could, but obviously you are way more experienced in this codebase, so please disregard it if I'm wrong.

Meanwhile I will try to reproduce this better in isolation.

the person who reported the issue apparently ran into [the request] being null in some case.

@shakuzen FWIW, I actually only observed the Response being null. The Request was not.
Sorry, the original issue should've made that more clear.

}

private String getUriTag(Function<Request, String> urlMapper, @Nullable Request request) {
private String getUriTag(Function<@Nullable Request, String> urlMapper, @Nullable Request request) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking into account the if condition on the following line, I believe the urlMapper can remain untouched. AFAICT there are no unchecked usages of OkhttpContext.getUrlMapper in the micrometer codebase, so it should be safe to keep the public API in this case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right (this is the only place when it is called). I synchronized it with BiFunction because leaving them different might make things worse for the users when they pass them to the ctor of OkHttpContext.

OkHttpContext(
    Function<@Nullable Request, String> urlMapper,
    ...,
    Iterable<BiFunction<@Nullable Request, @Nullable Response, KeyValue>> contextSpecificTags,
    ...)

I found it confusing to leave Request non-null for the Function but nullable on BiFunction but not sure which one is better for the future (I want to properly fix this).

private final Iterable<Tag> extraTags;

private final Iterable<BiFunction<Request, Response, Tag>> contextSpecificTags;
private final Iterable<BiFunction<@Nullable Request, @Nullable Response, Tag>> contextSpecificTags;

@jckoenen jckoenen Apr 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through the codebase this morning with a fresh mind, so if I understand correctly the whole rabbit hole of nullability boils down to the fact that the public API OkHttpContext does not override the nullable SenderContext.getCarrier

I understand that changing the context is not really possible without widely affecting public API, but I believe this doesn't apply to OkHttpMetricsEventListener, which is completely isolated from that context (and pretty isolated in terms of public API, too). In this class, I think we can fully determine nullability and only need <_, @Nullable Response, _>:

  • The only entry-point is callStart. Here, Request is guaranteed not null, Response is guaranteed null
  • Other lifecycle callbacks do not add new entries to the shared callState map, and do not alter any entry's Request. So, its still guaranteed not null
  • responseHeadersEnd potentially updates the Response

Since CallState is private API, we know these are all relevant places, so we should be fine to drop the @Nullable annotation from CallState.Request

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that changing the context is not really possible without widely affecting public API,

I think I might be able to fix things by just breaking this:

    public void setState(OkHttpObservationInterceptor.CallState state) {
        this.state = state;
    }

    public OkHttpObservationInterceptor.@Nullable CallState getState() {
        return state;
    }

These are public methods but CallState is package-private so users are not able to use it anyways. (Though I don't like the parameters of the OkHttpContext either, they should be on the convention I think and that's really a breaking change.)

I believe this doesn't apply to OkHttpMetricsEventListener, which is completely isolated from that context (and pretty isolated in terms of public API, too). In this class, I think we can fully determine nullability and only need <_, @nullable Response, _>

I applied the same thought-process here as in #7375 (comment), to keep things consistent(ly wrong).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are public methods but CallState is package-private so users are not able to use it anyways.

I wonder if there is some static analysis or ErrorProne capability we could use to prevent adding such broken public API to begin with (public API that requires non-public types). I'm fine breaking that in 1.17.0-RC1 since external users can't use the API anyway, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can do context.setState(null) and/or Object obj = context.getState() but neither makes a lot of sense.
I haven't seen any errorprone check for this: https://www.jetbrains.com/help/inspectopedia/ClassEscapesDefinedScope.html#locating-this-inspection
Maybe we should open an issue and/or check if we can use spotbugs or pmd for this.

@jckoenen

jckoenen commented Apr 3, 2026

Copy link
Copy Markdown

Existing tests actually already cover the failing scenario, they are just missing the assertions which kotlin would insert. This will cause a failure:

diff --git a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
index 3e6809089..5f9268805 100644
--- a/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
+++ b/micrometer-core/src/test/java/io/micrometer/core/instrument/binder/okhttp3/OkHttpMetricsEventListenerTest.java
@@ -100,6 +100,11 @@ class OkHttpMetricsEventListenerTest {
 
         OkHttpClient client = new OkHttpClient.Builder().connectTimeout(1, TimeUnit.MILLISECONDS)
             .eventListener(OkHttpMetricsEventListener.builder(registry, "okhttp.requests")
+                .tag((req, res) -> {
+                    assertThat(req).isNotNull();
+                    assertThat(res).isNotNull();
+                    return Tag.of("key", "asdf");
+                })
                 .tags(Tags.of("foo", "bar"))
                 .uriMapper(URI_MAPPER)
                 .build())

I can also confirm that updating the annotations will really produce the expected behaviour when using kotlin (using the exact piece of code that triggered it before):

Bytecode with @Nullable Response
  private final static withMetrics$lambda$0$1(Lokhttp3/Request;Lokhttp3/Response;)Lio/micrometer/core/instrument/Tag;
   L0
    ALOAD 0
    LDC "request"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 50 L1
    ALOAD 0
    INVOKEVIRTUAL okhttp3/Request.url ()Lokhttp3/HttpUrl;
    
    ...
Original Bytecode
  private final static withMetrics$lambda$0$1(Lokhttp3/Request;Lokhttp3/Response;)Lio/micrometer/core/instrument/Tag;
   L0
    ALOAD 0
    LDC "request"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
    ALOAD 1
    LDC "<unused var>"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 50 L1
    ALOAD 0
    INVOKEVIRTUAL okhttp3/Request.url ()Lokhttp3/HttpUrl;
    
    ...

@jonatan-ivanov

Copy link
Copy Markdown
Member Author

@shakuzen

I don't think we can know the Request is not null unless we check because it is marked @nullable on the CallState, and the person who reported the issue apparently ran into it being null in some case.

Not in this PR, but I would get rid of the CallState entirely (there are multiple problems with it: being package-private but "leaked" on a public API is the most pressing one). In OkHttp's Kotlin API the request is not null-marked so it cannot be null which I think we can trust, it becomes nullable because of our own code. :(

@shakuzen shakuzen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good for 1.16.x to me. We can do more in main, I think.

@jonatan-ivanov
jonatan-ivanov merged commit 140b83b into micrometer-metrics:1.16.x Apr 7, 2026
11 checks passed
@jonatan-ivanov
jonatan-ivanov deleted the okhttp-vs-jspecify branch April 7, 2026 20:51
jonatan-ivanov added a commit that referenced this pull request Apr 7, 2026
When gh-7375 was merged in tests were ok locally and the PR build was
also fine. But after merge, it broke the build. The failure is valid,
the tests did not follow the implementation, this change fixes them.

See gh-7373
See gh-7375
@jonatan-ivanov

jonatan-ivanov commented Apr 7, 2026

Copy link
Copy Markdown
Member Author

Fyi, I needed to fix tests after merge: 55b8f06

Tests were ok locally for me so I did not even followed them through, NullAway should have screamed that my changes broke tests. The PR build was also fine. But after merge, this change broke the build. The failure is valid, the tests did not follow the implementation so I fixed them separately.

Right now I have no idea why this did not fail locally and (more importantly) on the PR build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong Nullability Information in OkHttpMetricsEventListener

3 participants