简体   繁体   中英

Angular frontend POST to Golang backend method not allowed and blocked by CORS policy

I'm trying to make a post request from my Angular front end to my Golang back end, both served from the same machine. I keep getting:

OPTIONS http://localhost:12345/anteroom 405 (Method Not Allowed)

and

Access to XMLHttpRequest at 'http://localhost:12345/anteroom' 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.

Golang back end, using Gorilla mux router:

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/anteroom", anteroom).Methods("POST")
    log.Fatal(http.ListenAndServe(":12345", router))
}

func anteroom(res http.ResponseWriter, req *http.Request) {
    res.Header().Set("Access-Control-Allow-Origin", "*")
    // *Edit 17 Jan 19 12.44pm*: Billy, user268396, and Peter suggest that OPTIONS should be added. As mentioned below, I've already tried that.
    res.Header().Set("Access-Control-Allow-Methods", "POST")
    res.Header().Set("Content-Type", "application/json")

    // Put it into a struct variable and print a bit of it.
    _ = json.NewDecoder(req.Body).Decode(&member)
    fmt.Println(member.ID)
}

Angular front end component:

export class AnteroomComponent implements OnInit {
  public profile: string;

  constructor(private http: HttpClient, private cookieService: CookieService) {}

  ngOnInit() {}

  // This is the relevant function.
  // Triggered by a button.
  sendProfile(Profile) {
    let httpHeaders = new HttpHeaders({
      "Content-Type": "application/json",
      "Origin": "http://localhost:4200"
    });

    return this.http
      .post("http://localhost:12345/anteroom", this.profile, {
        headers: httpHeaders,
        observe: "response"
      })
      .subscribe(
        data => {
          console.log("POST Request is successful ", data);
        },
        error => {
          console.log("Error", error);
        }
      );
  }
}

