Javascript fetch() is pinging my golang rest end point more than once?

Viewed 360

I have a bug that I don't understand is if I'm doing something wrong with Golang or if I'm doing something wrong with javascript fetch() statement. It's a simple situation where I want to make fetch() call to a golang endpoint and simply print hello world. But for some reason, the golang code is firing twice. Here's my code:

// main.go

package main

import (

  "route/page"
  "github.com/gorilla/mux"
  "log"
  "net/http"
)

func main() {

    router := mux.NewRouter()
    router.HandleFunc("/page", page.Search).Methods("GET","OPTIONS")
    log.Fatal(http.ListenAndServe(":8000", router))

}


//route/page.go

package page
import (
  "net/http"
  "log"
)

func Search(w http.ResponseWriter, r *http.Request) {

  w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Authorization, Organization")
  w.Header().Set("Access-Control-Allow-Origin", "*")
  w.Header().Set("Content-Type", "application/json")

  log.Println(r.Header.Get("authorization"))

  log.Println("Hello World")
  w.Write([]byte("{}"))
  return
}

I then went to JSFiddle.net and pasted in the following JS code and pressed Run:

fetch('http://192.168.0.21:8000/page', {
  method: 'GET',
  headers: {
    'Content-Type': 'text/plain',
    'Organization': 'afae3@@@@@%2Fajfoij',
    'Authorization': 'Basic tajoie#$@asdfall%DF;++JI%2F3255',
  }
})

And for some reason my console prints Hello World twice like this:

john@ubuntu:~/go/src/myproject$ go run main.go
2018/09/20 18:42:29
2018/09/20 18:42:29 Hello World
2018/09/20 18:42:29 Basic tajoie#$@asdfall%DF;++JI%2F3255
2018/09/20 18:42:29 Hello World

I transcribed my JS code into PHP, put my PHP code on the same localhost as my golang code, ran the PHP code from console, and got the expected result of the golang code executing only ONCE. Does anyone know what's going on? Did I mis-understand something about fetch()? Is there something unusual going on with the way I set the headers?

3 Answers
router.HandleFunc("/page", page.Search).Methods("GET","OPTIONS")

Tells the router to direct both GET and OPTIONS method to the Search handler.

CORS preflight is usually handled by a separate handler. In your case, you're telling Go to handle both the pre-flight check and the get request with Search handler.

Typically you would want to handle the pre-flight check separately from the follow on request, so the request isn't executing twice (once for no reason.)

An extremely simple version of a pre-flight handler would be:

func PreFlightHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "http://localhost")
    w.Header().Set("Vary", "Origin")
    w.Header().Set("Vary", "Access-Control-Request-Method")
    w.Header().Set("Vary", "Access-Control-Request-Headers")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept")
    w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
    w.Header().Set("Access-Control-Allow-Credentials", "true")
}

Full Golang code would be:

// main.go

package main

import (

  "route/page"
  "github.com/gorilla/mux"
  "log"
  "net/http"
)

func main() {

    router := mux.NewRouter()
    router.HandleFunc("/page", page.PreFlightHandler).Methods("OPTIONS")

    router.HandleFunc("/page", page.Search).Methods("GET")

    log.Fatal(http.ListenAndServe(":8000", router))

}

// route/page.go
package page
import (
  "net/http"
  "log"
)

func PreFlightHandler(w http.ResponseWriter, r *http.Request) {

    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Vary", "Origin")
    w.Header().Set("Vary", "Access-Control-Request-Method")
    w.Header().Set("Vary", "Access-Control-Request-Headers")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Origin, Accept, Authorization, Organization")
    w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
    w.Header().Set("Access-Control-Allow-Credentials", "true")
}
func Search(w http.ResponseWriter, r *http.Request) {

  log.Println(r.Header.Get("authorization"))

  log.Println("Hello World")
  w.Write([]byte("{}"))
  return
}

This is an extension of spencedev's answer

As user rmn suggested, this article differentiates between the concept of a "Simple Request" and a "non-Simple Request" from the perspective of CORS:

https://codeburst.io/cors-story-of-requesting-twice-85219da7172d

The presence of Authorization and Organization in my header makes my HTTP Request a "non-Simple Request" from CORS perspective, and causes fetch() to trigger both a GET and a OPTION request, with the OPTION request being known as a CORS Pre-flight Request. As spencedev pointed out, my golang code

router.HandleFunc("/page", page.Search).Methods("GET","OPTIONS")

says that page.Search should handle both the GET and OPTIONS request.

As sberry pointed out, the Gorilla library has a package for dealing with CORS. An alternative to spencedev's answer using Gorilla CORS handler is below

// main.go
package main

import (

  "route/page"
  "github.com/gorilla/mux"
  "github.com/gorilla/handlers"
  "log"
  "net/http"
)

func main() {

    router := mux.NewRouter()

    router.HandleFunc("/page", page.Search).Methods("GET")

    headersOk := handlers.AllowedHeaders([]string{"Content-Type","Authorization","Organization"})
    originsOk := handlers.AllowedOrigins([]string{"*"})
    methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "OPTIONS"})
    log.Fatal(http.ListenAndServe(":8000", handlers.CORS(originsOk, headersOk, methodsOk)(router)))

}

// route/page.go
package page
import (
  "net/http"
  "log"
)


func Search(w http.ResponseWriter, r *http.Request) {

  log.Println(r.Header.Get("authorization"))

  log.Println("Hello World")
  w.Write([]byte("{}"))
  return
}

To enable CORS for all API, You need to handle preflight request.

import (
  "log"
  "net/http"
  "github.com/gorilla/mux"
)

func main() {
  router := mux.NewRouter()

  // Handle all preflight request for CORS
  router.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Methods", "*")
    w.Header().Set("Access-Control-Allow-Headers", "*")
    w.WriteHeader(http.StatusNoContent)
    return
  })

  // Your route handlers goes right here
  router.HandleFunc("/page", page.Search).Methods("GET")

  log.Fatal(http.ListenAndServe(":3000", router))
}
Related