Command handler isn't being found by AxonServer, even though Framework says it's registered

Hey there! I’m running into a strange issue with Axon Framework in a small proof-of-concept application I’m working on to try out various features. Here’s the components I’m using:

  • Axon Server 4.5.5
  • Axon Framework 4.5
  • Axon Reactor Plugin 4.5
  • Spring Boot 2.5.2
  • Kotlin 1.4.32

I have a WebFlux controller set up that sends a command on the command gateway to create new instances of the aggregate, like so:

    @PostMapping
    fun createNewUser(@Valid @RequestBody user: NewUserRequest): Mono<UUID> {
        require(!user.username.isNullOrBlank() || !user.emailAddress.isNullOrBlank()) { "A user must provide a username or email address." }
        require(user.firstName.isNotBlank() && user.lastName.isNotBlank()) { "First name and last name cannot be blank." }

        val newUserCommand = CreateUserCommand(
            user.username ?: user.emailAddress ?: throw NullPointerException("Expect username or password"),
            user.firstName,
            user.lastName,
            user.emailAddress
        )

        return commandGateway.send<User>(newUserCommand).map { it.id }.toMono()
    }

On the aggregate side, I have a command handler set up on the constructor to handle the creation of the aggregate:

@Aggregate
class User() {
    @AggregateIdentifier
    lateinit var id: UUID

    var state = UserState.Closed

    @AggregateMember(eventForwardingMode = ForwardMatchingInstances::class)
    val productSubscriptions = mutableMapOf<Product, Subscription>()

    @CommandHandler(payloadType = CreateUserCommand::class)
    constructor(command: CreateUserCommand, validationErrors: Errors): this() {
        require(command.username.isNotBlank() && command.username.length > 6) { "Username must be greater than six characters in length." }
        require(command.firstName.isNotBlank() && command.lastName.isNotBlank()) { "First name and last name are required." }
        handleValidationErrors(validationErrors)

        applyEvent(NewUserEvent(UUID.randomUUID(), command.username, command.firstName, command.lastName, command.emailAddress))
    }

    ...
}

When I start the application, I can see from the logs that the command handler is found and registered:

2021-07-21 13:09:52.130 DEBUG 9118 --- [  restartedMain] o.a.a.c.command.AxonServerCommandBus     : Subscribing command with name [com.poc.model.commands.user.CreateUserCommand] to this distributed CommandBus. Expect similar logging on the local segment.
2021-07-21 13:09:52.147  INFO 9118 --- [  restartedMain] i.a.a.c.command.impl.CommandChannelImpl  : CommandChannel for context 'default' connected, 0 command handlers registered
2021-07-21 13:09:52.149  INFO 9118 --- [  restartedMain] i.a.a.c.command.impl.CommandChannelImpl  : Registered handler for command 'com.poc.model.commands.user.CreateUserCommand' in context 'default'

But when I invoke the endpoint and the command gets sent, Axon Server replies that it couldn’t find a command handler:

org.axonframework.commandhandling.CommandExecutionException: No handler available to handle command [com.poc.model.commands.user.CreateUserCommand]

Any recommendations on how I can further troubleshoot this? Is there something I’m missing in terms of how this is all tied together and set up that’s causing this issue? Thanks!

Hi @jesse.docken! Could you share what your command looks like?

You know, I was going to do that and completely forgot. Here it is:

data class CreateUserCommand(
    @Unique(byAggregate = User::class, mode = CrudMode.Create)
    val username: String,
    val firstName: String,
    val lastName: String,
    @Email
    @Unique(byAggregate = User::class, mode = CrudMode.Create)
    val emailAddress: String?
): Command {
    val id: UUID
        @RoutingKey
        get() = UUID.randomUUID()
}

Hi @jesse.docken,

I think it’s not found because of the second parameter validationErrors: Errors.
Which parameter is this one? Is it supposed to be resolved by Axon/Spring?

I myself faced that in the past and that was the problem!

Hope it helps.

KR,

I have a parameter resolver that injects an Errors object in to perform validation on any command on the executable. It also technically handles uniqueness verification (ala the uniqueness set validation that Yvonne wrote).

Hi @jesse.docken, got it.
I believe the error is somewhere there… can you remove it for a quick test? Just to make sure it is or isn’t there.

Also, sharing the parameter resolver and how you are registering it might be useful!

KR,

Huh! Removing it causes it to start working. Alright, that narrows down the cause!

So here’s the abridged form of my parameter resolver, based on what documentation I’ve been able to find:

@Component
class ErrorsParameterResolverFactory(
    private val adapter: SpringValidatorAdapter,
    val uniqueOps: ReactiveRedisOperations<String, String>
): ParameterResolver<Errors>, ParameterResolverFactory {
    ...
    override fun resolveParameterValue(message: Message<*>): Errors {
        val payload = message.payload
        val identifier = message.payloadType.simpleName

        val errorsContainer = BeanPropertyBindingResult(payload, identifier)
        adapter.validate(message, errorsContainer)
        ...
        return errorsContainer
    }

    override fun matches(message: Message<*>): Boolean {
        val payload = message.payload ?: return false

        if (!Command::class.isInstance(payload)) {
            return false
        }

        if (getConstraints(payload).isEmpty() && getUniqueValueMap(payload).isEmpty()) {
            return false
        }

        return true
    }

    override fun createInstance(executable: Executable, parameters: Array<out Parameter>, index: Int): ParameterResolver<*>? {
        return if (Errors::class.java.isAssignableFrom(parameters[index].type)) {
            this
        } else null
    }
}

I just have the class registered as a Spring bean. There isn’t really any documentation that I could find that mentioned how to register a parameter resolver in Axon Spring Boot, but I did see createInstance() get called properly. I think the bug might be in the matches() logic, actually… going down the debugging rabbit hole, I’ve discovered that the methods that are supposed to look for the annotations don’t seem to be working properly. :confused:

Alright, turns out the logic for matches() was wrong. Fixing that made everything work again. Sorry for the false alarm!

1 Like

Hi @jesse.docken, thanks for letting us know it was resolved.

As a follow up, I would like to share 2 issues we have.

First one is on AF itself, where we could have a nicer exception/log message about having a command handler but without the complete list of parameters. You can find it here: Fine tune "No Handler For..." Exception · Issue #1646 · AxonFramework/AxonFramework · GitHub

Second one is a documentation issue where we can expand our ref-guide to add a ParameterResolver example. You can find it here: Provide a `ParameterResolver` and `ParameterResolverFactory` example · Issue #215 · AxonIQ/reference-guide · GitHub

KR,