Currently, Spring 7.0.0-M7/Spring Boot 4.0.0-M1, the server config is a little confused, we can set header, URI path index, query parameter, Media type, one by one, but can not configure them in the same backend API application.

  1. On the server side, I think it could support one or more strategies in one application. eg. support X-API-Version:1.0 and /api/someAPI?v=1.0 at the same time. The client application can select anyone to interact with it.

  2. On the client, we can configure a default strategy when building the HTTP client instance, and allow the developer to mutate it before sending the request in tests, like we handled the security credentials.

Comment From: spencergibb

You can add as many as you want currently with the use methods. The configurer adds them to a list. See https://github.com/spring-projects/spring-framework/blob/09917fad7bca9b3997522f0a75d6319203f2127f/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java#L67-L110

Comment From: hantsy

@spencergibb A simple producer example to describe the problem I encountered,

The web configuration like this.

public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer
                .usePathSegment(0)
                .useRequestHeader("X-API-Version")
                //.useRequestParam("version")

                .setDefaultVersion("1.0")
                // When a defaultVersion is also set, this is automatically set to false.
                // .setVersionRequired(true)

                // default is SemanticApiVersionParser
                //.setVersionParser(new SemanticApiVersionParser())

                // set detectSupportedVersions(false) when adding supported versions
                .detectSupportedVersions(false)
                .addSupportedVersions("1.0", "1.1", "2.0");

And the rest controller for demonstration.

@RestController
@RequestMapping("/hello")
public class GreetingController {

    @GetMapping()
    public String helloDefault() {
        return "Hello v1.0(Default)";
    }

    @GetMapping(version = "1.1")
    public String helloV1_1() {
        return "Hello v1.1";
    }

    @GetMapping(version = "2.0")
    public String helloV2_0() {
        return "Hello v2.0";
    }
}

And then configure a WebClient to access the API.

@BeforeEach
    public void setup() {
        var reactorHttpClient = HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30_000);
        var clientConnector = new ReactorClientHttpConnector(reactorHttpClient);

        this.disposableServer = this.httpServer.bindNow(Duration.ofMillis(5_000));
        this.client = WebClient.builder()
                .baseUrl("http://localhost:" + this.port)
                .codecs(c -> c.defaultCodecs().enableLoggingRequestDetails(true))
                //.defaultHeaders(headers -> headers.set("X-API-Version", "1.0"))
                .apiVersionInserter(ApiVersionInserter.builder()
                                .useHeader("X-API-Version")
//                        .usePathSegment(0)
//                        .useQueryParam("version")
                                // .withVersionFormatter(ApiVersionFormatter)
                                .build()
                )
                .clientConnector(clientConnector)
                .build();
    }

    @AfterEach
    public void teardown() {
        this.disposableServer.disposeNow();
    }

    @Test
    public void testHello() {
        this.client.get().uri("/hello")
                .apiVersion("1.0")
                .retrieve()
                .bodyToMono(String.class)
                .as(StepVerifier::create)
                .expectNext("Hello v1.0(Default)")
                .verifyComplete();
    }

It is failed due to exception like this.

2025-07-31 09:51:33,497 DEBUG [reactor-http-nio-4] o.s.w.s.h.ResponseStatusExceptionHandler: 
[4fdc77a2-1] Resolved [InvalidApiVersionException: 
"400 BAD_REQUEST "Invalid API version: 'hello'.""] for HTTP GET /hello
2025-07-31 09:51:33,501 TRACE [reactor-http-nio-4] o.s.c.l.LogFormatUtils: 
[4fdc77a2-1] Completed 400 BAD_REQUEST, headers={masked}
2025-07-31 09:51:33,503 TRACE [reactor-http-nio-4] o.s.c.l.CompositeLog: 
[4fdc77a2-1, L:/127.0.0.1:8888 - R:/127.0.0.1:61834] Handling completed
2025-07-31 09:51:33,530 TRACE [reactor-http-nio-3] o.s.c.l.LogFormatUtils: 
[935493d] [b98aa0fc-1] Response 400 BAD_REQUEST, headers=[content-length:"0"]

...

