Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using the MvcUriComponentsBuilder to get a URL to a controller method doesnt add parameters when the value complex #33989

Open
lildadou opened this issue Nov 29, 2024 · 0 comments
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: waiting-for-triage An issue we've not yet triaged or decided on

Comments

@lildadou
Copy link

I'm developing a Spring boot with Spring MVC web app using rest controllers. I'd like to redirect URLs that have empty parameters to the same URL but without its parameters; the idea being to explicitly remove the semantic ambiguity between an “empty filter” and “no filter”. I'm using the Links to Controllers to do this.

Sample:

@RestController
@RequestMapping(path = "/foo")
public class MyRestController {
    @GetMapping(path = "/bar")
    public ResponseEntity<List<Integer>> search(@ModelAttribute @NotNull BarRequest barRequest, @RequestParam(defaultValue = "10") Integer limit) {
        final BarRequest disambiguatedBarRequest = barRequest.disambiguated();
        if (barRequest != disambiguatedBarRequest) {
            return ResponseEntity
                    .status(HttpStatus.PERMANENT_REDIRECT)
                    .header(HttpHeaders.LOCATION, MvcUriComponentsBuilder
                            .fromMethodName(this.getClass(), "search", disambiguatedBarRequest, limit).build()
                            .encode().toUri().toASCIIString())
                    .build();
        }

        return ResponseEntity.ok(generateRandomArray(limit, 0, 100));
    }

    // credits: Ashish Lahoti
    public static List<Integer> generateRandomArray(int size, int min, int max) {
        return IntStream
                .generate(() -> min + new Random().nextInt(max - min + 1))
                .limit(size).boxed().toList();
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class BarRequest {
        private Set<String> categories;
        private Set<String> otherFilters;

        public BarRequest disambiguated() {
            BarRequest candidate = BarRequest.builder()
                    .categories(disambiguateStringSet(categories))
                    .otherFilters(disambiguateStringSet(otherFilters))
                    .build();
            if (Objects.equals(candidate, this)) {
                return this;
            } else {
                return candidate;
            }
        }

        private Set<String> disambiguateStringSet(Set<String> set) {
            return Optional.ofNullable(set)
                    .map(categories -> categories.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
                    .filter(c -> !c.isEmpty())
                    .orElse(null);
        }
    }
}

In this example, the URI /foo/bar?categories=not_empty&otherFilters=&limit=3 is expected to result in a redirect to /foo/bar?categories=not_empty&limit=3 but I am redirected to /foo/bar?limit=3 instead.

The problem comes from MvcUriComponentsBuilder which only prepares the url for “simple” method parameters (Integer, String, Set).

@jcagarcia explains well how MvcUriComponentsBuilder works in this post and it allows us to to identify how these "contributors" are chosen and used (CompositeUriComponentsContributor):

@Override
public void contributeMethodArgument(
		MethodParameter parameter, Object value,
		UriComponentsBuilder builder, 
		Map<String, Object> uriVariables, 
		ConversionService conversionService) {

	for (Object contributor : this.contributors) {
		if (contributor instanceof UriComponentsContributor) {
			UriComponentsContributor ucc = (UriComponentsContributor) contributor;
			if (ucc.supportsParameter(parameter)) {
				ucc.contributeMethodArgument(parameter, value, builder, uriVariables, conversionService);
				break;
			}
		}
		else if (contributor instanceof HandlerMethodArgumentResolver) {
			if (((HandlerMethodArgumentResolver) contributor).supportsParameter(parameter)) {
				break;
			}
		}
	}
}

In my example, it is contributor ServletModelAttributeMethodProcessor who is chosen but as he does not implement UriComponentsContributor then MvcUriComponentsBuilder will simply ignore these parameters, resulting in an incomplete URL.

You will find a ready-to-run example on this repository.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Nov 29, 2024
@rstoyanchev rstoyanchev added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Feb 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) status: waiting-for-triage An issue we've not yet triaged or decided on
Projects
None yet
Development

No branches or pull requests

3 participants