Gin: Get matched route in context

Created on 27 Nov 2016  ·  24Comments  ·  Source: gin-gonic/gin

I have a use case where the list of exposed apis should be available as an api, where admin user can add some additional parameter for storing to DB.

What I essentially need is to get the matched route in context for processing in middleware or handler function.

router.GET("/get_user_details",func (c *gin.Context){c.JSON(200, gin.H{
           "matched_route":c.MatchedRoute(),
        })})

So it there some like c.MatchedRoute or some other way to achieve the result?

Most helpful comment

try with gin middleware

       r.Use(func(c *gin.Context) {
        url := c.Request.URL.String()
        for _, p := range c.Params {
            url = strings.Replace(url, p.Value, ":"+p.Key, 1)
        }
        c.Set("matched_path", url)
    })

then you can get matched_path from gin.Context

    func(c *gin.Context) {
            if path, exist := c.Get("matched_path"); exist {
           ...
        }
    }

All 24 comments

You could use c.Request.URL and extract the data you need from the URL

So c.Request.URL gives out the actual path that was called and not the path that gin (or httprouter) matched. For example for requests which have params, let's say /user/:id I'd like to be able to retrieve the raw string /user/:id is that possible ?

@Depado could you please give an example?

Hey there

Let's say I have this code :

package main

import (
    "net/http"

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

func mymiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // There ?
        c.Next()
    }
}

func main() {
    r := gin.New()
    r.GET("/user/:id", mymiddleware(), func(c *gin.Context) {
        // How can I get the litteral string "/user/:id" here ?
        c.JSON(http.StatusOK, gin.H{"message": "received request"})
    })
}

Is there a way I can retrieve inside the handler the litteral string /user/:id, the pattern that matched the request ? If I use c.Request.Path it will give me the full output of the path so things like /user/123456

Why won't you just keep it in a constant and so you'll have it as a variable. I mean, if the function runs, it means that it matched the pattern

Sure but that means there's no generic way of getting the matched pattern in the handler and possibly automate stuff. I'd like to add this as a prometheus label. What I came up with is a middleware that takes this pattern as an argument, making it redundant r.GET("/user/:id", p.Instrument("/user/:id"), func(c *gin.Context) {})

Again, I can't really understand your problem.

Routes are pre-defined, they're not dynamically created. Therefore, if a route is matched then it means that you know which router is being used.

I could see it being a problem which is necessary to solve on a larger scale. An easy solution would be to provide the matched route via Context.

Maybe you could create a pull request with this feature?

I'd love to if I had time ^^ I'll try to give that a look later.

Seems like this would require using node.getValue and node.path.

node.getValue should either return node.path and bind it to the context or something like that. Or maybe bind the node to the context...

I do it like this in my prometehus middleware:

func(c *gin.Context) {
        if c.Request.URL.String() == p.MetricsPath {
            c.Next()
            return
        }
        routes := p.parent.Routes()
        url := ""
        for _, r := range routes {
            if r.Handler == c.HandlerName() {
                url = r.Path
                break
            }
        }
.........
......

What's p.parent.Routes() ?

gin.Engine

On Thu, Oct 19, 2017, 16:50 Depado notifications@github.com wrote:

What's p.parent.Routes() ?


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/gin-gonic/gin/issues/748#issuecomment-337932638, or mute
the thread
https://github.com/notifications/unsubscribe-auth/ABF-FeXrALCzTR_Jj9W7xjkKHV0xwu0Yks5st2HRgaJpZM4K9F8w
.

I see ! I'll give that a try thanks ! 👍

That works fine @jonaz 👍
Although cycling other all your routes each time there is a request could be optimized so I used a map[string]string like this :

func (p *Prometheus) Instrument() gin.HandlerFunc {
    return func(c *gin.Context) {
        var path string
        start := time.Now()
        reqSz := computeApproximateRequestSize(c.Request)

        if c.Request.URL.String() == p.MetricsPath {
            c.Next()
            return
        }

        if in, ok := p.PathMap[c.HandlerName()]; ok {
            path = in
        } else {
            // We miss some routes so let's parse that again
            for _, ri := range p.Engine.Routes() {
                p.PathMap[ri.Handler] = ri.Path
            }
            if in, ok := p.PathMap[c.HandlerName()]; ok {
                path = in
            } // If we don't know the path here, then we'll never have it
        }

        c.Next()

        status := strconv.Itoa(c.Writer.Status())
        elapsed := float64(time.Since(start)) / float64(time.Second)
        resSz := float64(c.Writer.Size())

        p.reqDur.Observe(elapsed)
        p.reqCnt.WithLabelValues(status, c.Request.Method, c.HandlerName(), c.Request.Host, path).Inc()
        p.reqSz.Observe(float64(reqSz))
        p.resSz.Observe(resSz)
    }
}

This way I have a chance to initialize the map when r.Use(mymiddleware) is used, and if a route isn't found while responding to a request, I update the map. So only the first request can potentially be a bit slower.

i came across same problem, and if do it like you said, must guarantee every route match different handler.

I ended up re-creating a gin middleware for Prometheus there. I had to use a protected map (that includes a mutex).
And yes indeed you must use different handler for the different paths.

I also had done the same method(redundant function argument) to solve this last year.

try with gin middleware

       r.Use(func(c *gin.Context) {
        url := c.Request.URL.String()
        for _, p := range c.Params {
            url = strings.Replace(url, p.Value, ":"+p.Key, 1)
        }
        c.Set("matched_path", url)
    })

then you can get matched_path from gin.Context

    func(c *gin.Context) {
            if path, exist := c.Get("matched_path"); exist {
           ...
        }
    }

@tsirolnik the reason for somthing like this is for adding per-api metrics for instance. It's nice to be able to have metrics counts by rest path such as a dimensional metric for:

POST /users/:userid/stories/:storyid

The current alternative is that you just have a metric per user / storyid combo which isn't super useful.

The info needed is in the router and we currently have to either deep inspect into the router to figure that out (some patches on related), or would have to have the middleware know what all the route mappings are and understand that.

The Prometheus middleware is a very good example of this. https://github.com/Depado/ginprom/blob/master/prom.go

Just came across this and wanted to mention another way of achieving this is to declare the "metricName" whilst declaring the endpoint name. This is how we did it prior.

So our routes looked like this:

r.GET("/users/:userId", metrics("users"), getUserById)

Where the metrics middleware was capturing the latency and sending it to DataDog.

Is it available?

PR #1826 adds gin.Context.FullPath(). It's merged to master but not released yet.

When next version will be released?

If you're already using a route like router.GET("/:serviceEnv/*routePath", controllers.ProxyToAnotherServer),

then you can well access your matched route through *routePath no?

func ProxyToAnotherServer(c *gin.Context) {
    relativeRoutePath := c.Param("routePath")
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dre1080 picture dre1080  ·  25Comments

mytharcher picture mytharcher  ·  28Comments

miketonks picture miketonks  ·  25Comments

al3xandru picture al3xandru  ·  30Comments

mdsantosdev picture mdsantosdev  ·  30Comments