I have a single page Angular app with Spring Boot. It looks like the following:
src
main
java
controller
HomeController
CustomerController
OtherController
webapp
js/angular-files.js
index.html
Spring boot correctly defaults to webapp folder and serves index.html file.
What I am looking to do is:
For every local REST request not starting with /api overwrite and redirect to default webapp/index.html. I plan to serve anything /api to the spring controllers.
Is there a way to prefix all controllers with API so that I do not have to write API every time?
e.g.
#RequestMapping("/api/home") can write shorthand in code #RequestMapping("/home")
or
#RequestMapping("/api/other-controller/:id") can write shorthand #RequestMapping("/other-controller/:id")
I'm looking for every API request, e.g. 1) http://localhost:8080/api/home keep API with API and resolve to correct controller and return JSON, however if someone enters a URL like http:///localhost/some-url or http:///localhost/some-other/123/url then it will serve the index.html page and keep the URL.
Alternative ways to do it: try adding #ErrorViewResolver:
Springboot/Angular2 - How to handle HTML5 urls?
If you're tired of trying to solve this problem by following so many conflicting solutions - look here!!
After hours upon hours trying to follow all the scattered advice from dozens of stack overflow and blog posts, I've finally found the minimum PURE spring boot + angular 6 application to always redirect to index.html after a refresh on a non-root page WHILE maintaining all your REST API endpoint paths. No #EnableWebMvc, no #ControllerAdvice, no changes to application.properties, no custom ResourceHandlerRegistry modifications, just simplicity:
Very important pre-requisite
You *must* include the output of ng build into Spring's resources/static folder. You can accomplish this via the maven-resources-plugin. Learn here: Copying multiple resource directories to independent target directories with maven
Code
#Controller
#SpringBootApplication
public class MyApp implements ErrorController {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public String error() {
return "forward:/index.html";
}
#Override
public String getErrorPath() {
return PATH;
}
}
Reasoning
Including the output of ng-build into resources/static at build time allows spring view redirects ("forward:/index.html") to succeed. It seems spring cannot redirect to anything outside of the resources folder so if you're trying to access pages at the root of the site, it won't work.
With default functionality (i.e. no additions of #EnableWebMvc or changes to application.properties) navigating to / automatically serves the index.html (iff it was included in the resources/static folder) so no need to make changes there.
With default functionality (as stated above), any error encountered in a spring boot app routes to /error and implementing ErrorController overrides that behavior to - you guessed it - route to index.html which allows Angular to take over the routing.
Remarks
Don't settle for the HashLocationStrategy to get over this problem as it is not recommended by Angular: https://angular.io/guide/router#which-strategy-is-best
For every local REST request not starting with /api overwrite and redirect to default webapp/index.html. I plan to serve anything /api to the spring controllers.
Update 15/05/2017
Let me re-phrase your query for other readers. (Correct me, if misunderstood)
Background
Using Spring Boot and Serving static resources from classpath
Requirement
All 404 non api requests should be redirected to index.html.
NON API - means Requests in which URL doesn't start with /api.
API - 404 should throw 404 as usual.
Sample Response
/api/something - will throw 404
/index.html - will server index.html
/something - will redirect to index.html
My Solution
Let the Spring MVC throw exceptions, if any handler is not available for the given resource.
Add following to application.properties
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
Add a ControllerAdvice as follows
#ControllerAdvice
public class RedirectOnResourceNotFoundException {
#ExceptionHandler(value = NoHandlerFoundException.class)
public Object handleStaticResourceNotFound(final NoHandlerFoundException ex, HttpServletRequest req, RedirectAttributes redirectAttributes) {
if (req.getRequestURI().startsWith("/api"))
return this.getApiResourceNotFoundBody(ex, req);
else {
redirectAttributes.addFlashAttribute("errorMessage", "My Custom error message");
return "redirect:/index.html";
}
}
private ResponseEntity<String> getApiResourceNotFoundBody(NoHandlerFoundException ex, HttpServletRequest req) {
return new ResponseEntity<>("Not Found !!", HttpStatus.NOT_FOUND);
}
}
You can customize the error message as you like.
Is there a way to prefix all controllers with api so that I do not have to write api every time.
For this, you can create a BaseController and set the RequestMapping path to /api
Example
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RequestMapping("/api")
public abstract class BaseController {}
And extend this BaseController and make sure you do not annotate child class with #RequestMapping
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class FirstTestController extends BaseController {
#RequestMapping(path = "/something")
public String sayHello() {
return "Hello World !!";
}
}
Previous Answer
You can create a Filter which redirects to /index.html if request path doesn't startsWith /api.
// CODE REMOVED. Check Edit History If you want.
Try this instead
#SpringBootApplication
#Controller
class YourSpringBootApp {
// Match everything without a suffix (so not a static resource)
#RequestMapping(value = "/**/{path:[^.]*}")
public String redirect() {
// Forward to home page so that route is preserved.(i.e forward:/intex.html)
return "forward:/";
}
}
#Controller
public class RedirectController {
/*
* Redirects all routes to FrontEnd except: '/', '/index.html', '/api', '/api/**'
*/
#RequestMapping(value = "{_:^(?!index\\.html|api).*$}")
public String redirectApi() {
return "forward:/";
}
}
Too late on this thread, but thought it might help someone
Tried many solutions, but this looked pretty straight forward and great to me
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
#Configuration
public class MvcConfiguration implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Credits: https://keepgrowing.in/java/springboot/make-spring-boot-surrender-routing-control-to-angular/
The solution that works to me is to overwrite the BasicErrorController of Spring Boot:
#Component
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes, new ErrorProperties());
}
#RequestMapping(produces = "text/html")
#Override
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NOT_FOUND) {
return new ModelAndView("forward:/");
} else {
return super.errorHtml(request, response);
}
}
}
The method errorHtml only intercepts not found requests and is transparent for responses 404 (not found) from the api.
Most reasonable solution, imho, for Spring Boot 2+ (code is in Kotlin):
#Component
class ForwardErrorsToIndex : ErrorViewResolver {
override fun resolveErrorView(request: HttpServletRequest?,
status: HttpStatus?,
model: MutableMap<String, Any>?): ModelAndView {
return ModelAndView("forward:/index.html")
}
}
For whole application, you can add context path in application.properties
server.contextPath=/api
It will append "/api" to every requested URL after http://localhost:8080/api/home
For Redirection,
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/home");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers(registry);
}
Put this bunch of code in WebMVCConfig.java
In the #Configuration bean you can add a ServletRegistrationBean to make the spring server for the /api/* resquest only, then in the Controller you don't need to add it.
#Bean
public ServletRegistrationBean dispatcherRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(
dispatcherServlet());
registration.addUrlMappings("/api/*");
registration.setLoadOnStartup(1);
registration.setName("mvc-dispatcher");
return registration;
}
I don't know why, but the root url "/" would not resolve without adding a little more code. This is what I ended up with.
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
#EnableWebMvc
#Configuration
public class MvcConfiguration implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
registry.addResourceHandler("/**/*")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
#Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource requestedResource = location.createRelative(resourcePath);
return requestedResource.exists() && requestedResource.isReadable() ? requestedResource
: new ClassPathResource("/static/index.html");
}
});
}
}
Ok, let's start with the simple part of your question:
Is there a way to prefix all controllers with api so that I do not have to write api every time?
The answer is yes, just mark your controller with a "global" #RequestMapping annotation, for example:
#RestController
#RequestMapping("/api")
public class ApiController{
#RequestMapping("/hello")
public String hello(){
return "hello simple controller";
}
#RequestMapping("/hello2")
public String hello2(){
return "hello2 simple controller";
}
}
In the example above you can invoke hello method with this URL: /api/hello
and the second method with this URL: /api/hello2
This is how I didn't have to mark each method with /api prefix.
Now, to the more complex part of your question:
is how to achieve a redirect if the request doesn't start with /api prefix?
You can do it by returning an HTTP status code (302) of Redirect, after all, angularJs "speaks" REST natively, thus you can't force a redirect from Java/Spring code like you use to.
Then just return an HTTP message with the status code of 302, and on your angularJS do the actual redirection.
For example:
On AngularJS:
var headers = {'Content-Type':'application/json', 'Accept':'application/json'}
var config = {
method:'GET'
url:'http://localhost:8080/hello',
headers:headers
};
http(config).then(
function onSuccess(response){
if(response.status == 302){
console.log("Redirect");
$location("/")
}
}, function onError(response){
console.log("An error occured while trying to open a new game room...");
});
On Spring:
#RestController
#RequestMapping("/api")
public class ApiController{
#RequestMapping("/hello")
public ResponseEntity<String> hello(){
HttpHeaders header = new HttpHeaders();
header.add("Content-Type", "application/json");
return new ResponseEntity<String>("", header, HttpStatus.FOUND);
}
}
of course, you'll need to custom it to your project.
All you need to try is put the index.html to src/main/resources/static/
See Example: https://github.com/reflexdemon/shop/tree/master/src/main/resources/static
In my package.josn I try to copy it to this location.
See PackageJSON: https://github.com/reflexdemon/shop/blob/master/package.json#L14
I'm using Camel to route http requests. A client reaches my Camel router at a servlet endpoint providing the information required to route the request, then I lookup into the database to resolve the endpoint. The requests are routed correctly but the response I get seems to be corrupted (I'm making the call from a Rest client), here the corrupted response:
If I call the destination endpoint without passing from camel it returns the right response:
I also checked that the response leaves the destination endpoint not corrupted.
Here my Camel configuration classes:
#Singleton
#Startup
public class CamelStartupBean {
#PostConstruct
public void init() {
CamelContext camelContext = new DefaultCamelContext();
camelContext.addRoutes(new CamelRouteConfiguration());
camelContext.start();
}
static class CamelRouteConfiguration extends RouteBuilder {
#Override
public void configure() {
from("servlet:callService?matchOnUriPrefix=true")
.routeId("callService")
.recipientList(method(CallServiceConfiguration.class, "resolveServiceRoute"));
}
}
static class CallServiceConfiguration {
public String resolveServiceRoute(Exchange exchange) {
String route;
// lookup the database to find a route ...
route = "http://demo.apps.closhlab.osh.local/rest/DemoService?bridgeEndpoint=true";
return route;
}
}
}
I'm using Camel 3.9.0 and my app is deployed on the Docker image jboss/wildfly:15.0.0.Final.
Any idea? Thank you.
Upgrading to Camel 3.11.0 solved the issue.
I am implementing a simple authentication program using AngularJS frontend and Spring Boot backend. I am facing an issue while sending the login request. When the relavent request sent, following error prints in the console
Error:-
Access to XMLHttpRequest at 'http://localhost:8080/rest/users/user' from origin 'http://localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
POST http://localhost:8080/rest/users/user net::ERR_FAILED
Same error occurred when sending the request for registration function. Then I found a solution of adding #CrossOrigin(origins = "http://localhost:4200") to the controller. It fixed the error occured during the registration.
Now the problem is even though I have written the login method in the same controller, it gives me the error while trying to log in and will not gives any error if I try to register new user.
Below is the implementation of the backend
Repository :-
import com.app.cashier.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.web.bind.annotation.CrossOrigin;
import java.util.List;
public interface UsersRepository extends JpaRepository<User, Integer> {
List<User> findByUserName(String userName);
}
Resource :-
package com.app.cashier.resource;
import com.app.cashier.model.User;
import com.app.cashier.repository.UsersRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#CrossOrigin(origins = "http://localhost:4200") //<------------ I addded #CrossOrigin
#RestController
#RequestMapping(value = "/rest/users")
public class UserResource {
#Autowired
UsersRepository usersRepository;
#GetMapping(value = "/all")
public List<User> getAll(){
return usersRepository.findAll();
}
#GetMapping(value = "/user") //<----- Login function. This still gives the above error
public List<User> getUser(#RequestBody final User user){
return usersRepository.findByUserName(user.getUserName());
}
#PostMapping(value = "/load") //<----- Registration function. This gives no error after adding #CrossOrigin
public List<User> persist(#RequestBody final User user){
usersRepository.save(user);
return usersRepository.findAll();
}
}
AngularJS frontend request
login(userName) {
console.log(userName)
return this.http.post<any>(`http://localhost:8080/rest/users/user`, { userName })
.pipe(map(user => {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify(user));
this.currentUserSubject.next(user);
console.log(user);
return user;
}));
}
How can I overcome this issue. Massive thanks!
Providing the #CrossOrigin annotation at the controller level should enable cross origin for all the methods under that controller.lar request so it might be because of some additional headers that you are adding for that particular request so try like :
#CrossOrigin(origins = "http://localhost:4200", allowedHeaders = "*")
#RestController
#RequestMapping(value = "/rest/users")
public class UserResource {
//Your code
}
If still having issues then Could you share the url and the headers that you are using to login the new user ?. Also , try having a global cors configuration instead of a controller level one and provide fine grained properties like the methods that you want to expose. Provide the following in a configuration class :
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/greeting-javaconfig").allowedOrigins("http://localhost:4200");
}
};
}
Similar : CORS policy conflict in Spring boot
I have example code below, why is the process method in MockEndpoint.whenAnyExchangeReceived NOT executed?
I expect the response is "Expected Body from mock remote http call", but the actual response is what passed in request("Camel rocks").
public class CamelMockRemoteHttpCallTest extends CamelTestSupport {
#Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
from("direct:start")
.to("http://abc/bcd")
;
}
};
}
#Override
public String isMockEndpointsAndSkip() {
return "http://abc/bcd";
}
#Test
public void testSimulateErrorUsingMock() throws Exception {
MockEndpoint http = getMockEndpoint("mock:http://abc/bcd");
http.whenAnyExchangeReceived(new Processor() {
public void process(Exchange exchange) throws Exception {
exchange.getOut().setBody("Expected Body from mock remote http call"); //why this line doesn't execute
}
});
String response = template.requestBody("direct:start", "Camel rocks", String.class);
assertEquals("Expected Body from mock remote http call", response); //failed, the actual response is "Camel rocks"
}
}
I have added some breakpoints to your test and it seems, that automatically created mock endpoint is mock://http:abc/bcd, not mock:http://abc/bcd.
To find, why is this happening, you can look to method org.apache.camel.impl.InterceptSendToMockEndpointStrategy#registerEndpoint, which is called as part of mock endpoint auto registration. There is // removed from http URI. And then to org.apache.camel.util.URISupport#normalizeUri method, where is // added for mock uri prefix.
There is also nice comment in implementation of InterceptSendToMockEndpointStrategy, but I couldn't find it mentioned in documentation.
// create mock endpoint which we will use as interceptor
// replace :// from scheme to make it easy to lookup the mock endpoint without having double :// in uri
When you change it to getMockEndpoint("mock://http:abc/bcd"), the test passes.
The best way to avoid these issues, is pass false as second parameter of getMockEndpoint() method, if you expect already created endpoint. This will throw exception, if mock endpoint does not exists. Otherwise is new mock endpoint created on demand.
I have this problem using cxf dispatching behavior.
I have developed an Interceptor that implements the org.apache.cxf.jaxrs.ext.RequestHandler interface.
In its "public Response handleRequest(Message m, ClassResourceInfo resourceClass)" method I throw an exception (e.g. a WebServiceException) or a Fault. I have not apparent problems but, on the client side, the client receives a different exception (a ServerWebApplicationException) with the error message empty.
Here the code:
Server side:
public class RestInterceptor implements RequestHandler {
......
#Override
public Response handleRequest(Message m, ClassResourceInfo resourceClass){
.....
throw new WebServiceException("Failure in the dispatching ws invocation!");
.....
}
}
ServerWebApplicationException received on client side:
Status : 500
Headers :
Content-Length : 0
Connection : close
Server : Jetty(7.x.y-SNAPSHOT)
cause=null
detailMessage=null
errorMessage=""
.....
I received the same exception also if I throw a Fault.
What is the problem? I have to use another exception? Why?
Thanks a lot,
Andrea
OK, I've found the solution.I've registered an ExceptionMapper on the dispatcher and use it to encapsulate the exception inside the Response sent to the client.
To do this the interceptor is registered as provider at the web service publication and it implements the "ExceptionMapper" interface. In its "toResponse" method it encapsulates the exception in the Response.
Look at code:
public static<T extends Throwable> Response convertFaultToResponse(T ex, Message inMessage) {
ExceptionMapper<T> mapper = ProviderFactory.getInstance(inMessage).createExceptionMapper(ex.getClass(), inMessage);
if (mapper != null) {
try {
return mapper.toResponse(ex);
} catch (Exception mapperEx) {
mapperEx.printStackTrace();
return Response.serverError().build();
}
}
return null;
}
#Override
public Response toResponse(Exception arg0) {
ResponseBuilder builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR).type("application/xml");
String message = arg0.toString();
return builder.entity(message).build();
}
Andrea
Annotate your exception with #WebFault
example :
#WebFault(name = "oauthFault")
public class OAuthException extends RuntimeException {
...
}