gorilla / mux, rs / cors, [justinas / alice](https://github. I will show you how to make a server by combining com / justinas / alice). In particular, when I tried to divide the middleware to be applied according to the path, I was addicted to CORS, so I will focus on the part related to that. In the code to be posted, it is basically a way of writing that ignores errors, so please rewrite as appropriate. Also, note that ...
in the code simply means abbreviation and has no grammatical meaning, so be careful when copying and pasting.
tl;dr
Watch out for the ʻOPTIONS` method of the preflight request.
Some examples are shown, but since it is the basic content as described in each README, if you know it, skip to [Implementation main subject](#Implementation main subject).
gorilla/mux
Here is an example of a server using mux.
package main
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
type Character struct {
Name string `json:"name"`
}
func pilotFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Shinji"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func angelFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Adam"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func main() {
r := mux.NewRouter() //r is*mux.Router type
r.Methods("GET").Path("/pilot").HandlerFunc(pilotFunc) // r.to routes*mux.Add a Route.
r.Methods("GET").Path("/angel").HandlerFunc(angelFunc) // r.routes is[]*mux.Since it is a Route type, you can add more and more.
http.ListenAndServe(":8000", r)
}
With this, a little server is completed.
$ go run main.go
$ curl http://localhost:8000/pilot
{"name": "Shinji"}
However, when I use fetch
from a browser, I get the following error:
Access to fetch at 'http://localhost:8000/user' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
rs/cors
That's where rs / cors comes in. For CORS, the MDN page (https://developer.mozilla.org/ja/docs/Web/HTTP/CORS) is easy to understand. In order to support CORS, it is necessary to set an appropriate value in the response header on the server side. Rs / cors is responsible for that. This is the previous example with rs / cors added.
import (
...
"github.com/rs/cors"
)
func main() {
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").HandlerFunc(pilotFunc)
r.Methods("GET").Path("/angel").HandlerFunc(angelFunc)
c := cors.Default().Handler(r) //Added.
http.ListenAndServe(":8000", c) //Changed from r to c.
}
You can support CORS just by adding one line.
justinas/alice
Furthermore, justinas / alice is useful when you want to add middleware that outputs the contents of the request to the log each time, or middleware that acquires the value from the request header before performing the main processing. (: //github.com/justinas/alice). Here is an example of creating and adding middleware that outputs request information to a log.
import (
...
"log"
"github.com/justinas/alice"
)
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Method: %v; URL: %v; Protocol: %v", r.Method, r.URL, r.Proto)
h.ServeHTTP(w, r)
})
}
func main() {
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").HandlerFunc(pilotFunc)
r.Methods("GET").Path("/angel").HandlerFunc(angelFunc)
c := cors.Default()
chain := alice.New(c.Handler, logHandler).Then(r) //Added.
http.ListenAndServe(":8000", chain) //Changed from c to chain.
}
By the way, gorilla / mux, rs / cors, [justinas / alice](https: // I will repost the combination of all combinations (github.com/justinas/alice) (definition of packge, import, struct is omitted).
...
func pilotFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Shinji"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func angelFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Adam"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Method: %v; URL: %v; Protocol: %v", r.Method, r.URL, r.Proto)
h.ServeHTTP(w, r)
})
}
func main() {
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").HandlerFunc(pilotFunc)
r.Methods("GET").Path("/angel").HandlerFunc(angelFunc)
c := cors.Default()
chain := alice.New(c.Handler, logHandler).Then(r)
http.ListenAndServe(":8000", chain)
}
However, with this method, the set middleware will be applied to all requests.
For example, / pilot
checks if you are logged in and returns data only if you are logged in, but / angel
cannot respond to requests that return data even if you are not logged in. With Qiita, you can see the article page even if you are not logged in, but imagine a situation where you cannot see the draft of My Page unless you are logged in.
Therefore, try the following. As I said earlier, I'm addicted to the writing style below (laughs).
type funcHandler struct {
handler func(w http.ResponseWriter, r *http.Request)
}
func (h funcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func pilotFunc(w http.ResponseWriter, r *http.Request) {...}
func angelFunc(w http.ResponseWriter, r *http.Request) {...}
func logHandler(h http.Handler) http.Handler {...}
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Authentication")
h.ServeHTTP(w, r)
})
}
func main() {
c := cors.Default()
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
Now, only when fetch
is done to / pilot
, ʻAuthentication` will be output to the standard output. Now it is possible to separate the middleware to be applied according to the path.
$go run main.go
2020/09/29 20:41:18 Method: GET; URL: /pilot; Protocol: HTTP/1.1
2020/09/29 20:41:18 Method: GET; URL: /pilot; Protocol: HTTP/1.1; Authentication
2020/09/29 20:41:18 Method: GET; URL: /angel; Protocol: HTTP/1.1
I'll leave an explanation of why this is addictive, and briefly explain the mechanism by which it can be rewritten in this way. If you want to see the reason why you are addicted first, please skip to [Reason for addiction](# Reason for addiction).
First of all, I would like to confirm what http.ListenAndServer
is doing, but [Go] How to read the net / http package and execute http.HandleFunc / items / 1d1c64d05f7e72e31a98) is quite detailed, so I will throw it here. It seems that the information is a little old, and the line number written does not match the actual line number, or the writing style is slightly different, but I think that there is no problem in basic understanding.
In conclusion, http.ListenAndServe
passes the received request to ServeHTTP
in the second argument (this time mux.Router
). Therefore, the second argument must implement ServeHTTP
. ServeHTTP
is implemented in mux.Router
, and in the http
package,
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Any http.Handler
type variable can be the second argument to ListenAndServe
. Within this ServeHTTP
, call the ServeHTTP
of the other http.Hanlder
, and then within that ServeHTTP
, call the ServeHTTP
of the other http.Hanlder
, and so on. By chaining .Handler`, you can process requests in order.
In other words, you can chain handlers by using a function that takes http.Handler
as an argument and returns a http.Handler
type, ** or middleware **. As an example, consider the chain of logHandler
and ʻauthHandler`.
chain := logHandler(authHandler(router))
Such a chain can be considered. However, with this method, it becomes more difficult to understand as the number of middleware increases, so using alice,
chain := alice.New(logHandler, authHandler).Then(router)
Is written briefly.
Here, let's compare before and after rewriting so that the middleware applied can be changed depending on the path.
//Change before
func main() {
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").HandlerFunc(pilotFunc)
r.Methods("GET").Path("/angel").HandlerFunc(angelFunc)
c := cors.Default()
chain := alice.New(c.Handler, logHandler).Then(r)
http.ListenAndServe(":8000", chain)
}
//After change
func main() {
c := cors.Default()
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
Therefore, the flow that the request before and after the change follows is
Previous: request -> CORS -> logging -> routing -> pilotFunc or angelFunc
rear: request -> routing -> CORS -> logging (-> auth) -> pilotFunc or angelFunc
You can see that the routing order has changed. This is one of the reasons why I am addicted to it.
Next, I'll explain why I'm addicted to CORS, which is the main subject of this article.
First, I will describe an implementation of the authentication mechanism (details will be described later) that is a little closer to the actual one.
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Token")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/justinas/alice"
"github.com/rs/cors"
)
type Character struct {
Name string `json:"name"`
}
type funcHandler struct {
handler func(w http.ResponseWriter, r *http.Request)
}
func (h funcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func pilotFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Shinji"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func angelFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Adam"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Method: %v; URL: %v; Protocol: %v", r.Method, r.URL, r.Proto)
h.ServeHTTP(w, r)
})
}
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Token")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
func main() {
c := cors.Default()
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
In this state
fetch(url, {
headers: new Headers({
Token: "abcd"
})
})
When you execute ...
Access to fetch at 'http://localhost:8000/pilot' from origin 'null' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
** An error will occur. ** **
To understand why you're addicted
It is necessary to understand the three points of. I will explain in order.
When authenticating, whether using cookies or Tokens, it is necessary to put it in the request header and pass it from the front end to the back end. Therefore, if you reimplement ʻauthHandler` used in the example to something more realistic,
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Token")
if token == "" {
...
}
...
h.ServeHTTP(w, r)
})
}
It will be something like that. The important thing here is that you need to add a new ** header name ** that identifies it as a cookie or Token.
Actually, CORS has a limitation, and you can make a "simple request" only when the values that can be set in the method and header meet the following conditions. Here, a simple request is a general request.
method : GET, POST, HEAD header : Accept, Accept-Language, Content-Language, Content-Type(application/x-www-form-urlencoded, multipart/form-data, text/plain), DPR, Downlink, Save-Data, Viewport-Width, Width
Therefore, if you set your own values such as Cookie
or Token
in the ** header, you cannot make a simple request **. In this case, make a request called ** Preflight Request ** in advance. See MDN for more information. If you write only the necessary information, the request will be sent as a preflight request with the ʻOPTIONSmethod. Only if a normal response is returned to this preflight request, we will continue to send a
GET or
POSTrequest with a value such as
Tokenin the header. Therefore, when using
fetch`, the request is sent twice behind the scenes.
In summary, if you use your own Header value, a preflight request will be sent. Here, I will repost the flow before and after changing so that multiple middleware can be applied.
Previous: request -> CORS -> logging -> routing -> pilotFunc or angelFunc
rear: request -> routing -> CORS -> logging (-> auth) -> pilotFunc or angelFunc
Looking at the CORS middleware Handler
,
func (c *Cors) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
...
w.WriteHeader(http.StatusNoContent)
} else {
...
h.ServeHTTP(w, r)
}
})
}
So, the process is branched depending on whether it is the Options method or not, and the preflight request is handled appropriately. Next, take a look at ServeHTTP
of mux.Router
.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
if r.Match(req, &match) {
handler = match.Handler
...
}
if handler == nil && match.MatchErr == ErrMethodMismatch {
handler = methodNotAllowedHandler()
}
if handler == nil {
handler = http.NotFoundHandler()
}
handler.ServeHTTP(w, req)
}
It looks for a handler
that matches the value of ʻurl` in the request, and if it doesn't, it branches between the method with an error and the routing with an error to handle the error. ..
Therefore, before and after changing to be able to apply multiple middleware, the processing for preflight requests has changed as follows.
Previous: preflight request -> CORS check -> 204 No content
rear: preflight request -> routing check -> 405 Method Not Allowed
This is why we are getting CORS errors.
Once you know this, it's easy to deal with. For example, is it simplest to add ʻOPTINOSwhen routing
mux.Router`? Sorry for the later, but in rs / cors, when adding your own header, you need to specify what kind of header is allowed. See the README for details. Based on that, the implementation will be as follows.
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET", "OPTIONS").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET", "OPTIONS").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/justinas/alice"
"github.com/rs/cors"
)
type Character struct {
Name string `json:"name"`
}
type funcHandler struct {
handler func(w http.ResponseWriter, r *http.Request)
}
func (h funcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler(w, r)
}
func pilotFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Shinji"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func angelFunc(w http.ResponseWriter, r *http.Request) {
res, _ := json.Marshal(Character{"Adam"})
w.WriteHeader(http.StatusOK)
w.Write(res)
}
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Method: %v; URL: %v; Protocol: %v", r.Method, r.URL, r.Proto)
h.ServeHTTP(w, r)
})
}
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Token")
if token == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET", "OPTIONS").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET", "OPTIONS").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
Alternatively, you can use PathPrefix
to handle all ʻOPTIONS`. (I don't know if this method is good, I'm sorry.)
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("OPTIONS").PathPrefix("/").HandlerFunc(c.HandlerFunc)
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
http.ListenAndServe(":8000", r)
}
By the way, as the name suggests, Pathprefix
adds a common prefix to urls. For example
r.PathPrefix("/products/")
If set to, requests such as http: // localhost: 8000 / products / hoge
and http: // localhost: 8000 / products / fuga
will be applicable.
If you want to support CORS, make sure to support ʻOPTIONS`.
Thank you for reading the long text. When you understand it, it seems like it's a simple thing, but there is one more thing to do. Actually, I didn't write it from scratch, but changed what other people wrote, and I got hooked in the process. The code written by other people is
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
},
AllowedHeaders: []string{"*"},
AllowCredentials: false,
})
logChain := alice.New(c.Handler, logHandler)
authChain := logChain.Append(authHandler)
r := mux.NewRouter()
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
r.Methods("GET").Path("/angel").Handler(logChain.Then(funcHandler{angelFunc}))
r.PathPrefix("").Handler(logChain.Then(http.StripPrefix("/img", http.FileServer(http.Dir("./img"))))) //There was this.
http.ListenAndServe(":8000", r)
}
It was like that. To return the image, it is called r.PathPrefix (""). Handler (logChain.Then (http.StripPrefix ("/ img", http.FileServer (http.Dir ("./ img")))))
There was something. However, I didn't have a chance to return the image on my server, so I deleted this line. Then, suddenly an error started to occur in CORS and it was panic. In addition, moving this line to the beginning resulted in an error. However, it was difficult to identify the cause of the error.
I knew at the time of writing this article
CORS can fail with various errors, but for security reasons it is stipulated that the errors cannot be known from JavaScript. The code only tells you that an error has occurred. The only way to know exactly what went wrong is to look at the details in your browser console.
... apparently ... In other words, no matter what error occurs
Access to fetch at 'http://localhost:8000/pilot' from origin 'null' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Is displayed. This made debugging difficult. When I think about it now, I can't deny the feeling that I wasted my time thinking that I could guess what kind of error could occur by looking at the types of 4XX errors from the Network tab in chrome. is. In fact, when I removed the Pathprefix ("")
and when I brought it to the beginning, the error text displayed on the console was the same, but the status was 405 and 404, respectively.
So why is r.PathPrefix (""). Handler (logChain.Then (http.StripPrefix ("/ img", http.FileServer (http.Dir ("./ img")))))
added If so, did the error occur? The request ʻOPTIONS http: // localhost: 8000 / pilot`
r.Methods("GET").Path("/pilot").Handler(authChain.Then(funcHandler{pilotFunc}))
I get a Method Not Allowed error for
r.PathPrefix("").Handler(logChain.Then(http.StripPrefix("/img", http.FileServer(http.Dir("./img")))))
Will fit against. Actually, the path registered with gorilla / mux is converted to a regular expression, but if you register an empty string "" in PathPrefix
, the corresponding regular expression will be"^"
. Therefore, / pilot
will not be Not Found for the above routing and will proceed.
Also, if the method is ʻOPTIONS, rs / cors immediately returns 204 No Content, so the subsequent
http.StripPrefix ("/ img", http.FileServer (http.Dir ("./ img")) Requests that cause an error with))) `will also be passed. As a result, the routing you set up to return the image unintentionally avoided CORS errors.
There is an idea that the Go language is a combination of simple ones, but for gollira / mux, rs / cors, and justinas / alice this time, the number of files to read to understand the behavior was one or two. , It was easy to read the process in order. I don't think the explanations in this article have completely explained everything, so if you have any questions, please take a look at each implementation for yourself.
Recommended Posts