Repository navigation
Reject futures that graphql-java can't wait on - #839
Merged
Merged
Conversation
The default Future wrapper only unwraps the type at scan time. At runtime graphql-java only waits on a CompletionStage, so any other Future (a FutureTask, or the observer returned by RxJava's toFuture()) reached graphql-java as the field value. Object fields then failed on their children with a source type mismatch, list fields failed with a type mismatch, and scalar fields silently serialized the future's toString. A suspend function that returned any future, CompletableFuture included, hit the same problem, because graphql-java doesn't flatten a future that completes with another future. When a resolver resolves to such a future, after its generic wrapper transformer ran, the field now fails with an error that says what to return instead. The future is never waited on, because blocking the fetching thread delays sibling fields and hangs on a future that only completes after graphql-java dispatches DataLoaders. The value is only rejected if it isn't an instance of the class the schema type was matched to at scan time. A value that implements Future but is the schema type itself, or a Future that isn't unwrapped because the default wrappers are disabled, is left alone. Because it compares against the matched class rather than the declared return type, the check also covers a return type that is a type variable and a custom transformer that returns a plain future. Fixes #203 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A method declared to return Future<Object> unwraps to Object, which every value is an instance of, so a plain future returned for a union field slipped through and graphql-java threw a TypeResolverError out of execute. When the return type unwraps to Object, every future returned by a method declared to return Future is now checked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #203
Checklist
Description
Stacked on #836, merge that first.
In #203 a resolver declared as
Future<Organization>returns RxJava'sSingle#toFuture(), and its child fields fail withExpected source object to be an instance of '...Organization' but instead got 'io.reactivex.internal.observers.FutureSingleObserver'. The defaultGenericWrapper(Future::class, 0)is an identity transformer, so it only unwraps the type at scan time. At runtime graphql-java only waits on aCompletionStage, so any otherFuturebecame the field value:type mismatch error, expected type LIST;toString, e.g.java.util.concurrent.FutureTask@1b2c3d[Completed normally].A suspend function that returns any future,
CompletableFutureincluded, has the same problem, because graphql-java doesn't flatten a future that completes with another future.Now the field fails with an error that says what to return instead:
Resolver method 'Query.organization' resolved to a io.reactivex.internal.observers.FutureSingleObserver, which graphql-java can't wait on. Return a CompletionStage, such as a CompletableFuture, from the method or its generic wrapper transformer instead.Suspend function 'Query.organization' resolved to a java.util.concurrent.CompletableFuture, which graphql-java can't wait on. Return its value instead.The future is never waited on, even if it's already done. Blocking the fetching thread would delay sibling fields, since graphql-java calls fetchers one at a time on that thread, and it would hang on a plain future that only completes after graphql-java dispatches DataLoaders. Reading only futures that are already done would make the result depend on timing: it would work in tests and fail under load.
The check runs after the generic wrapper transformer, so a transformer can still unwrap a plain future (
GenericWrapper.withTransformer(FutureTask::class, 0, { it.get() })). A value is only rejected if it isn't an instance of the class the schema type was matched to at scan time. Because of that:Futurebut is the schema type itself (fun job(): Jobwithclass Job : Future<String>) still resolves;Futurethat isn't unwrapped becauseuseDefaultGenericWrappers(false)is set still resolves;Futureis checked;Flowable.toList().toFuture()in a transformer.Future<Object>(orFuture<Any>) is checked. Every value is anObject, so when the type unwraps toObjectany future from a method declared to returnFutureis checked. On master a plain future there made graphql-java throw aTypeResolverErrorout ofexecute.Not covered, same behaviour as master:
@JvmFieldtypedFuture<T>. Those go throughPropertyFieldResolver, which doesn't apply generic wrapper transformers at all;Optional<Future<T>>andDataFetcherResult<Future<T>>, where the future is nested inside another container;Future<out R>, a wildcard over a class type variable;FutureTask<String>or rawFutureon a scalar field, which no wrapper unwraps.Behaviour change, so this should go out in a minor release:
ValueUnboxer, anInstrumentationthat wraps data fetchers, aSchemaDirectiveWiring, or a custom scalarCoercingthat unwraps the future, now gets the error, because the data fetcher throws before their code sees the value. The fix is to move the unwrapping into a generic wrapper transformer, or return aCompletionStage.Job. For suspend functions, the error is thrown inside the coroutine like any other resolver exception. If the request's context is aCoroutineScopewith a regularJob, sibling suspend fields are cancelled.Futureas a well-known wrapper without saying the runtime value has to be aCompletionStage. That needs a follow-up there.🤖 Generated with Claude Code