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.

Comment From: rstoyanchev

We support combining multiple strategies, but those have to be able to differentiate the absence of a value in order to return null and yield to the next resolver. So you can combine header, request param, and media type param resolvers. The path resolver is different in that regard, and I don't think it's valid to combine with anything else.

Path based versioning changes the structure of resource URLs with each version, and it can't be optional to have the version in the URL or not. The above example does not have a {version} in the segment, and if it did it would never map requests without the version in the path.

Generally, request headers / parameters are quite opposite as a strategy to the path. The former allows selective versioning of some endpoints from version to version, and that's a good fit for lightweight change. Path versioning implies deeper structural change when it is necessary to have all new URLs one way another (e.g. different host or different path) with no interlinking across versions.

Comment From: rstoyanchev

This has actually made me realize the path resolver could be improved, so I've created #35265.

Comment From: hantsy

How about the client support, most time, as a developer, we just need to set up one strategy to consume APIs.

And how to change the default settings in tests?

Comment From: rstoyanchev

Yes, client side is different and it is one inserter, but this is now off-topic