Request Rewrite in Gin

Let’s say you want to rewrite an incoming request to a gin server because maybe because you want to transparently handle a number of equivalent prefixes but only want to define one set of canonical routes. It’s a common enough problem, Apache installs typically have the mod_rewrite module installed by default. So we know what we want to accomplish, how do we do it with gin?

Redirection Methods

Easy option is to serve a redirect

func (c *gin.Context) {
  c.Redirect(http.StatusMovedPermanently, "/canonical/path")
}

That might be all you need and is pretty easy to do. However, you may by required to handle those optional paths as if they were correct. No redirect response, just handle the route as if it was made using another request path. For that you can use the HandleContext function in *gin.Engine

func rewritePath(root *gin.Engine) gin.HandlerFunc { 
  return func (c *gin.Context) {
    c.Request.URL.Path = "/canonical/path"
    root.HandleContext(c)
  }
}

The documentation warns that you can busy loop yourself to death if you manage to rewrite the path to match the same handler again, so be careful with your rewrite rules.

Response behavior

There is one thing I left out of the example above. If you used the code above as is, it will work but it may write the response buffer twice, essentially concatenating the response body with a copy of itself. This can happen if the response is big enough that the HTTP server decides to chunk the response.

What’s going on is that there are two opportunities for the response buffer to be written to when using the HandleContext method, caller’s view of the context and callee’s view of the context. HandleContext basically starts the routing process over from the beginning, which means all the same HandlerChain operations in the router happen again, that includes writing the response to the ResponseWriter. HandleContext doesn’t cancel the callers HandlerChain, so after HandleContext returns it proceeds with its own chain. That, in turn, will use the same context object as the callee, which will then write the same response from that context if given the chance. For small responses, the response may be flushed immediately upon being written by the callee, preventing the caller from writing to the response buffer as well. On larger responses, especially chunked ones, the caller chain has an opportunity to write to the response stream before it is closed. How do we prevent this behavior? We abort the caller request chain using c.Abort().

func rewritePath(root *gin.Engine) gin.HandlerFunc { 
  return func (c *gin.Context) {
    c.Request.URL.Path = "/canonical/path"
    root.HandleContext(c)
    c.Abort()
  }
}

This tells the caller chain to just stop here and do not proceed with actions after the current handler. That includes writing the response. We assume that the callee context has written all the response that we needed.

That’s all that you need to handle rewrites with the gin web framework. There aren’t really any limitations on how you can rewrite the path, just make sure to not catch yourself in an infinite loop and Abort your context if you don’t want it to attempt to write to the response buffer multiple times.

Extended Example

Here is a complete example of a rewrite that removes a prefix from a route before returning a response.

package main

import (
  "net/http"
  "github.com/gin-gonic/gin"
)

func main() {
  engine := gin.Default()
  engine.GET("/echo/:value", getValue)
  engine.GET("/extra-prefix/*rest", rewritePath(engine))
}

func getValue (c *gin.Context) {
  c.JSON(http.StatusOK, gin.H{"echo": c.Param("value")})
}

func rewritePath(e *gin.Engine) {
  return func (c *gin.Context) {
    c.Request.URL.Path = "/" + c.Param("rest")
    root.HandleContext(c)
    c.Abort()
  }
}

last-modified: 2024-03-17 21:37 CDT