Gin middleware examples
I've been playing around with Gin web framework
in Go for a while for small side projects and its been amazing so far. Gin attracted me by its
simplicity and compatibility with a default net/http
library and somewhat similarity with
Sinatra, which is a minimalistic web framework for Ruby.
So far i've written a few open source projects powered by Gin:
- pgweb - PostgreSQL web interface
- omxremote - GUI and API for Raspberry Pi's omxplayer
- envd - API to serve environment variables over HTTP
- hipache-api - HTTP API for Hipache
While most of those projects are pretty simple under the hood, i started to explore more on how to bring some of my experience with Sinatra into Go. In particular i was interested how to write middleware handlers. You can check Gin's documentation, there's a few small samples.
Here's the baseline app:
package main
import(
"github.com/gin-gonic/gin"
)
func GetDummyEndpoint(c *gin.Context) {
resp := map[string]string{"hello":"world"}
c.JSON(200, resp)
}
func main() {
api := gin.Default()
api.GET("/dummy", GetDummyEndpoint)
api.Run(":5000")
}
Now, lets add some middleware:
func DummyMiddleware(c *gin.Context) {
fmt.Println("Im a dummy!")
// Pass on to the next-in-chain
c.Next()
}
func main() {
// Insert this middleware definition before any routes
api.Use(DummyMiddleware)
// ... more code
}
In example above there's a call c.Next()
. It means that after our middleware is done
executing we can pass on request handler to the next func in the chain.
As you can see, middleware functions are no different from regular endpoint
functions as they only take one argument *gin.Context
. However, there's also another
way of defining middleware functions, like this one:
func DummyMiddleware() gin.HandlerFunc {
// Do some initialization logic here
// Foo()
return func(c *gin.Context) {
c.Next()
}
}
func main() {
// ...
api.Use(DummyMiddleware())
// ...
}
The difference between those two ways of defining middleware functions is that you
can do some initialization logic in later example. Say you need to fetch some data
from a third-party service, but you cannot do that on per-request basis. When middleware
gets loaded into request chain, whatever you define before the return
statement (Foo()
in example)
will be executed only once. This could be useful if you want to have condition
checking, like return one middleware function if one header is present, or another
one if its not. Lets move onto examples!
API authentication middleware
If you're building an API with Gin, you will probably want to add some sort of authentication
mechanism into your application. Easiest solution is to check if client has provided an
additional url parameter, like api_token
. Then it should be validated on each request
before anything else.
func respondWithError(c *gin.Context, code int, message interface{}) {
c.AbortWithStatusJSON(code, gin.H{"error": message})
}
func TokenAuthMiddleware() gin.HandlerFunc {
requiredToken := os.Getenv("API_TOKEN")
// We want to make sure the token is set, bail if not
if requiredToken == "" {
log.Fatal("Please set API_TOKEN environment variable")
}
return func(c *gin.Context) {
token := c.Request.FormValue("api_token")
if token == "" {
respondWithError(c, 401, "API token required")
return
}
if token != requiredToken {
respondWithError(c, 401, "Invalid API token")
return
}
c.Next()
}
}
Example above will check for presence of api_token
parameter on every request and
validate it against one defined as API_TOKEN
environment variable. The important
part is if you need to terminate request chain, you can call c.Abort
. This will
prevent any handlers in the chain to take place.
Code revision middleware
This type of middleware usually injects special headers into request response that
could provide some idea on which git commit your application is running. In Ruby world
that git sha is usually stored in REVISION
or COMMIT
file in the release directory,
created by capistrano or other deployment tools.
In fact, i created rack middleware just for that.
func RevisionMiddleware() gin.HandlerFunc {
// Revision file contents will be only loaded once per process
data, err := ioutil.ReadFile("REVISION")
// If we cant read file, just skip to the next request handler
// This is pretty much a NOOP middlware :)
if err != nil {
// Make sure to log error so it could be spotted
log.Println("revision middleware error:", err)
return func(c *gin.Context) {
c.Next()
}
}
// Clean up the value since it could contain line breaks
revision := strings.TrimSpace(string(data))
// Set out header value for each response
return func(c *gin.Context) {
c.Writer.Header().Set("X-Revision", revision)
c.Next()
}
}
As a result you'll get a new header in http response:
X-Revision: d4b371692d361869183d92d84caa5edb8835cf7d
Request ID middleware
Ofter API services inject a special header X-Request-Id
to response headers that could
be used to track incoming requests for monitoring/debugging purposes.
Value of request id header is usually formatted as UUID V4.
// ...
import github.com/satori/go.uuid
// ...
func RequestIdMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("X-Request-Id", uuid.NewV4().String())
c.Next()
}
}
After you make a request to your service, you'll see a new header in the response, similar to this one:
X-Request-Id: ea9ef5f9-107b-4a4e-9295-57d701d85a92