Repository: gorilla/mux
Pull request #761 (2024-05-31 16:47:31)

Feature router in request context
<!--
     For Work In Progress Pull Requests, please use the Draft PR feature,
     see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.

     For a timely review/response, please avoid force-pushing additional
     commits if your PR already received reviews or comments.

     Before submitting a Pull Request, please ensure that you have:
     - 📖 Read the Contributing guide: https://github.com/gorilla/.github/blob/main/CONTRIBUTING.md
     - 📖 Read the Code of Conduct: https://github.com/gorilla/.github/blob/main/CODE_OF_CONDUCT.md

     - Provide tests for your changes.
     - Use descriptive commit messages.
	 - Comment your code where appropriate.
	 - Squash your commits
     - Update any related documentation.

     - Add gorilla/pull-request-reviewers as a Reviewer
-->

## What type of PR is this? (check all applicable)

- [ ] Refactor
- [x] Feature
- [ ] Bug Fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Go Version Update
- [ ] Dependency Update

## Description
Named routes are great, and being able to retrieve the URL for a route based on its name is a feature I use a lot. However, when handlers reside in a different package from the router instance, this can become cumbersome. Therefore, it would be great if we could fetch the current router just like we can fetch the current route. This PR adds that functionality.

```go
router := mux.CurrentRouter(r)
```
By default, the router is added to the request context but can be omitted using the `omitRouterFromContext` option or by calling `router.OmitRouterFromContext(true)`.

## Related Tickets & Documents

<!--
For pull requests that relate or close an issue, please include them
below.  We like to follow [Github's guidance on linking issues to pull requests](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).

For example having the text: "closes #1234" would connect the current pull
request to issue 1234.  And when we merge the pull request, Github will
automatically close the issue.
-->

- Related Issue #
- Closes #

## Added/updated tests?

- [x] Yes
- [ ] No, and this is why: _please replace this line with details on why tests
      have not been included_
- [ ] I need help with writing tests

## Run verifications and test

- [x] `make verify` is passing
- [x] `make test` is passing


AlexVulaj: I really like this change - I foresee some good use cases in middleware or anything that handles multiple routes. Thanks for the contribution!
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/gorilla/mux/pull/761?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 80.91%. Comparing base [(`7d9a6f7`)](https://app.codecov.io/gh/gorilla/mux/commit/7d9a6f73b4eadcde69da1398a254ed4421de8285?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla) to head [(`2f46728`)](https://app.codecov.io/gh/gorilla/mux/commit/2f4672826a08ad8e7a28c9373b94037002bb73e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla).
> Report is 7 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #761      +/-   ##
==========================================
+ Coverage   79.58%   80.91%   +1.33%     
==========================================
  Files           5        5              
  Lines         911      938      +27     
==========================================
+ Hits          725      759      +34     
+ Misses        131      126       -5     
+ Partials       55       53       -2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/gorilla/mux/pull/761?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gorilla).


Patch:
diff --git a/mux.go b/mux.go
index c9396e9..eaff897 100644
--- a/mux.go
+++ b/mux.go
@@ -94,6 +94,9 @@ type routeConf struct {
 	// If true, the http.Request context will not contain the Route.
 	omitRouteFromContext bool
 
+	// if true, the the http.Request context will not contain the router
+	omitRouterFromContext bool
+
 	// Manager for the variables from host and path.
 	regexp routeRegexpGroup
 
@@ -207,6 +210,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 			} else {
 				req = requestWithRouteAndVars(req, match.Route, match.Vars)
 			}
+
+			if !r.omitRouterFromContext {
+				req = requestWithRouter(req, r)
+			}
 		}
 	}
 
@@ -279,6 +286,15 @@ func (r *Router) OmitRouteFromContext(value bool) *Router {
 	return r
 }
 
+// OmitRouterFromContext defines the behavior of omitting the Router from the
+// http.Request context.
+//
+// RouterFromRequest will yield nil with this option.
+func (r *Router) OmitRouterFromContext(value bool) *Router {
+	r.omitRouterFromContext = value
+	return r
+}
+
 // UseEncodedPath tells the router to match the encoded original path
 // to the routes.
 // For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to".
@@ -443,6 +459,7 @@ type contextKey int
 const (
 	varsKey contextKey = iota
 	routeKey
+	routerKey
 )
 
 // Vars returns the route variables for the current request, if any.
@@ -464,6 +481,13 @@ func CurrentRoute(r *http.Request) *Route {
 	return nil
 }
 
+func CurrentRouter(r *http.Request) *Router {
+	if rv := r.Context().Value(routerKey); rv != nil {
+		return rv.(*Router)
+	}
+	return nil
+}
+
 // requestWithVars adds the matched vars to the request ctx.
 // It shortcuts the operation when the vars are empty.
 func requestWithVars(r *http.Request, vars map[string]string) *http.Request {
@@ -486,6 +510,11 @@ func requestWithRouteAndVars(r *http.Request, route *Route, vars map[string]stri
 	return r.WithContext(ctx)
 }
 
+func requestWithRouter(r *http.Request, router *Router) *http.Request {
+	ctx := context.WithValue(r.Context(), routerKey, router)
+	return r.WithContext(ctx)
+}
+
 // ----------------------------------------------------------------------------
 // Helpers
 // ----------------------------------------------------------------------------


Test patch:
diff --git a/mux_test.go b/mux_test.go
index 0845d7f..bac758b 100644
--- a/mux_test.go
+++ b/mux_test.go
@@ -1768,6 +1768,73 @@ func TestPanicOnCapturingGroups(t *testing.T) {
 	NewRouter().NewRoute().Path("/{type:(promo|special)}/{promoId}.json")
 }
 
+func TestRouterInContext(t *testing.T) {
+	router := NewRouter()
+	router.HandleFunc("/r1", func(w http.ResponseWriter, r *http.Request) {
+		contextRouter := CurrentRouter(r)
+		if contextRouter == nil {
+			t.Fatal("Router not found in context")
+			return
+		}
+
+		route := contextRouter.Get("r2")
+		if route == nil {
+			t.Fatal("Route with name not found")
+			return
+		}
+
+		url, err := route.URL()
+		if err != nil {
+			t.Fatal("Error while getting url for r2: ", err)
+			return
+		}
+
+		_, err = w.Write([]byte(url.String()))
+		if err != nil {
+			t.Fatalf("Failed writing HTTP response: %v", err)
+		}
+	}).Name("r1")
+
+	noRouterMsg := []byte("no-router")
+	haveRouterMsg := []byte("have-router")
+	router.HandleFunc("/r2", func(w http.ResponseWriter, r *http.Request) {
+		var msg []byte
+
+		contextRouter := CurrentRouter(r)
+		if contextRouter == nil {
+			msg = noRouterMsg
+		} else {
+			msg = haveRouterMsg
+		}
+
+		_, err := w.Write(msg)
+		if err != nil {
+			t.Fatalf("Failed writing HTTP response: %v", err)
+		}
+	}).Name("r2")
+
+	t.Run("router in request context get route by name", func(t *testing.T) {
+		rw := NewRecorder()
+		req := newRequest("GET", "/r1")
+
+		router.ServeHTTP(rw, req)
+		if !bytes.Equal(rw.Body.Bytes(), []byte("/r2")) {
+			t.Fatalf("Expected output to be '/r1' but got '%s'", rw.Body.String())
+		}
+	})
+
+	t.Run("omit router from request context", func(t *testing.T) {
+		rw := NewRecorder()
+		req := newRequest("GET", "/r2")
+
+		router.OmitRouterFromContext(true)
+		router.ServeHTTP(rw, req)
+		if !bytes.Equal(rw.Body.Bytes(), noRouterMsg) {
+			t.Fatal("Router not omitted from context")
+		}
+	})
+}
+
 // ----------------------------------------------------------------------------
 // Helpers
 // ----------------------------------------------------------------------------