Here're some of the many things I tried:

  • I read that it's the browser's job to set headers, not mine (which doesn't make sense to me because then what is HttpHeaders() for?), so I removed all the headers from Angular.

  • I tried enabling CORS in Golang as shown here :

     (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") if (*req).Method == "OPTIONS" { return } 
  • I tried changing Access-Control-Allow-Origin from * to Origin because I read somewhere that * prevents cookies from being sent/received. (Lost the link.) Maybe it prevents some other MIME types too? I don't know, just trying.

  • Mozilla says "The constructor initializes an XMLHttpRequest. It must be called before any other method calls." So I thought I'd do that but then Angular says "The HttpClient in @angular/common/http offers a simplified client HTTP API for Angular applications that rests on the XMLHttpRequest interface exposed by browsers." So, I guess that's not necessary? Constructor's already got HttpClient in it.

  • this.Profile seems pretty standard as JSON: {id: testid, username: "Mr Odour", age: "87"} . But maybe it's the problem. So I put it into String() . Then I dumped the response from the front end with Golang's httputil.DumpRequest() :

     output, err := httputil.DumpRequest(req, true) if err != nil { fmt.Println("Error dumping request:", err) return } fmt.Println(string(output)) 

    This provided a bit more insight, maybe. It printed out:

     POST /anteroom HTTP/1.1 Host: localhost:12345 Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9,en-GB;q=0.8 Connection: keep-alive Content-Length: 15 Content-Type: text/plain Dnt: 1 Origin: http://localhost:4200 Referer: http://localhost:4200/anteroom User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36 

I think it came through? I'm not sure. It doesn't say 200 OK. Console does print "POST Request is successful ", but without the data . I tried to read it with this in Golang:

body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(body)

This produced [] . Empty.

It also says Content-Type: text/plain . Is that the problem? Shouldn't it be application/json ? But other people's code uses it the way it is, without String() , like this :

return this.http.post<Article>(this.url,
     {
    id: 100, 
    title: 'Java Functional Interface', 
    category: 'Java 8', 
    writer: 'Krishna'
     }
  );
  • Edit 17 Jan 19 12.45pm : Billy suggests to use Header().Add() instead of Header().Set() like so:

     res.Header().Add("Access-Control-Allow-Origin", "*") res.Header().Add("Access-Control-Allow-Methods", "POST") res.Header().Add("Access-Control-Allow-Methods", "OPTIONS") res.Header().Add("Content-Type", "application/json") 

    So, I did. Didn't work either.

The back end seems ok. I posted to it with curl and it produces:

Connected to localhost (::1) port 12345 (#0)
> POST /anteroom HTTP/1.1
> Host: localhost:12345
> User-Agent: curl/7.47.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 126
>
} [126 bytes data]
* upload completely sent off: 126 out of 126 bytes
< HTTP/1.1 200 OK
*Edit 17 Jan 19 12.45pm*: Methods contains OPTIONS once the header is set in backend. So, no problem with curl this way.
< Access-Control-Allow-Methods: POST
< Access-Control-Allow-Origin: *
< Date: Tue, 15 Jan 2019 19:34:32 GMT
< Content-Length: 0

It also printed out fmt.Println(member.ID) and everything else from the JSON string just fine.

Edit 17 Jan 19 13.25pm : When I issue curl -i -X OPTIONS http://localhost:12345/anteroom , this comes back: "Access-Control-Allow-Methods: POST, OPTIONS". So OPTIONS is working. But front end request remains the same, no explicit 200 OK and JSON doesn't seem to go through even though console logs it as a success. The MIME type is listed as "text/plain" in response, but isn't that ok for JSON? As mentioned, other solutions use the same format and it works for them. I'm working on this part of the problem now.

I'm new to Golang, TypeScript, and Angular, so I hope I'm posting all the relevant code. It seems like a popular problem so I looked through quite a handful of similar questions on here but still can't figure it out. What on earth have I missed?

This is not how you do CORS on the backend. Your backend needs to listen to HTTP OPTIONS type requests and send the CORS headers there.

The control flow is roughly:

  1. You request something in the frontend
  2. The browser determines: uh-oh, this requires CORS
  3. The browser first performs an OPTIONS request to the requested resources to negotiate CORS
  4. Depending on the result, either browser throws your frontend an error
  5. Or it continues with the actual request your frontend issued.
  6. Things work almost normally from here on out
  7. When the browser gets the result of your actual API call back it filters out things that have not been whitelisted/negotiated in steps 3 and 4.
  8. It presents that potentially censored result to the frontend as the result of the HTTP call.

@user268396 had told you why, and I will tell you how.

" Access-Control-Allow-Origin " means which origin you allowed to make request to this server.

It can be in forms like:

" Access-Control-Allow-Methods " should be []string("POST", "OPTION") when you want to post data from CORS.

When it come to golang, see godoc https://golang.org/pkg/net/http/#Header

type Header map[string][]string

And I always suggest to use Header.Add() instead of Header.Set() unless you know exactly what you are doing. Because each value in header always be []string.

So it should be

    res.Header().Add("Access-Control-Allow-Origin", "*")
    res.Header().Add("Access-Control-Allow-Methods", "POST")
    res.Header().Add("Access-Control-Allow-Methods", "OPTION")
    res.Header().Add("Content-Type", "application/json")

Problem solved thanks in large part to everyone who helped me realise I wasn't doing CORS properly. Basically, there were two problems: 1. I didn't know to set headers properly in the back end, and 2. I didn't format my JSON data properly.

I noticed that this is a fairly popular problem on Stack Overflow, possibly in part because some of us don't really understand how requests work. So here is a summary, drawn from Mozilla's invaluable and simply written documentation and user268396's input. I'm new to this so please correct me if I'm wrong.

Usually, it's straightforward: when you want to request something, the browser sets the appropriate headers based on whatever content you already have, and sends the request out. Server responds.

But, assuming you have an API POST endpoint on your back end and a front end on the same domain (I suppose that's most of us in development, though maybe not in production), and you're working with anything other than application/x-www-form-urlencoded, multipart/form-data, and text/plain, then it's a bit different. In my case, I'm working with application/json. So, it looks like this:

  1. Browser sends request with the OPTIONS method asking for what methods and content-types and other things that are allowed. This is called a preflight request. It doesn't send my JSON object yet.

  2. Server responds to the preflight request.

For the server to respond with the allowed stuff, we need to properly set the back end to allow the OPTIONS method in Access-Control-Allow-Methods, and if like me you're using Gorilla mux, remember to allow it in router.HandleFunc() too.

Because our front and back ends are on the same domain ( http://localhost ), the easiest thing to do is to set Access-Control-Allow-Origin to "*". However, we probably don't want to do this in production depending on our needs because * means all the world and beyond gets to send in requests.

Because I want to receive JSON, I also set Content-Type in the back end to application/json. This was the other half of my problem. Long story short, no matter what worked for other people, for some reason I still had to apply JSON.stringify() to my raw JSON data. Then it worked perfectly.

  1. Once the browser receives the allowed OPTIONS back, it filters out the stuff that's not allowed, and sends back a request with only the appropriate data. In my case, this would be the JSON object.

And that's it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM