Cèsar Ordiñana opened SPR-14890 and commented
I'm developing a Spring boot with Spring MVC web app using Thymeleaf views. I'm using the Building URIs to Controllers and methods from views feature through the Thymeleaf #mvc.url utility.
I have the following sample controller:
@Controller
@RequestMapping(value = "/categories/{category}/products",
name = "CategoriesItemProductsThymeleafController",
produces = MediaType.TEXT_HTML_VALUE)
public class CategoriesItemProductsThymeleafController {
public CategoryService categoryService;
@Autowired
public CategoriesItemProductsThymeleafController(CategoryService categoryService) {
this.categoryService = categoryService;
}
@PostMapping(name = "addToProducts")
@ResponseBody
public ResponseEntity<?> addToProducts(@ModelAttribute Category category,
@RequestParam("products") List<Long> products) {
categoryService.addToProducts(category, products);
return ResponseEntity.ok().build();
}
}
In the Thymeleaf view I get the link to that controller method with something like:
<div id="create-url" data-data-create-url="${(#mvc.url('CategoriesItemProductsThymeleafController#addToProducts')).buildAndExpand('CATEGORY_ID')}"></div>
It uses internally the MvcUriComponentsBuilder.fromMappingName(String) method which returns a MethodArgumentBuilder. It expects a list of product ids as a request parameter, but I can't use the MethodArgumentBuilder.arg() method because the values I want to send are to be selected in javascript.
The problem with this method is that the Url return is the following one:
/categories/CATEGORY_ID/products?products
I use that Url as a parameter to call jQuery.ajax() using my own products values. If I use a GET method, the url ends up being:
/categories/CATEGORY_ID/products?products&products=1&products=2
Then the controller method receives the following list of values: [null, 1, 2]. As a workaround I can remove those null values from the list, but I think the Url is not constructed as it should.
After some debugging I've found the code which add those empty parameters is the HierarchicalUriComponents.getQuery() method. It has the following code:
for (Object value : values) {
if (queryBuilder.length() != 0) {
queryBuilder.append('&');
}
queryBuilder.append(name);
if (value != null) {
queryBuilder.append('=');
queryBuilder.append(value.toString());
}
}
The problem could be solved by changing the code to add the parameter only if value!=null.
Affects: 4.3.3
Issue Links: - #18113 UriComponentsBuilder interprets empty request parameters as null
1 votes, 3 watchers
Comment From: spring-projects-issues
Rossen Stoyanchev commented
The resulting list of values [null, 1, 2] seems odd but it does reflect the actual URL. The behavior is discussed under #18113 and is intentional and the treatment of empty parameters is also consistent across single vs multi-valued parameters.
If I understand correctly the URL contains "products&"
mainly because it is pre-generated first on the server side without any values and then used later on the client side where actual values are appended. So the URL is not quite what it should be. This approach could lead to other subtle issues where unnecessary empty parameters are combined with actual values.
Thinking about an alternative approach one could argue that anything in the query of the pre-generated URL can be safely ignored because it is generated based on declared request parameters but without values. The MvcUriComponentsBuilder.fromMappingName(String)
was actually created as a compromise to use in HTML templates. In this case it brings no value I can see. Why not pre-generate the URL in Java code instead, from the controller and adding it to the model as an attribute. Something like this which is strongly typed and does not require using a mapping name:
fromMethodCall(on(CategoriesItemProductsThymeleafController.class).addToProducts(null, null))
.replaceQuery(null)
.build()
.toUriString();
Comment From: spring-projects-issues
Cèsar Ordiñana commented
We've found it's not only the case of appending values in the URL, but also when you use it in a form. With the previous example, we use the given URL for the action form attribute, and then we have a products select multiple input field. When the user send the form data with a POST, we get the same result, a list of product ids with the first one being null.
It would be nice if there was an option to remove those empty parameters. Otherwise I think this reduces a lot the usefulness of the mvc.url utility and the MvcUriComponentsBuilder.fromMappingName(String) method. It that won't change, it would be a good idea to add a note about this limitation in the documentation.
We wanted to use the utility to generate all the links in our HTML templates. But now we have to use a different approach to generate the form and dynamic links. In addition the default mapping name strategy is very simple (capital letters of the controller class name), so we had a lot of name coincidences and had to fill the name parameter in all our controller class and method request mappings.
We'll have to try going another route, trying the option to provide the URLs in the model as you suggested, or maybe developing our own utility.
Comment From: spring-projects-issues
Juan Carlos García del Canto commented
Seems like the problem described by Cèsar appears when the method contains a parameter annotated with @RequestParam
or @PathVariable
.
The MvcUriComponentsBuilder obtains a list of contributors that will be used to evaluate the method parameter. In case that some contributor manages the annotated parameter type, it will be added to a Map
called uriVars
where the key is the parameter name and the value is that one specified in the MvcUriComponentsBuilder.fromMethodCall
. Finally, the URL will be expanded with the provided values registered in the map.
(Check this method here)
In this case, the contributor RequestParamMethodArgumentResolver and the contributor PathVariableMethodArgumentResolver knows how to manage these annotated parameters, so they're beeing replaced with the values provided in the MvcUriComponentsBuilder.fromMethodCall
although the provided value is null.
I think that to use the MvcUriComponentsBuilder
to generate the URL from the view layer (as Cèsar proposes) is really useful in that views where the URLs will be calculated dynamically. In that cases I don't want to provide an specific value to a parameter in the URL, I just want to replace that uriVar
with an static string that will be replaced dynamically by some Javascript function.
To solve this problem and to be able to include CATEGORY_ID
string in the provided URL, I've just extended the MvcUriComponentsBuilder class and the necessary static methods to be able to overwrite the applyContributors method. Now, before expand the URL it's checking if some of the included elements of the uriVars
has null value. If exists some entry with a null value, it will be removed.
Check my custom implementation here
Would be great to find a mixed solution that allows developers to replace all kinds of annotated parameters with whatever we want when the value provided in the MvcUriComponentsBuilder.fromMethodCall
for this parameter is null.