Skip to content

[BUG][JAVA][vertx] useVertx5: first API call throws NPE — buildWebClient constructs PoolOptions from empty JSON, leaving all pool defaults uninitialized #24015

Description

@IvanVas

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)

Description

With library=vertx and useVertx5=true, the generated client throws a NullPointerException on the first API call when the ApiClient is built with the two-argument constructor — the documented default:

Vertx vertx = Vertx.vertx();
ApiClient client = new ApiClient(vertx, new JsonObject());   // 2-arg ctor
new DefaultApiImpl(client).someOperation(request)            // → NPE
java.lang.NullPointerException: Cannot invoke "java.util.concurrent.TimeUnit.toMillis(long)" because "sourceUnit" is null
	at java.base/java.util.concurrent.TimeUnit.convert(TimeUnit.java:189)
	at io.vertx.core.http.impl.HttpClientImpl.<init>(HttpClientImpl.java:72)
	at io.vertx.core.http.impl.HttpClientBuilderInternal.build(HttpClientBuilderInternal.java:111)
	at io.vertx.core.Vertx.createHttpClient(Vertx.java:274)
	at io.vertx.ext.web.client.WebClient.create(WebClient.java:86)
	at com.zyte.api.client.ApiClient.buildWebClient(ApiClient.java:644)
	at com.zyte.api.client.ApiClient.getWebClient(ApiClient.java:134)
	at com.zyte.api.client.ApiClient.invokeAPI(ApiClient.java:472)
	at com.zyte.api.client.api.DefaultApiImpl.extract(DefaultApiImpl.java:94)
	at com.zyte.api.client.api.DefaultApi.extract(DefaultApi.java:32)

(com.zyte.api.client is a custom invokerPackage; the default org.openapitools.client produces the same trace. The NPE surfaces on the first call rather than at construction because buildWebClient is invoked lazily from getWebClient().)

Root cause — two pieces interact:

  1. The template's 2-arg constructor delegates with an empty pool config, and buildWebClient unconditionally turns it into a PoolOptions
    (ApiClient.mustache lines 76–78 and 728–735 on current master):

    public ApiClient(Vertx vertx, JsonObject config) {
        this(vertx, config, new JsonObject());
    }
    ...
    protected WebClient buildWebClient(Vertx vertx, JsonObject config, JsonObject poolConfig) {
        ...
        return WebClient.create(vertx, new WebClientOptions(config), new PoolOptions(poolConfig));
    }
  2. Vert.x 5's PoolOptions(JsonObject) constructor does not initialize defaults, unlike its no-arg constructor, and unlike sibling options classes such as HttpClientOptions(JsonObject), which calls init() before applying the JSON.

    public PoolOptions() {
        http1MaxSize = DEFAULT_MAX_POOL_SIZE;          // 5
        http2MaxSize = DEFAULT_HTTP2_MAX_POOL_SIZE;    // 1
        maxLifetime = DEFAULT_MAXIMUM_LIFETIME;        // 0
        maxLifetimeUnit = DEFAULT_MAXIMUM_LIFETIME_TIME_UNIT;  // SECONDS
        cleanerPeriod = DEFAULT_POOL_CLEANER_PERIOD;   // 1000
        eventLoopSize = DEFAULT_POOL_EVENT_LOOP_SIZE;  // 0
        maxWaitQueueSize = DEFAULT_MAX_WAIT_QUEUE_SIZE; // -1
    }
    
    public PoolOptions(JsonObject json) {
        PoolOptionsConverter.fromJson(json, this);     // no defaults applied first
    }

    So new PoolOptions(new JsonObject()) yields maxLifetimeUnit = null (plus http1MaxSize = 0, http2MaxSize = 0, cleanerPeriod = 0, …), and HttpClientImpl's constructor NPEs on
    (HttpClientImpl.java line 72):

    this.maxLifetime = MILLISECONDS.convert(poolOptions.getMaxLifetime(), poolOptions.getMaxLifetimeUnit());

Note the NPE is only the loudest symptom. A partially populated poolConfig (e.g. just {"maxLifetimeUnit": "SECONDS"}) avoids the NPE but silently produces http1MaxSize = 0 / http2MaxSize = 0 instead of the documented defaults (5 / 1), because every field not present in the JSON keeps its uninitialized value. Any fix should restore Vert.x defaults for all absent keys, not just dodge the NPE.

The minimal demonstration needs no generated code at all:

// NPEs immediately on any Vert.x 5.0.x:
WebClient.create(Vertx.vertx(), new WebClientOptions(), new PoolOptions(new JsonObject()));

openapi-generator version

7.23.0 (first affected — the poolConfig parameter was introduced by #23829, milestone 7.23.0). Still present on current master. Not a regression from 7.22.0 in the sense that 7.22.0's useVertx5 did not pass PoolOptions at all.

Vert.x: reproduced with 5.0.11

OpenAPI declaration file content or url

Any spec reproduces it — the bug is in the supporting ApiClient, not in path/model generation. Minimal example:

openapi: 3.0.3
info:
  title: repro
  version: 1.0.0
paths:
  /ping:
    get:
      operationId: ping
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object

Generation Details

openapi-generator-cli generate \
  -g java \
  --library vertx \
  --additional-properties useVertx5=true \
  -i repro.yaml \
  -o out

(supportVertxFuture is not required to reproduce.)

Steps to reproduce

  1. Generate a client with the command above.

  2. Run:

    Vertx vertx = Vertx.vertx();
    ApiClient client = new ApiClient(vertx, new JsonObject());
    new DefaultApiImpl(client).ping().onComplete(System.out::println);
  3. First call fails with the NullPointerException above (thrown from buildWebClient → WebClient.create).

Related issues/PRs

Suggest a fix

In Java/libraries/vertx/ApiClient.mustache, build the PoolOptions on top of Vert.x defaults so absent keys keep their documented values (PoolOptionsConverter's fromJson/toJson methods are package-private — @JsonGen(publicConverter = false) — so the overlay has to go through PoolOptions.toJson()):

protected WebClient buildWebClient(Vertx vertx, JsonObject config, JsonObject poolConfig) {

    if (!config.containsKey("userAgent")) {
        config.put("userAgent", "...");
    }

    // PoolOptions(JsonObject) does not initialize defaults (unlike its no-arg
    // constructor); overlay the user config on serialized defaults so absent
    // keys keep their documented values instead of 0/null.
    PoolOptions poolOptions = new PoolOptions(new PoolOptions().toJson().mergeIn(poolConfig));
    return WebClient.create(vertx, new WebClientOptions(config), poolOptions);
}

A minimal alternative — poolConfig.isEmpty() ? WebClient.create(vertx, options) : ... — fixes the default-constructor NPE but still mis-handles partially populated pool configs, so the overlay variant is preferable.

Workaround for affected users (no fork needed, buildWebClient is protected):

ApiClient client = new ApiClient(vertx, config) {
    @Override
    protected WebClient buildWebClient(Vertx vertx, JsonObject config, JsonObject poolConfig) {
        return WebClient.create(vertx, new WebClientOptions(config)); // let WebClient apply pool defaults
    }
};

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions