From cb7f233b45f80f667f833aedf20e1c534af1c8ce Mon Sep 17 00:00:00 2001 From: Isaac Chung Date: Mon, 12 Jan 2026 13:51:22 -0800 Subject: [PATCH] fix: version path detection bug --- epoch/middleware.go | 34 +++++++++++++++++++++++++++++++++- epoch/middleware_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/epoch/middleware.go b/epoch/middleware.go index ada3d6f..dde9537 100644 --- a/epoch/middleware.go +++ b/epoch/middleware.go @@ -65,13 +65,45 @@ func (vm *VersionManager) GetVersion(c *gin.Context) (string, error) { // Second, check URL path matches := vm.versionRegex.FindStringSubmatch(c.Request.URL.Path) if len(matches) > 1 { - return matches[1], nil + potentialVersion := matches[1] + // Only return as version if it matches a known version (exact or partial match) + if vm.isKnownVersion(potentialVersion) { + return potentialVersion, nil + } } // No version found in any location return "", nil } +// isKnownVersion checks if the potential version matches any known version +// (either exactly or as a partial match like "v1" matching "v1.0.0") +func (vm *VersionManager) isKnownVersion(potentialVersion string) bool { + // Fast path: exact match + if vm.possibleVersions[potentialVersion] { + return true + } + + // Normalize: lowercase and remove 'v' prefix + normalized := strings.TrimPrefix(strings.ToLower(potentialVersion), "v") + + for version := range vm.possibleVersions { + normalizedKnown := strings.TrimPrefix(strings.ToLower(version), "v") + + // Exact match after normalization (handles "V1.0.0" == "v1.0.0") + if normalizedKnown == normalized { + return true + } + + // Partial match: "1" matches "1.0.0", "1.2" matches "1.2.3" + if strings.HasPrefix(normalizedKnown, normalized+".") { + return true + } + } + + return false +} + // VersionMiddleware handles version detection and context setting type VersionMiddleware struct { versionBundle *VersionBundle diff --git a/epoch/middleware_test.go b/epoch/middleware_test.go index 6ddd80b..5ec9b5c 100644 --- a/epoch/middleware_test.go +++ b/epoch/middleware_test.go @@ -758,6 +758,38 @@ var _ = Describe("Middleware", func() { }) Describe("Version Manager Edge Cases", func() { + It("should not treat numeric-prefixed path segments as versions", func() { + manager := NewVersionManager("X-API-Version", []string{"1.0.0", "2.0.0"}) + + cases := []string{ + "/accounts/123-test-org/repositories", + "/users/12345/profile", + "/orgs/99-team/repos", + } + + for _, path := range cases { + req := httptest.NewRequest("GET", path, nil) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = req + + version, err := manager.GetVersion(c) + Expect(err).NotTo(HaveOccurred()) + Expect(version).To(Equal(""), "path %s should not extract a version", path) + } + }) + + It("should recognize registered versions in path", func() { + manager := NewVersionManager("X-API-Version", []string{"1.0.0", "2.0.0"}) + req := httptest.NewRequest("GET", "/api/1.0.0/users", nil) + + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = req + + version, err := manager.GetVersion(c) + Expect(err).NotTo(HaveOccurred()) + Expect(version).To(Equal("1.0.0")) + }) + It("should handle case-insensitive headers", func() { manager := NewVersionManager("x-api-version", []string{"1.0.0", "2.0.0"}) req := httptest.NewRequest("GET", "/test", nil)