DEV Community

Ushakov Michael
Ushakov Michael

Posted on • Updated on

No more troubles with CORS using Gorilla/Mux and Go

Introduction

Nowadays big backend solutions typically look like a set of microservices interacting with each other and with one or more frontend (typically browser) applications.
These services could be distributed across multiple virtual or physical machines and as a result every application (means server that hosts application) would get a different domain name / ip address.
A frontend application typically uses Javascript or Typescript to interact with the backend, but if a request goes to a different (from the frontend server) server a browser might assume that it poses a potential risk to security and block it. To solve this issue the backend API should provide headers with a valid origin (a domain name or ip where the frontend application is located). Today we gonna learn how to properly setup Gorilla/mux Web API to forever forget about CORS.

What we should provide from backend

Consider the following REST resource i.e. user:

  • GET /api/user/ - for getting all users
  • GET /api/user/{id}/ - for getting single user by id
  • POST /api/user/ - for creating new user
  • PUT /api/user/{id}/ - for update existing user
  • DELETE /api/user/{id}/ - for delete existing user

Basically, we have to implement the following things:

  1. Add response to OPTIONS to endpoints: OPTIONS /api/user/ and OPTIONS /api/user/{id}/ with the following headers:
    • Access-Control-Allow-Origin - domain name/ip or * for any origin;
    • Access-Control-Allow-Headers - here we could simply set *
    • Access-Control-Allow-Methods - we could simply list all methods, but i think it is better to send OPTIONS, GET, POST as a value for /api/user/ and OPTIONS, GET, PUT, DELETE for /api/user/{id}/;
  2. Add optional origin check on server side (if neccessary).

Today’s article is about the first one, you can find the second one github.com\gorilla\handlers. it It has a middleware that restricts access only to specific origins. Perhaps you noticed that your backend is not always required to respond to OPTIONS request because OPTIONS request is not being sent by browsers for simple requests. But in the above example we have to do this. When your api is quite big you have to constantly add preflight handlers (see example below) and every once in a while you could forget to do that especially when you have just finished designing some complicated endpoints. Using our (Wissance LLC) solution (open source github package) you can forget about adding preflight handlers because our package does this automatically. Please give us a star if you find our package useful for you (it’s very important to us).

How to make all these works easily

We implemented our own HandlerFunc that has a signature similar to mux.Router.HandlerFunc but with some minor differences:

  1. We are passing a pointer to mux.Router as a first parameter. We did this because we have a requirement to work with subrouters too, take a look at this unit test you can use a reference.
  2. We are passing handler methods parameters as a last variardic parameter instead of calling .Methods() when assigning route handler, we think this also makes things simpler.

Basically for auto preflight handling we need to create WebApiHandler instance and pass the required origin value to it as its second argument (Access-Control-Allow-Origin header supports only single value) and use our HandlerFunc instead of gorilla/mux, see example:

        handler := NewWebApiHandler(true, AnyOrigin)
    // Get only method
        baseUrl := "http://127.0.0.1:8998"
    configResource := baseUrl + "/api/config/"
    handler.HandleFunc(handler.Router, configResource, 
                           webApi.GetConfigHandler,"GET")
    // full crud
    functionResourceRoot := baseUrl + "/api/function/"
    handler.HandleFunc(handler.Router, functionResourceRoot, 
                           webApi.GetAllFunctions, "GET")
    handler.HandleFunc(handler.Router, functionResourceRoot, 
                           webApi.CreateFunction, "POST")
    functionResourceById := baseUrl + "/api/function/{id:[0-9]+}/"
    handler.HandleFunc(handler.Router, functionResourceById, 
                           webApi.GetFunctionById, "GET")
    handler.HandleFunc(handler.Router, functionResourceById, 
                           webApi.UpdateFunction, "PUT")
    handler.HandleFunc(handler.Router, functionResourceById, 
                           webApi.DeleteFunction, "DELETE")
Enter fullscreen mode Exit fullscreen mode

For the snippet above we have to add 3 additional preflight Handlers:

  • http://127.0.0.1:8998/api/config/;
  • http://127.0.0.1:8998/api/function/;
  • http://127.0.0.1:8998/api/function/{id};

Before we created our package we always were writing handlers like this:

        router.HandleFunc(functionResourceRoot, 
                          webApi.PreflightRoot).Methods("OPTIONS")
    router.HandleFunc(functionResourceById, 
                          webApi.PreflightByID).Methods("OPTIONS")

        func (webApi *WebApiContext) PreflightRoot(respWriter http.ResponseWriter, request *http.Request) {
            rest.EnableCors(&respWriter)
            respWriter.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
}

        func (webApi *WebApiContext) PreflightByID(respWriter http.ResponseWriter, request *http.Request) {
            rest.EnableCors(&respWriter)
            respWriter.Header().Set("Access-Control-Allow-Methods", "GET, PUT, DELETE, OPTIONS")
}
Enter fullscreen mode Exit fullscreen mode

We also should show what the webApi object is and how some of its request handler functions look:

type WebApiContext struct {
    Db     *gorm.DB // passing gorm to Context
    Config *config.AppConfig
        // other fields
}

func (webApi *WebApiContext) GetAllFunctions(respWriter http.ResponseWriter, request *http.Request) {
         // return here array of functions, body omitted
}

func (webApi *WebApiContext) GetFunctionById(respWriter http.ResponseWriter, request *http.Request) {
         // return function by id, body omitted
}
Enter fullscreen mode Exit fullscreen mode

Let's see how our code changed after we added the library:

        webApi := rest.WebApiContext{Db: appContext.ModelContext.Context, Config: appContext.Config, WebApiHandler: gr.NewWebApiHandler(true, gr.AnyOrigin)}

        webApi.WebApiHandler.Router.Use( keycloakAuthService.KeycloakAuthMiddleware)
    webApi.WebApiHandler.Router.Use(r.InspectorMiddleware)
    router := webApiContext.WebApiHandler.Router
    router.StrictSlash(true)

    // function resource
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/", webApi.GetAllFunctions, "GET")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/find/", webApi.FindFunctions, "GET").Queries("query", "{query}")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/name/", webApi.GetFunctionsNames, "GET")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/{id:[0-9]+}/", webApi.GetFunctionById, "GET")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/{id:[0-9]+}/body/", webApi.GetFunctionWithBodyById, "GET")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/", webApi.CreateFunction, "POST")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/{id:[0-9]+}/", webApi.UpdateFunction, "PUT")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/{id:[0-9]+}/", webApi.DeleteFunction, "DELETE")
    webApi.WebApiHandler.HandleFunc(router, baseUri+"/function/filter/", webApi.FilterFunctions, "GET")
Enter fullscreen mode Exit fullscreen mode

Conclusion

We made this package not because we have a lot of spare time on our hands, but because we were forced to do that because of a huge amount of CORS-related errors we were getting every time we worked on something big. Now that we have this solution CORS is no longer a problem.

Discussion (1)

Collapse
evillord666 profile image
Ushakov Michael Author

Recently a new release (v1.2.3) was published in which decorator for handler function was added for auto add AccessControlAllowOriginHeader header to any response therefore we could claim that we fully solved the issue with CORS.