java.lang.AssertionError: expectation "expectNext(Hello v1.0(Default))" failed (expected: onNext(Hello v1.0(Default)); actual: onError(org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8888/hello))

    at reactor.test.MessageFormatter.assertionError(MessageFormatter.java:115)
    at reactor.test.MessageFormatter.failPrefix(MessageFormatter.java:104)
    at reactor.test.MessageFormatter.fail(MessageFormatter.java:73)
    at reactor.test.MessageFormatter.failOptional(MessageFormatter.java:88)
    at reactor.test.DefaultStepVerifierBuilder.lambda$addExpectedValue$10(DefaultStepVerifierBuilder.java:509)
    at reactor.test.DefaultStepVerifierBuilder$SignalEvent.test(DefaultStepVerifierBuilder.java:2289)
    at reactor.test.DefaultStepVerifierBuilder$DefaultVerifySubscriber.onSignal(DefaultStepVerifierBuilder.java:1529)
    at reactor.test.DefaultStepVerifierBuilder$DefaultVerifySubscriber.onExpectation(DefaultStepVerifierBuilder.java:1477)
    at reactor.test.DefaultStepVerifierBuilder$DefaultVerifySubscriber.onError(DefaultStepVerifierBuilder.java:1129)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:241)
    at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:315)
    at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onError(Operators.java:2235)
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onError(FluxOnAssembly.java:544)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onError(MonoFlatMap.java:180)
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:106)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onError(MonoIgnoreThen.java:280)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:232)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:204)
    at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onComplete(FluxOnErrorReturn.java:169)
    at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:89)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152)
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:549)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.core.publisher.Operators.complete(Operators.java:137)
    at reactor.netty.channel.FluxReceive.startReceiver(FluxReceive.java:182)
    at reactor.netty.channel.FluxReceive.subscribe(FluxReceive.java:148)
    at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68)
    at reactor.netty.ByteBufFlux.subscribe(ByteBufFlux.java:340)
    at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
    at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
    at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
    at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:103)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.secondError(MonoFlatMap.java:241)
    at reactor.core.publisher.MonoFlatMap$FlatMapInner.onError(MonoFlatMap.java:315)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onError(MonoIgnoreThen.java:280)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:232)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:204)
    at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onComplete(FluxOnErrorReturn.java:169)
    at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:89)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152)
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:549)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.core.publisher.Operators.complete(Operators.java:137)
    at reactor.netty.channel.FluxReceive.startReceiver(FluxReceive.java:182)
    at reactor.netty.channel.FluxReceive.subscribe(FluxReceive.java:148)
    at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68)
    at reactor.netty.ByteBufFlux.subscribe(ByteBufFlux.java:340)
    at reactor.core.publisher.Mono.subscribe(Mono.java:4571)
    at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
    at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:165)
    at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
    at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:162)
    at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2096)
    at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:134)
    at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:549)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152)
    at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onComplete(FluxContextWrite.java:126)
    at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onComplete(FluxMapFuseable.java:350)
    at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onComplete(FluxFilterFuseable.java:391)
    at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097)
    at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)
    at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
    at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:419)
    at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:456)
    at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:510)
    at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:820)
    at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434)
    at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:167)
    at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
    at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:381)
    at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:575)
    at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:550)
    at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:491)
    at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:468)
    at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:207)
    at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:178)
    at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1073)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:1583)
    Suppressed: org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest: 400 Bad Request from GET http://localhost:8888/hello
        at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:306)
        Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ 400 BAD_REQUEST from GET http://localhost:8888/hello [DefaultWebClient]
Original Stack Trace:
            at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:306)
            at org.springframework.web.reactive.function.client.DefaultClientResponse.lambda$createException$1(DefaultClientResponse.java:214)
            at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:106)
            at reactor.core.publisher.FluxOnErrorReturn$ReturnSubscriber.onNext(FluxOnErrorReturn.java:162)
            at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2096)
            at reactor.core.publisher.FluxDefaultIfEmpty$DefaultIfEmptySubscriber.onComplete(FluxDefaultIfEmpty.java:134)
            at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:152)
            at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onComplete(FluxContextWrite.java:126)
            at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onComplete(FluxMapFuseable.java:350)
            at reactor.core.publisher.FluxFilterFuseable$FilterFuseableConditionalSubscriber.onComplete(FluxFilterFuseable.java:391)
            at reactor.core.publisher.Operators$BaseFluxToMonoOperator.completePossiblyEmpty(Operators.java:2097)
            at reactor.core.publisher.MonoCollect$CollectSubscriber.onComplete(MonoCollect.java:145)
            at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
            at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260)
            at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144)
            at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:419)
            at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:456)
            at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:510)
            at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:820)
            at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:115)
            at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:356)
            at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:434)
            at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
            at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
            at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:249)
            at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:354)
            at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1429)
            at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918)
            at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:167)
            at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.handle(AbstractNioChannel.java:445)
            at io.netty.channel.nio.NioIoHandler$DefaultNioRegistration.handle(NioIoHandler.java:381)
            at io.netty.channel.nio.NioIoHandler.processSelectedKey(NioIoHandler.java:575)
            at io.netty.channel.nio.NioIoHandler.processSelectedKeysOptimized(NioIoHandler.java:550)
            at io.netty.channel.nio.NioIoHandler.processSelectedKeys(NioIoHandler.java:491)
            at io.netty.channel.nio.NioIoHandler.run(NioIoHandler.java:468)
            at io.netty.channel.SingleThreadIoEventLoop.runIo(SingleThreadIoEventLoop.java:207)
            at io.netty.channel.SingleThreadIoEventLoop.run(SingleThreadIoEventLoop.java:178)
            at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:1073)
            at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
            at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
            at java.base/java.lang.Thread.run(Thread.java:1583)



The problem is I used header to set the version, but server side use path segment to process version.

You can run the example project on your local machine, check out https://github.com/hantsy/spring7-sandbox/tree/master/api-versioning, change to the WebConfig to set multiple version stetragy, run the tests.

Comment From: spencergibb

Yeah, I just ran into something similar, the path one will interfere. You can try playing with the ordering of how you add them

Comment From: hantsy

@spencergibb But in the real world, the RestClient could not know how server side handle the version in which order.

Comment From: hantsy

I think when analyze the version of incoming requests, the characteristics (eg, http header, request parameter, media type, etc.) existence and matching result for the version should be considered in a high order than the nature order of resolver registration.