cors issue when throwing a runtime exception with spring and docker

Viewed 728

I' building a web app using spring, angular and docker. I also have for the moment two environments. A dev environment where angular is served using npm start and spring using mvn spring-boot:run. And a test environment where I use digitalocean to host my app and containerize the angular and spring apps using docker.

In this application I have a registration page where user can register and a spring controller to handle registration request. Here is the code for my controller :

@PostMapping("/register")
public SaveUserDto register(@RequestBody @Valid SaveUserDto saveUserDto, WebRequest request) {

        User userToSave = this.modelMapper.map(saveUserDto, User.class);
        User registeredUser = null;
        try {
            registeredUser = this.registrationService.register(userToSave);
        } catch (DataIntegrityViolationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            if(e.getRootCause().toString().contains("uk_pfki8mu6sefesty9h30d4n3yv")) {
                throw new EmailAlreadyExistException();
            }
        }
        try {
            eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registeredUser, request.getLocale(), request.getHeader("Origin")));
        } catch (Exception me) {
            System.out.println(me.getMessage());
        }

        if(registeredUser != null) {
            return this.modelMapper.map(registeredUser, SaveUserDto.class);
        } else {
            return null;
        }
}

Since my angular is served from host:4200 (where host is either localhsot in my dev environment, or 165.22.72.253 in my test environment) and my api is served from host:8761 I have a cors configuration to enable cors request. Here is the configuration :

@Bean
public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("http://" + clientHost + ":4200");
        config.addAllowedOrigin("http://" + clientHost + ":80");
        config.addAllowedHeader("*");
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("DELETE");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
}

Here clientHost is a parameter with either value is localhost when running in dev environment or 165.22.72.253 for my test environment. For most use case this setting is working but it fails in one particular occasion.

The problem arise when a user try to register whith a existing email adress. Then as you can see from the controller method I throw an EmailAlreadyExistException. In dev env everything is working great that is the request return an error code 500 with a message "Email already exist". But in the test environment I get Access to XMLHttpRequest at 'http://165.22.72.253:8762/register' from origin 'http://165.22.72.253:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource..

If useful here is my docker-compose file :

version: "3.5"

services:

  tomcat-file:
    container_name: tomcat-file-server
    image: tomcat-file-server
    build:
      context: .
      dockerfile: tomcat-file.prod.dockerfile
    volumes:
      - /home/vetter_leo/medias:/usr/local/tomcat/src
    ports:
      - "8205:8205"

networks:
  default:
    external:
      name: helenos-network

At this point I don't understand why it's working in dev env and not in test env. Any help will be greatly appreciated. Thanks in advance.

5 Answers

What I finaly ended up to do was adding an handler to the exception in the controller that return me the right message : this way :

@ExceptionHandler({ EmailAlreadyExistException.class })
public String handleException() {
    return "{\"error\": \"" + messages.getMessage("emailAlreadyExist", null, Locale.ENGLISH) + "\"}";
}

Did you try to proxy the API calls? In the angular project, you can create a proxy.conf.json file and add below configurations.

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false
  }
}

Then in the package.json change the start,

"start": "ng serve --proxy-config proxy.conf.json"

If you want to do it from the psring side add @CrossOrigin to controller.

But if you think you are going to face the same issue in production, my advice is to use a reverse proxy like Nginx. You're Angular application and APIs are served via the same port but all the APIs are prefixed with api. From the Nginx side, you can filter API calls using api prefix and proxy them to backend service.

Spring boot uses a default error controller. You can supply your own and add an Access-Control-Allow-Credentials header to the response:

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class CustomErrorController implements ErrorController {

    @RequestMapping("/error")
    public String handleErrors(HttpServletRequest request, HttpServletResponse response) {
        response.addHeader("Access-Control-Allow-Credentials","true");
        String errorMessage = request.getAttribute(RequestDispatcher.ERROR_MESSAGE).toString();

        return errorMessage;
    }

    @Override
    public String getErrorPath() {
        return "/error";
    }
}

Maybe a problem that the exception is thrown before the cors filter is called?

Can you try this?

  //...

  source.registerCorsConfiguration("/**", config);
  FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
  bean.setOrder(Ordered.HIGHEST_PRECEDENCE);

  return bean;
}
Related