1

I am trying to call my controller's delete method:

Spring:

@RestController
@RequestMapping("/api")
@CrossOrigin
public class Controller
{
    @DeleteMapping("thing/item/{name}")
    public void deleteThing(@PathVariable String name)
    {
        System.out.println("CALL"); // Never called!!!
    }
}

Angular 7:

deleteTemplate(name: string) {
    const url = `host/api/thing/item/${name}`;
    return this.http.delete(url);
}

I've found something about including options:

httpOptions = {
    headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

deleteTemplate(name: string) {
    const url = `${host}/api/thing/item/${name}`; // host is not relevant, same path with GET works.
    return this.http.delete(url, this.httpOptions);
}

But I don't think that it's even needed since everything I am sending is in link itself (no json).

Every time its:

WARN 15280 --- [io-8443-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'DELETE' not supported]

How to fix this?

DELETE is sent with 405 error and preceeding OPTIONS (with 200 code). It reaches server and does this error.

EDIT

Above is a sample of code that is simplified and still doesn't work. I understand your comments, but host can be anything and doesn't matter here. Its localhost now and as mentioned - it works in sense of reaching endpoint.

To check if I am wrong I did this:

@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;

this.requestMappingHandlerMapping.getHandlerMethods()
                .forEach((e, k) -> System.out.println(e + "   OF   " + k));

After startup, it printed everything I expected, including:

{GET /api/thing/item/{name}}   OF   public myPackage.Thing myPackage.Controller.getThing(java.lang.String)
{DELETE /api/thing/item/{name}}   OF   public void myPackage.Controller.deleteThing(java.lang.String)

Any ideas?

Ernio
  • 948
  • 10
  • 25
  • The controller is mapped to /api/thing/item/{name}. You send a request to host/api/thing/item/${name}. So that clearly doesn't match. Remove the `host` at the beginning. – JB Nizet Dec 27 '18 at 23:13
  • @JBNizet host is just something I wrote to represent host. Path is matched and it reaches server. Just to reassure - same mapping with get works fine. – Ernio Dec 27 '18 at 23:15
  • 2
    Don't do that. If you want us to find issues in your code, post the actual code. – JB Nizet Dec 27 '18 at 23:16
  • I'm agree with @JBNizet – Jonathan JOhx Dec 27 '18 at 23:21
  • does adding `@ResponseBody` to your method signature helps? `public @ResponseBody void deleteThing(@PathVariable String name) {}` – Ram Dec 28 '18 at 07:24

2 Answers2

2

Goddamn. Turns out problem was in CSRF, mainly in hidden Angular documentation and misleading Spring filter logs.

It took me about 10h to make it work, but only in production (where Angular is on same host/port with Spring). I will try to make it work in dev, but that requires basically rewriting whole module supplied by Angular that is bound to its defaults.

Now as to the problem:

Everything starts with Spring Security when we want to use CSRF (and we do). Apparently CsrfFilter literally explodes if it can't match CSRF tokens it expects and falls back to /error. This all happens without ANY specific log message, but simple Request method 'DELETE' not supported, which from my question we know IT IS present.

This is not only for DELETE, but all action requests (non-GET/HEAD).

This all points to "How to setup CSRF with Angular?". Well, here we go:

And it WORKS by DEFAULT. Based on Cookie-Header mechanism for CSRF.

But wait, there's more!

Spring needs to bo configured for Cookie-Header mechanism. Happily we got:

@Override
protected void configure(HttpSecurity http) throws Exception
{
    http.csrf().csrfTokenRepository(this.csrfRepo());
}

private CookieCsrfTokenRepository csrfRepo()
{
    CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
    repo.setCookieHttpOnly(false);
    return repo;
}

Which is built-in in spring and actually uses same cookie/header names as ones used by Angular. But what's up with repo.setCookieHttpOnly(false);? Well... another poor documented thingy. You just NEED to set it to false, otherwise Angular's side will not work.

Again - this only works for production builds, because Angular doesn't support external links. More at: Angular 6 does not add X-XSRF-TOKEN header to http request

So yeah... to make it work in dev localhost with 2 servers I'll need 1st to recreate https://angular.io/api/common/http/HttpClientXsrfModule mechanism to also intercept non-default requests.

EDIT

Based on JB Nizet comment I cooked up setup that works for both dev and production. Indeed proxy is awesome. With CSRF and SSL enabled (in my case), you also need to enable SSL on Angular CLI, otherwise Angular will not be able to use CSRF of SSL-enabled proxy backend and thus not work.

"serve": {
    "options": {
            "proxyConfig": "proxy.conf.json",
            "sslKey": "localhost.key",
            "sslCert": "localhost.crt",
            "ssl": true
    }
}

Note that Cert and Key will be auto-generated if not found :) For Spring backend you don't need same cert. I use separate JKS there.

Cheers!

enter image description here

Ernio
  • 948
  • 10
  • 25
  • 1
    If your target is to have one server serving everything, then do that in your development environment too. Just configure ng serve toact as a reverse-proxy for the REST API requests. It's 3lines of code, and itmakes everything much simpler. https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md – JB Nizet Dec 29 '18 at 00:50
  • 1
    @JBNizet Awesome! :) This will save me SO MUCH time in dev. – Ernio Dec 29 '18 at 02:53
0

Add a slash / at the beginning of your path.

@DeleteMapping("/thing/item/{name}")
Jonathan JOhx
  • 5,784
  • 2
  • 17
  • 33