Tomirio
Tomirio

Reputation: 149

Spring WebFlux Route always returns 404

I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin. I wrote test for my handler function (in which I mock the dependencies using Mockito).

However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).

I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.

The code is as follows (and can also be found on GitHub):

UserRouterTest

@ExtendWith(SpringExtension::class, MockitoExtension::class)
@Import(UserHandler::class)
@WebFluxTest
class UserRouterTest {

    @MockBean
    private lateinit var userService: UserService

    @Autowired
    private lateinit var userHandler: UserHandler

    @Test
    fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
        val expectedCustomer = User("test", "test")
        val id = expectedCustomer.userID

        `when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))

        val router = UserRouter().userRoutes(userHandler)
        val client = WebTestClient.bindToRouterFunction(router).build()

        client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)
    }
}

User

@Entity
class User(var username : String, var password: String) {

    @Id
    val userID = UUID.randomUUID()
}

UserRepository

@Repository
interface UserRepository : JpaRepository<User, UUID>{
}

UserService

@Service
class UserService(
        private val userRepository: UserRepository
) {
    fun getUserByID(id: UUID): Optional<User> {
        return Optional.of(
                try {
                    userRepository.getOne(id)
                } catch (e: EntityNotFoundException) {
                    User("test", "test")
                }
        )
    }

    fun addUser(user: User) {
        userRepository.save(user)
    }
}

UserHandler

@Component
class UserHandler(
        private val userService: UserService
) {
    fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
        val id = try {
            UUID.fromString(request.pathVariable("userID"))
        } catch (e: IllegalArgumentException) {
            return ServerResponse.badRequest().syncBody("Invalid user id")
        }
        val user = userService.getUserByID(id).get()
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(user))
    }
}

UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {
        contentType(MediaType.APPLICATION_JSON_UTF8).nest {
            GET("/users/{userID}", userHandler::getUserWithID)
            GET("") { ServerResponse.ok().build() }
        }
    }
}

EDIT

To route based on the presence of one or more query parameter (regardless of their values), we can do the following: UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {

        GET("/users/{userID}", userHandler::getUserWithID)
        (GET("/users/")
                and queryParam("username") { true }
                and queryParam("password") { true }
                )
                .invoke(userHandler::getUsers)
    }
}

Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.

Upvotes: 5

Views: 8566

Answers (3)

Kuppusamy
Kuppusamy

Reputation: 153

Spent sometime to find this 404 was due to the presence of both libraries. The project could work in SpringBoot 2.7.0 but not in 3.0.5.

Upvotes: 0

HereAndBeyond
HereAndBeyond

Reputation: 1454

To all those facing the same issue, but have noticed that your issue does not involve contentType mismatching, I recommend checking your dependencies (build.gradle, pom.xml, etc.).

In my case, I encountered the same problem when I had both of the following dependencies defined:

    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'

If you're using the functional style for declaring your endpoints (using RouterFunction), having both dependencies can cause issues.

To resolve this, you should remove one of the dependencies. If you are working with spring-webflux, make sure to keep only the following in your build configuration:

    implementation 'org.springframework.boot:spring-boot-starter-webflux'

Note: It's important to note that as of spring-boot 3.1.0, there are no warnings or errors when both of the dependencies are present. You just receive 404 Not Found response without any further indication of the problem.

You can find a working small application here in the spring.io guide

Upvotes: 7

Viktor Penelski
Viktor Penelski

Reputation: 151

The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it

client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .header("Content-Type", "application/json;charset=UTF-8")
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)

Upvotes: 4

Related Questions