Skip to content

Wait on a future inside a generic wrapper whose transformer returns a future - #941

Merged
oryan-block merged 1 commit into
masterfrom
bugfix/938-future-returning-wrapper
Oct 30, 2026
Merged

oryan-block merged 1 commit into
masterfrom
bugfix/938-future-returning-wrapper

Conversation

@oryan-block

Copy link
Copy Markdown
Collaborator

Description

Fixes #938

A generic wrapper registered with a transformer that returns a future of its content, like Reactor's Mono with Mono::toFuture, lost a future when its type argument was itself a future. For Mono<CompletableFuture<Item>>, { item { name } } failed with

Expected source object to be an instance of 'Item' but instead got 'java.util.concurrent.CompletableFuture'

and a Mono<CompletionStage<String>> field returned "java.util.concurrent.CompletableFuture@4d3c6593[Completed normally]" with no error. Same inside lists and other wrappers.

The unwrapper hands a wrapper's transformer result straight to the declared future below it. That's right for a Try<CompletableFuture<Item>> whose transformer returns the declared future (graphql-java gets the resolver's own future), but Mono::toFuture returns a future of it. Both build the same steps, and only the completed value tells them apart. Checking that would add a thenCompose on every wrapper around a future and misread union members that implement Future, so the wrapper has to say it.

Now:

  • GenericWrapper.withCompletionStageTransformer(Mono::class, 0, Mono<*>::toFuture) (or (Mono.class, 0, Mono::toFuture) from Java) registers a wrapper whose transformer returns a CompletionStage of its content. The unwrapper waits on that future first, then on the declared one, at the top level and nested
  • wrappers registered with withTransformer behave as before, so a Mono registered that way still has the bug. Nothing is guessed at runtime
  • GenericWrapper itself is unchanged (constructor, copy, componentN). The factory marks the transformer instead, and copy keeps it since it keeps the transformer

I first had the flag as a new GenericWrapper property. Keeping the old JVM signatures then needs a hand-written 4-parameter constructor and copy that read as dead code, and a caller could set the flag on a transformer that returns the content. With the factory, the transformer's type has to be a CompletionStage.

No DataFetchingEnvironment or schemaWrapper overloads, since nobody asked for them. Easy to add later.

Release note: GenericWrapper.withCompletionStageTransformer registers a wrapper whose transformer returns a future of its content, such as Reactor's Mono::toFuture. A future declared inside it, like Mono<CompletableFuture<Item>>, is then waited on too. Before, graphql-java got the inner future as the value. Wrappers registered with withTransformer keep their behaviour.

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

mvn clean verify passes (440 tests). New tests in NestedGenericWrapperTest cover the future at the top level, a scalar, a list, and a union member that implements Future. The first three fail without the unwrapper change.

Also checked through the installed jar in a separate consumer project with reactor-core 3.7.19. With withCompletionStageTransformer, Mono<CompletableFuture<Item>>, List<Mono<CompletableFuture<Item>>>, Mono<CompletionStage<String>> and Try<Mono<CompletableFuture<Item>>> all resolve. With withTransformer they still fail like on master. A Java caller compiles with -Xlint:all -Werror (apart from the raw Mono.class), and javap against 14.5.0 shows no removed or changed GenericWrapper signature.

The bounded check of every return type up to 3 containers deep (about 172k executions), with its future-returning wrapper registered through the new factory, no longer finds the 192 failing return types it finds on master, and finds nothing new.

GenericWrapper.withCompletionStageTransformer registers a wrapper whose
transformer returns a future of its content, like Mono::toFuture. The
unwrapper then waits on that future before the declared one, so
Mono<CompletableFuture<Item>> resolves to the Item. Wrappers registered
with withTransformer keep handing their result to a declared future as is.
@sonarqubecloud

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit cd3233c into master Oct 30, 2026
5 checks passed
@oryan-block
oryan-block deleted the bugfix/938-future-returning-wrapper branch October 30, 2026 00:32
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.

A generic wrapper whose transformer returns a future loses a nested future

1 participant