Skip to content

🎯 Fix URL Parameter Loss When Request Context is Replaced in Middleware After Routing #1

Description

@RoseMark45

📝 Description

When a middleware replaces the request using r.WithContext(newCtx) after routing has already matched a route (for example, inline middleware on a sub-router or route group), downstream handlers can intermittently lose access to URL parameters stored in Chi's routing context.

The issue occurs because the routing context (chi.Context) attached to the original request is not consistently preserved or propagated across the cloned request in certain middleware chains. As a result, calls to chi.URLParam(r, "param") or chi.RouteContext(r.Context()) return empty values or nil even though the route was successfully matched. This behavior is particularly prevalent when middleware performs request cloning after routing and before the handler executes, or when context pooling (sync.Pool) prematurely reclaims the routing context.

🎯 Acceptance Criteria

  • chi.URLParam(r, "param") must consistently return the correct route parameters in downstream handlers, even if intermediate middleware clones or replaces the request context using r.WithContext().
  • chi.RouteContext(r.Context()) must correctly resolve to the active routing context after request cloning.
  • The fix must support nested routers and sub-routers where inline middleware clones the request.
  • Prevent premature release of chi.Context back to the sync.Pool when cloned requests/contexts are still referencing it.
  • No performance regressions or additional heap allocations in the hot path of the router.

🛠️ Technical Specifications & Context

In RoseMark45/chi, routing context is managed via chi.Context and stored in the request context under the private RouteCtxKey.

Key Areas of Interest:

  1. context.go:
    • Inspect RouteContext(ctx context.Context) *Context. If a middleware creates a new context that does not inherit from the request's original context (or if the context chain is structured such that the key is shadowed), RouteContext will fail to retrieve the active routing context.
  2. mux.go:
    • Look at the ServeHTTP and routing dispatch logic where chi.Context is fetched from the sync.Pool and subsequently put back using defer mx.pool.Put(rctx).
    • If a middleware replaces the request context, the reference to the original context might be lost, or the lifecycle of the pooled context might be cut short, leading to intermittent nil-pointer dereferences or cleared parameters when the pool reclaims the object.

Proposed Solution Directions:

  • Ensure that if a middleware calls r.WithContext(newCtx), chi provides a robust way to retrieve the routing context, or ensure that chi's internal handlers can recover the routing context from the parent context chain.
  • If the user derives newCtx from r.Context(), verify that the pointer to chi.Context is not being mutated or reset prematurely by nested routing logic.
  • Add defensive checks in RouteContext() to handle cases where the context has been wrapped.

🧪 Verification & Testing

Create a test case in mux_test.go (or a new test file) that reproduces the issue:

func TestMiddlewareRxCloneURLParams(t *testing.T) {
	r := chi.NewRouter()

	r.Route("/users/{userID}", func(r chi.Router) {
		// Middleware that clones the request context
		r.Use(func(next http.Handler) http.Handler {
			return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
				// Simulate context replacement (e.g., adding a tracing ID or user session)
				ctx := context.WithValue(req.Context(), "customKey", "customValue")
				next.ServeHTTP(w, req.WithContext(ctx))
			})
		})

		r.Get("/profile", func(w http.ResponseWriter, req *http.Request) {
			userID := chi.URLParam(req, "userID")
			if userID != "123" {
				t.Errorf("expected userID to be '123', got '%s'", userID)
			}
			w.Write([]byte("ok"))
		})
	})

	ts := httptest.NewServer(r)
	defer ts.Close()

	res, err := http.Get(ts.URL + "/users/123/profile")
	if err != nil {
		t.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		t.Errorf("expected status OK, got %d", res.StatusCode)
	}
}

Run the tests using:

go test -v -run=TestMiddlewareRxCloneURLParams ./...

Opire Bounty


This repo is using Opire - what does it mean? 👇
💵 Everyone can add rewards for this issue commenting /reward 100 (replace 100 with the amount).
🕵️‍♂️ If someone starts working on this issue to earn the rewards, they can comment /try to let everyone know!
🙌 And when they open the PR, they can comment /claim #1 either in the PR description or in a PR's comment.

🪙 Also, everyone can tip any user commenting /tip 20 @RoseMark45 (replace 20 with the amount, and @RoseMark45 with the user to tip).

📖 If you want to learn more, check out our documentation.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions