Search code examples
springspring-booturl-rewritinghttp-status-code-301request-mapping

How to rewrite URLs with Spring (Boot) via REST Controllers?


Let's say I have the following controller with its parent class:

@RestController
public class BusinessController extends RootController {

    @GetMapping(value = "users", produces = {"application/json"})
    @ResponseBody
    public String users() {
        return "{ \"users\": [] }"
    }

    @GetMapping(value = "companies", produces = {"application/json"})
    @ResponseBody
    public String companies() {
        return "{ \"companies\": [] }"
    }

}

@RestController
@RequestMapping(path = "api")
public class RootController {

}

Data is retrieved by calling such URL's:

http://app.company.com/api/users
http://app.company.com/api/companies

Now let's say I want to rename the /api path to /rest but keep it "available" by returning a 301 HTTP status code alongside the new URI's

e.g. client request:

GET /api/users HTTP/1.1
Host: app.company.com

server request:

HTTP/1.1 301 Moved Permanently
Location: http://app.company.com/rest/users

So I plan to change from "api" to "rest" in my parent controller:

@RestController
@RequestMapping(path = "rest")
public class RootController {

}

then introduce a "legacy" controller:

@RestController
@RequestMapping(path = "api")
public class LegacyRootController {

}

but now how to make it "rewrite" the "legacy" URI's?

That's what I'm struggling with, I can't find anything Spring-related on the matter, whether on StackOverflow or elsewhere.

Also I have many controllers AND many methods-endpoints so I can not do this manually (i.e. by editing every @RequestMapping/@GetMapping annotations).

And project I'm working on is based on Spring Boot 2.1

Edit: I removed the /business path because actually inheritance doesn't work "by default" (see questions & answers like Spring MVC @RequestMapping Inheritance or Modifying @RequestMappings on startup ) - sorry for that.


Solution

  • I finally found a way to implement this, both as a javax.servlet.Filter AND a org.springframework.web.server.WebFilter implementation.

    In fact, I introduced the Adapter pattern in order to transform both:

    • org.springframework.http.server.ServletServerHttpResponse (non-reactive) and
    • org.springframework.http.server.reactive.ServerHttpResponse (reactive)

    because on the contrary of the Spring's HTTP requests' wrappers which share org.springframework.http.HttpRequest (letting me access both URI and HttpHeaders), the responses's wrappers do not share a common interface that does it, so I had to emulate one (here purposely named in a similar fashion, HttpResponse).

    @Component
    public class RestRedirectWebFilter implements Filter, WebFilter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
                throws IOException, ServletException {
            ServletServerHttpRequest request = new ServletServerHttpRequest((HttpServletRequest) servletRequest);
            ServletServerHttpResponse response = new ServletServerHttpResponse((HttpServletResponse) servletResponse);
    
            if (actualFilter(request, adapt(response))) {
                chain.doFilter(servletRequest, servletResponse);
            }
        }
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            if (actualFilter(exchange.getRequest(), adapt(exchange.getResponse()))) {
                return chain.filter(exchange);
            } else {
                return Mono.empty();
            }
        }
    
        /**
         * Actual filtering.
         * 
         * @param request
         * @param response
         * @return boolean flag specifying if filter chaining should continue.
         */
        private boolean actualFilter(HttpRequest request, HttpResponse response) {
            URI uri = request.getURI();
            String path = uri.getPath();
            if (path.startsWith("/api/")) {
                String newPath = path.replaceFirst("/api/", "/rest/");
                URI location = UriComponentsBuilder.fromUri(uri).replacePath(newPath).build().toUri();
                response.getHeaders().setLocation(location);
                response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
                response.flush();
                return false;
            }
            return true;
        }
    
        interface HttpResponse extends HttpMessage {
    
            void setStatusCode(HttpStatus status);
    
            void flush();
    
        }
    
        private HttpResponse adapt(ServletServerHttpResponse response) {
            return new HttpResponse() {
                public HttpHeaders getHeaders() {
                    return response.getHeaders();
                }
    
                public void setStatusCode(HttpStatus status) {
                    response.setStatusCode(status);
                }
    
                public void flush() {
                    response.close();
                }
            };
        }
    
        private HttpResponse adapt(org.springframework.http.server.reactive.ServerHttpResponse response) {
            return new HttpResponse() {
                public HttpHeaders getHeaders() {
                    return response.getHeaders();
                }
    
                public void setStatusCode(HttpStatus status) {
                    response.setStatusCode(status);
                }
    
                public void flush() {
                    response.setComplete();
                }
            };
        }
    
    }