Close

Spring MVC - Path Extension Content Negotiation Strategy (legacy)

[Last Updated: Sep 29, 2026]

In the previous tutorial, we saw how content negotiation works in Spring MVC. In this example, we will use ServletPathExtensionContentNegotiationStrategy by adding a path extension to the request URI. We will request the XML media type this way, without sending an 'Accept' header.

Deprecation/removal path extension content negotiation is a legacy feature. It was deprecated in Spring Framework 5.2.4, it was disabled by default starting with Spring Framework 5.3, and it was removed completely in Spring Framework 7.0 (favorPathExtension, ignoreUnknownPathExtensions, PathExtensionContentNegotiationStrategy and ServletPathExtensionContentNegotiationStrategy). Suffix pattern matching (e.g. /user.xml matching a handler mapped to /user) was removed in the same release. The examples below therefore only run on Spring 5.x (the strategy is enabled by default up to 5.2.x; in later 5.x versions it must be enabled explicitly). Do not use this approach in new code. See the last section for the modern alternatives.

Example

Writing Controller

package com.logicbig.example;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("user")
public class UserController {

  @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
  @ResponseBody
  public User getUserById(@RequestParam("id") long userId) {
    // creating dummy user
    User user = new User();
    user.setId(userId);
    user.setName("joe");
    user.setEmailAddress("joe@example.com");
    return user;
  }

  @RequestMapping
  @ResponseBody
  public String getUserStringById(@RequestParam("id") long userId) {
    return "User: joe, id: " + userId;
  }
}
package com.logicbig.example;

import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;

@XmlRootElement
public class User implements Serializable {
  private Long id;
  private String name;
  private String password;
  private String emailAddress;
    .............
}

Writing JUnit tests

In this test, we are not using the xml extension yet.

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testUserRequest() throws Exception {
    this.mockMvc
        .perform(get("/user").param("id", "100"))
        .andExpect(status().isOk())
        .andExpect(content().string("User: joe, id: 100"));
  }
    .............
}
mvn test -Dtest=UserTests#testUserRequest

Output (filtered)

$ mvn test -Dtest=UserTests#testUserRequest
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.UserTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.794 s -- in com.logicbig.example.UserTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.732 s
[INFO] Finished at: 2026-09-29T05:24:14-05:00
[INFO] ------------------------------------------------------------------------

The request maps to the handler method that returns a String.

Now let's make request /user with xml file extension i.e. /user.xml:

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testUserRequestWithXmlExtension() throws Exception {

    MockHttpServletRequestBuilder builder = get("/user.xml").param("id", "100");

    this.mockMvc
        .perform(builder)
        .andExpect(status().isOk())
        .andExpect(
            content()
                .string(
                    containsString(
                        "<user><emailAddress>joe@example.com"
                            + "</emailAddress><id>100</id>"
                            + "<name>joe</name></user>")));
  }
    .............
}
mvn test -Dtest=UserTests#testUserRequestWithXmlExtension

Output (filtered)

$ mvn test -Dtest=UserTests#testUserRequestWithXmlExtension
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.UserTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.622 s -- in com.logicbig.example.UserTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.471 s
[INFO] Finished at: 2026-09-29T05:33:08-05:00
[INFO] ------------------------------------------------------------------------

This time we got an XML response, even though we did not specify an 'Accept' header. The xml extension in the request URI caused the request to map to the handler method that produces an XML response.

Specifying the Extension in the Controller Mapping

Now let's put the path extension in the @RequestMapping path, without using the 'produces' element.

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("super-user.xml")
public class SuperUserController {

  @RequestMapping
  @ResponseBody
  public User getUserById(@RequestParam("id") long userId) {
    User user = new User();
    user.setId(userId);
    user.setName("tim");
    user.setEmailAddress("tim@example.com");
    return user;
  }
}

