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

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