2

I would like to implement header-based versioning on go using gin. I'm thinking to do this on the router using a middleware function.

The client will call the same API URL, and the version will be on a custom HTTP header, like this:

To call version 1 GET /users/12345678 Accept-version: v1

To call version 2: GET /users/12345678 Accept-version: v2

So, the router can identify the header and call the specific version. Something like this:

            router := gin.Default()

            v1 := router.Group("/v1")
            v1.Use(VersionMiddleware())
            v1.GET("/user/:id", func(c *gin.Context) {
                c.String(http.StatusOK, "This is the v1 API")
            })

            v2 := router.Group("/v2")
            v2.Use(VersionMiddleware())
            v2.GET("/user/:id", func(c *gin.Context) {
                c.String(http.StatusOK, "This is the v2 API")
            })

func VersionMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        version := c.Request.Header.Get(configuration.GetConfigValue("VersionHeader"))

        // Construct the new URL path based on the version number
        path := fmt.Sprintf("/%s%s", version, c.Request.URL.Path)

        // Modify the request URL path to point to the new version-specific endpoint
        c.Request.URL.Path = path
        c.Next()
    }
}

juan
  • 358
  • 1
  • 4
  • 12
  • 2
    I don't see a question here. What sort of difficulties are you having? – larsks Mar 27 '23 at 23:15
  • Hi @larsks. I got 404 the error: [GIN-debug] GET /v1/user/:id --> github.com/cmd/functions/user/routers.(*UserRouter).InitRouter.func1.1 (5 handlers) [GIN-debug] GET /v2/user/:id --> github.com/cmd/functions/user/routers.(*UserRouter).InitRouter.func1.2 (5 handlers) [GIN] 2023/03/28 - 15:12:41 | 404 | 198.458µs | | GET "/user/7d8c6792-0abf-440c-b75d-6b8ba6c92a3c" – juan Mar 28 '23 at 15:16

1 Answers1

1

Please check the below code snippet. I used ReverseProxy to redirect to the given version. You need to validate given version carefully. Otherwise, it will cause a recursive call.

Note: I used two versions of /user GET (/v1/user and /v2/user).

Sample Code

package main

import (
 "net/http"
 "net/http/httputil"
 "regexp"

 "github.com/gin-gonic/gin"
)



func main() {
 router := gin.Default()
 router.Use(VersionMiddleware())


 v1 := router.Group("/v1")
 v1.GET("/user", func(c *gin.Context) {
  c.String(http.StatusOK, "This is the v1 API")
 })

 v2 := router.Group("/v2")
 v2.GET("/user", func(c *gin.Context) {
  c.String(http.StatusOK, "This is the v2 API")
 })

 router.Run(":8082")
}



func VersionMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
  
  // You need to check c.Request.URL.Path whether 
  // already have a version or not, If it has a valid
  // version, return.
  regEx, _ := regexp.Compile("/v[0-9]+")
  ver := regEx.MatchString(c.Request.URL.Path)
  if ver {
   return
  }

  version := c.Request.Header.Get("Accept-version")
  
  // You need to validate  given version by the user here.
  // If version is not a valid version, return error 
  // mentioning that given version is invalid.

  director := func(req *http.Request) {
    r := c.Request
    req.URL.Scheme = "http"
    req.URL.Host = r.Host
    req.URL.Path =  "/"+ version + r.URL.Path
    }
  proxy := &httputil.ReverseProxy{Director: director}
  proxy.ServeHTTP(c.Writer, c.Request)
 }
}

OR

You can use below wrapper implementation for gin.

  • Example

package main

import (
 "net/http"

 "github.com/gin-gonic/gin"
 "github.com/udayangaac/stackoverflow/golang/75860989/ginwrapper"
)



func main() {
  engine := gin.Default()
 router := ginwrapper.NewRouter(engine)

 defaultRouter := router.Default()
 defaultRouter.Get("/profile",func(ctx *gin.Context) {

 })

 v1 := router.WithVersion("/v1")
 v1.Get("/user",func(ctx *gin.Context) {
  ctx.String(http.StatusOK, "This is the profile v1 API")
 })

 v2 := router.WithVersion("/v2")
 v2.Get("/user",func(ctx *gin.Context) {
  ctx.String(http.StatusOK, "This is the profile v2 API")
 })

 
 engine.Run(":8082")
}
  • Wrapper
package ginwrapper

import (
 "fmt"
 "net/http"

 "github.com/gin-gonic/gin"
)

type Router struct {
 router *gin.Engine
 versionGroups map[string]*gin.RouterGroup
}

type VersionedRouter struct {
 version string
 Router
}

func NewRouter(router *gin.Engine) *Router {
 return &Router{
  router: router,
  versionGroups: make(map[string]*gin.RouterGroup),
 }
}

func (a *Router) Default() VersionedRouter {
 return VersionedRouter{Router: *a }
}

func  (a *Router) WithVersion(version string) VersionedRouter {
 if _,ok := a.versionGroups[version]; ok {
  panic("cannot initialize same version multiple times")
 }
 a.versionGroups[version] = a.router.Group(version)
 return VersionedRouter{Router: *a,version:version }
}




func (vr VersionedRouter) Get(relativePath string, handlers ...gin.HandlerFunc)  {
 vr.handle(http.MethodGet,relativePath,handlers...)
}

// Note: You need to follow the same for other HTTP Methods.
// As an example, we can write a method for Post HTTP Method as below,
// 
//  func (vr VersionedRouter) Post(relativePath string, handlers ...gin.HandlerFunc)  {
//   vr.handle(http.MethodPost,relativePath,handlers...)
//  }





func (vr VersionedRouter)handle(method,relativePath string, handlers ...gin.HandlerFunc)  {
 if !vr.isRouteExist(method,relativePath) {
  vr.router.Handle(method,relativePath,func(ctx *gin.Context) {
   version := ctx.Request.Header.Get("Accept-version")
   if len(version) == 0 {
    ctx.String(http.StatusBadRequest,"Accept-version header is empty")
   }
   ctx.Request.URL.Path = fmt.Sprintf("/%s%s", version, ctx.Request.URL.Path)
   vr.router.HandleContext(ctx)
  })
 }

 versionedRelativePath := vr.version + relativePath
 if !vr.isRouteExist(method,versionedRelativePath) {
  vr.router.Handle(method,versionedRelativePath,handlers... )
 }
}


func (a VersionedRouter) isRouteExist(method,relativePath string) bool {
 for _,route := range a.router.Routes() {
  if route.Method == method && relativePath == route.Path {
   return true
  } 
 }
 return false
}

Sample Requests

  • /v1/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v1'
  • /v2/user
curl --location 'localhost:8082/user' \
--header 'Accept-version: v2'
  • The problem is that this is a lambda function, so I think the reverse proxy solution is not a good fit in this case, right? – juan Mar 28 '23 at 21:48
  • Hi @juan, Thank you for your feedback. I updated the answer and that might help you to resolve your question. – Chamith Udayanga Mar 29 '23 at 16:57