Let's invoke a test with request /super-user:

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testSuperUserWithoutExtension() throws Exception {

    MockHttpServletRequestBuilder builder =
        MockMvcRequestBuilders.get("/super-user").param("id", "200");

    this.mockMvc.perform(builder).andExpect(MockMvcResultMatchers.status().isNotFound());
  }
    .............
}
mvn test -Dtest=UserTests#testSuperUserWithoutExtension

Output (filtered)

$ mvn test -Dtest=UserTests#testSuperUserWithoutExtension
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.UserTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.609 s -- in com.logicbig.example.UserTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.521 s
[INFO] Finished at: 2026-09-29T05:40:07-05:00
[INFO] ------------------------------------------------------------------------

The test with status 404 passed because request didn't have .xml extension.

Let's use the file extension with the request /super-user.xml

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class UserTests {
  @Autowired private WebApplicationContext wac;
  private MockMvc mockMvc;
    .............
  @Test
  public void testSuperUserWithExtension() throws Exception {

    MockHttpServletRequestBuilder builder = get("/super-user.xml").param("id", "200");

    this.mockMvc
        .perform(builder)
        .andExpect(status().isOk())
        .andExpect(
            content()
                .string(
                    containsString(
                        "<user><emailAddress>"
                            + "tim@example.com</emailAddress>"
                            + "<id>200</id><name>tim</name>"
                            + "</user>")));
  }
}
mvn clean test -Dtest=UserTests#testSuperUserWithExtension

Output (filtered)

$ mvn clean test -Dtest=UserTests#testSuperUserWithExtension
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.logicbig.example.UserTests
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.086 s -- in com.logicbig.example.UserTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.456 s
[INFO] Finished at: 2026-09-29T05:50:39-05:00
[INFO] ------------------------------------------------------------------------

Conclusion

If we don't send an 'Accept' header with the request and use a file extension (xml) instead, the request maps to the handler method that 'produces' an XML response. That happens because of the path extension content negotiation strategy. Similarly, putting the file extension in the @RequestMapping path, instead of specifying the 'produces' element, also works, and the path extension strategy is used to resolve the media type. In both cases, the request URI must include the file extension (e.g. /user.xml).

Note that the path extension strategy used to resolve the extensions json, xml, rss and atom by default, provided the corresponding libraries (for example a JSON or XML message converter and a feed library) were on the classpath.

Again, this feature is legacy. It was deprecated because path extensions make request mapping ambiguous and can expose applications to Reflected File Download (RFD) attacks. It is not available in Spring Framework 7.0 and later.

What to Use Instead (Spring Framework 7 and Later)

The recommended approach is the standard 'Accept' request header, optionally combined with the 'produces' element of @RequestMapping (or @GetMapping), for example:

$ curl -H "Accept: application/xml" "http://localhost:8080/user?id=100"

If clients cannot set headers (for example, links opened in a browser), the query parameter strategy is still supported. It resolves the media type from a request parameter such as /user?id=100&format=xml:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true)
                  .parameterName("format")
                  .mediaType("xml", MediaType.APPLICATION_XML)
                  .mediaType("json", MediaType.APPLICATION_JSON);
    }
}

When migrating from a path extension setup, remove calls to favorPathExtension() and ignoreUnknownPathExtensions() from your configuration, since they no longer compile in Spring Framework 7.0.

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 5.0.0.RELEASE (Spring Web MVC)
     Version Compatibility: 3.2.9.RELEASE - 5.2.25.RELEASEVersion List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE

    Versions in green have been tested.

  • spring-test 5.0.0.RELEASE (Spring TestContext Framework)
  • javax.servlet-api 3.0.1 (Java Servlet API)
  • junit-jupiter-engine 5.0.0 (Module "junit-jupiter-engine" of JUnit 5)
  • hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
  • JDK 1.8
  • Maven 3.9.11

Spring MVC - Path Extension Content-Negotiation Strategy Example Select All Download
  • content-negotiation-path-extension-strategy
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • SuperUserController.java
        • test
          • java
            • com
              • logicbig
                • example

    See Also

    Join