Skip to content

Fix subscription resolver return type validation - #826

Merged
oryan-block merged 1 commit into
masterfrom
bugfix/766
Oct 3, 2026
Merged

oryan-block merged 1 commit into
masterfrom
bugfix/766

Conversation

@oryan-block

Copy link
Copy Markdown
Collaborator

Fixes #766

Checklist

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

Description

Since 13.0.4, a class that implements GraphQLMutationResolver (or GraphQLQueryResolver) together with GraphQLSubscriptionResolver fails to build with FieldResolverError: No method found as defined in schema ... addItem(~name) ... Note that a Subscription data fetcher must return a Publisher of events, even though the method is right there. The Publisher filter added in #756 checked search.source is GraphQLSubscriptionResolver. That's the resolver instance, not the root type being scanned, so every non-Publisher method on such a class was dropped when scanning Mutation or Query as well. getMissingFieldMessage used the same check, which is where the misleading note came from. RootResolverInfo now records whether it's the subscription root, and both places read that through Search.isSubscription.

The return type checks were also inverted. method.returnType.isAssignableFrom(Publisher::class.java) accepts supertypes of Publisher instead of subtypes, so a subscription returning a Flux, a Flowable, a custom Publisher subinterface or a concrete Publisher class got the same error. resolverMethodReturnsPublisher now accepts Publisher subtypes, futures of them (CompletionStage or Future, with a class, parameterized or ? extends type argument), and ReceiveChannel subtypes that have a wrapper registered for that exact type. Generic wrappers only unwrap their exact type, so MethodFieldResolver#scanForMatches now matches a subscription's return type against the schema as a Publisher of its events. Without that, a Flux<Item> return, or a covariant override of a Publisher<Item> method, mapped the Item type to the Flux class and failed with "Two different classes used for type Item".

Methods whose return type is erased to Object, like suspend functions, are still accepted without a check, same as before, because the real return type isn't known at that point. I left the transformer check in receiveChannelToPublisherWrapper alone. Kotlin lambdas are erased to return Object, so flipping it would reject every wrapper. DataFetcherResult<Publisher<T>>, Channel<T> with only the default ReceiveChannel wrapper, java.util.concurrent.Flow.Publisher, and types that only become a Publisher through a transforming wrapper (e.g. a Kotlin Flow) are still rejected, as they are on master. Each of those needs more than this fix.

Behaviour change: a class that is both a query/mutation resolver and a subscription resolver exposes all its methods to the Query/Mutation type again, as it did before 13.0.4. So if a non-Publisher method on it has the same name as a field another resolver already implements, the build now fails with "Found more than one matching resolver" instead of silently ignoring the method. Subscription methods returning Publisher subtypes, or futures of them, are now accepted. If a class has both a Publisher subtype method and a plain Publisher one for the same field, e.g. onItem(): ItemPublisher and getOnItem(): Publisher<Item>, onItem now wins.

🤖 Generated with Claude Code

The Publisher return type check for subscriptions was applied whenever
the resolver instance implemented GraphQLSubscriptionResolver, so a
class that is also a query or mutation resolver lost all of its
non-Publisher methods and failed with "No method found". The check and
the missing method note now depend on the root type being scanned,
which RootResolverInfo records.

The checks were also inverted: they accepted supertypes of Publisher
and CompletableFuture rather than subtypes, so Publisher
implementations such as Flux were rejected. Accept Publisher subtypes,
futures of them (Future or CompletionStage, with class, parameterized
or wildcard type arguments) and ReceiveChannel subtypes that have a
wrapper registered for that exact type. Methods erased to Object, such
as suspend functions, are still accepted since their return type can't
be checked here.

Generic wrappers only unwrap their exact type, so subscription return
types are now matched against the schema as a Publisher of their
events. Otherwise a Flux<Item> return, or a covariant override of a
Publisher<Item> method, would map the Item type to the Flux class.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit fc4376f into master Oct 3, 2026
6 checks passed
@oryan-block
oryan-block deleted the bugfix/766 branch October 3, 2026 19:26
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.

Unable to union MutationResolver and SubscriptionResolver in one class

1 participant