Skip to content

ClientBuilder fluent setters are uncallable outside com.duosecurity.client (build() inaccessible) #113

Description

@AaronAtDuo

Summary

Every fluent builder method on Http.ClientBuilder<T> is uncallable in a chain from outside the com.duosecurity.client package. Any chain such as the documented new Admin.AdminBuilder(...).useTimeout(30).build() fails to compile with:

build() is defined in an inaccessible class or interface

(and similarly for any method invoked after a fluent setter).

Root cause

ClientBuilder is declared protected, but its fluent setters are typed to return the ClientBuilder<T> base type rather than a public self-type:

  • Http.java:424 — protected abstract static class ClientBuilder<T extends Http> {
  • Http.java:455 — public ClientBuilder<T> useTimeout(int timeout)
  • Http.java:478 — public ClientBuilder<T> useMaxBackoffMs(long maxBackoffMs)
  • Http.java:493 — public ClientBuilder<T> useCustomCertificates(String pemContent)
  • Http.java:506 — public ClientBuilder<T> disableCaPinning()
  • Http.java:519 — public ClientBuilder<T> addAdditionalDuoHeader(String name, String value)
  • Http.java:534 — public ClientBuilder<T> addHeader(String name, String value)
  • Http.java:544 — public T build()

The public subclass Admin.AdminBuilder overrides only createClient, not the setters, so the setters keep the ClientBuilder<T> return type.

Although useTimeout/build/etc. are public, in Java a member is only accessible when the static type of the receiver expression is also accessible. The constructor expression new Admin.AdminBuilder(...) has the public type AdminBuilder, so calling .build() directly on it compiles. But the moment any setter is called, the expression's static type becomes the protected ClientBuilder<Admin>, and every subsequent call — another setter or build() — references a member declared in an inaccessible class, hence the error.

The library's own tests don't hit this because they live in com.duosecurity.client, where protected members are accessible. Any consumer in a different package hits it on the first chained setter.

Reproduction

From a package other than com.duosecurity.client:

Admin admin = new Admin.AdminBuilder("GET", host, uri)
        .useTimeout(30)
        .build();   // does not compile: build() is defined in an inaccessible class or interface

Workaround

Ignore the setters' return values and keep operating on the AdminBuilder-typed reference:

Admin.AdminBuilder builder = new Admin.AdminBuilder("GET", host, uri);
builder.useTimeout(30);
builder.disableCaPinning();
Admin admin = builder.build();

Suggested fix

Either make ClientBuilder public, or (preferably) give the fluent setters a proper self-type so chaining returns the concrete public builder — e.g. a recursive generic bound (ClientBuilder<T extends Http, B extends ClientBuilder<T, B>> returning B), or override the setters in each concrete builder (AdminBuilder, AuthBuilder, AccountsBuilder) with covariant return types.


This issue was generated with AI assistance (Claude).

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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions