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:
- Add response to OPTIONS to endpoints:
OPTIONS /api/user/
andOPTIONS /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 sendOPTIONS, GET, POST
as a value for/api/user/
andOPTIONS, GET, PUT, DELETE
for/api/user/{id}/
;
-
- 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:
- 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. - 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")
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")
}
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
}
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")
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.
Top comments (1)
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.