Skip to content

fix(gin): literal colon routes not working with engine.Handler()#4415

Merged
appleboy merged 7 commits intogin-gonic:masterfrom
pawannn:fix-literal-colon-routes
Nov 16, 2025
Merged

fix(gin): literal colon routes not working with engine.Handler()#4415
appleboy merged 7 commits intogin-gonic:masterfrom
pawannn:fix-literal-colon-routes

Conversation

@pawannn
Copy link
Copy Markdown
Contributor

@pawannn pawannn commented Oct 31, 2025

Problem

Literal colon routes (e.g., /api:v1) currently fail when using:

  • engine.Handler()
  • Direct http.Handler usage (e.g., &http.Server{Handler: engine})

They only work when using engine.Run()

Related

Reproduction

package main

import (
	"log"
	"net/http"

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

func main() {
	r := gin.Default()

	r.GET(`/api\:v1`, func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "literal colon works!"})
	})

	r.GET("/api/:version", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"version": c.Param("version")})
	})

	server := &http.Server{
		Addr:    ":8080",
		Handler: r,
	}

	log.Println("Server starting on :8080")
	log.Println("Test literal colon: curl http://localhost:8080/api:v1")
	log.Println("Test param route: curl http://localhost:8080/api/v2")

	if err := server.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}

Output Before Fix

Screenshot 2025-11-01 at 1 31 40 AM

Root Cause

The updateRouteTrees() method converts escaped paths (/api\:v1) to actual colon paths (/api:v1), but it's only called inside engine.Run(). Other usage patterns never trigger this conversion.

Fixes #4413

Solution

Add a sync.Once mechanism in ServeHTTP() to ensure updateRouteTrees() is called exactly once before processing the first request, regardless of how the engine is used.

Changes

Modified Files:

  • gin.go: Added routeTreesUpdated sync.Once field to Engine struct.
  • gin.go: Updated ServeHTTP() to call updateRouteTrees() once on first request.

Testing

  1. Literal colon routes with engine.Run()
  2. Literal colon routes with engine.Handler()
  3. Literal colon routes with direct ServeHTTP() usage
  4. Mixed routes (static, param, and literal colon) work together
  5. updateRouteTrees() is called only once across multiple requests

Output after the Fix:

Screenshot 2025-11-01 at 1 32 35 AM

Result: Request to /api:v1 now returns 200 OK with the correct response!

Breaking Changes

None. This is a pure bug fix with no API changes.

Pull Request Checklist

Please ensure your pull request meets the following requirements:

  • Open your pull request against the master branch.
  • All tests pass in available continuous integration systems (e.g., GitHub Actions).
  • Tests are added or modified as needed to cover code changes.

Thank you for contributing!

@appleboy appleboy added the type/bug Found something you weren't expecting? Report it here! label Nov 3, 2025
@appleboy appleboy added this to the v1.12 milestone Nov 3, 2025
@appleboy appleboy requested a review from Copilot November 3, 2025 04:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements lazy initialization of route trees using sync.Once to ensure the updateRouteTrees() function is called exactly once when routes are first accessed, rather than being called explicitly in every Run* method. This approach handles literal colons in route paths (escaped as \:) more consistently across different server startup patterns.

  • Added routeTreesUpdated sync.Once field to Engine struct to ensure single initialization
  • Modified ServeHTTP to call updateRouteTrees() lazily on first request using sync.Once
  • Added comprehensive tests covering literal colon handling in various scenarios

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
gin.go Added routeTreesUpdated sync.Once field and modified ServeHTTP to lazily initialize route trees on first request
literal_colon_test.go Added test suite covering literal colon functionality with different server initialization patterns and testing that route tree update happens only once
Comments suppressed due to low confidence (1)

gin.go:542

  • The Run() method still explicitly calls updateRouteTrees() before starting the server. With the new lazy initialization in ServeHTTP, this means updateRouteTrees() could be called twice: once here and once by the sync.Once in ServeHTTP. Since Run() calls engine.Handler() which returns the engine itself (when UseH2C is false), requests will trigger the sync.Once call. This creates inconsistent behavior where Run() eagerly updates but direct ServeHTTP usage updates lazily. Consider using engine.routeTreesUpdated.Do(func() { engine.updateRouteTrees() }) here instead.
	engine.updateRouteTrees()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gin.go Outdated
Comment on lines +97 to +99
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times
// concurrently. It helps prevent race conditions and redundant setup operations.
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states this prevents race conditions, but sync.Once alone doesn't prevent race conditions on the route trees themselves - it only ensures the function is called once. If routes can be added after the first ServeHTTP call, there could still be concurrent access issues. Consider clarifying that this ensures initialization happens once, and note any assumptions about route registration happening before serving requests.

Suggested change
// routeTreesUpdated ensures that the initialization or update of the route trees
// (used for routing HTTP requests) happens only once, even if called multiple times
// concurrently. It helps prevent race conditions and redundant setup operations.
// routeTreesUpdated ensures that initialization of the route trees (used for routing HTTP requests)
// happens only once, even if called multiple times concurrently. Note: sync.Once only ensures
// the initialization function is called once; it does not prevent race conditions on the route trees
// themselves if routes are added after the first ServeHTTP call. It is assumed that all route
// registration happens before serving requests to avoid concurrent access issues.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • sync.Once ensures updateRouteTrees() is called exactly once, not that it prevents all race conditions.
  • This implementation assumes routes are registered before serving begins, which is consistent with Gin's existing design. The engine.Run() method already calls updateRouteTrees() once with this same assumption - this PR simply extends that pattern to Handler() and direct http.Handler usage.
  • Adding routes dynamically after serving begins is not supported in Gin's current architecture (this is existing behavior, not introduced by this change).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the comments for routeTreesUpdated in Engine interface

@pawannn
Copy link
Copy Markdown
Contributor Author

pawannn commented Nov 3, 2025

Hi @appleboy , The golangci-lint was failing, I used "GET" instead of http.MethodGet in test cases, can you please re-run the checks.

@pawannn pawannn requested a review from Copilot November 3, 2025 04:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

gin.go:511

  • Corrected grammar: 'do update to' should be 'does update to' or 'updates'.
// updateRouteTrees do update to the route trees

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread literal_colon_test.go Outdated
assert.Equal(t, http.StatusOK, w.Code)
}

_ = originalUpdate
Copy link

Copilot AI Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This blank identifier assignment serves no purpose and suggests incomplete test implementation. Either remove this line or implement the intended verification logic for the test.

Suggested change
_ = originalUpdate

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a test case that checks whether updateRouteTrees is called only once.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the test case

@Zhang-Siyang
Copy link
Copy Markdown

Zhang-Siyang commented Nov 5, 2025

Hi @pawannn, thanks for your great work on this!

When I initially opened the issue, I was hoping it might lead to a discussion on potential optimizations, such as the placement of sync.Once in hot paths, but your current solution is also quite good.

I noticed some review comments and took the liberty of making a few small iterations on top of your commit to clean up the code and improve the tests. You can find my changes in my repository: Zhang-Siyang/gin@96b7c64...54a1967

Feel free to take a look and see if any of these changes are helpful for this PR. Looking forward to hearing from you!

@pawannn
Copy link
Copy Markdown
Contributor Author

pawannn commented Nov 6, 2025

Hey @appleboy, apologies for the formatting issue. I've run gofumpt and fixed the formatting in literal_colon_test.go. Could you please re-run the tests? Thanks!

Screenshot 2025-11-06 at 12 03 45 PM

@pawannn
Copy link
Copy Markdown
Contributor Author

pawannn commented Nov 6, 2025

Hey @Zhang-Siyang !
Thanks Buddy! I will check the changes in your branch and let you know.

@codecov
Copy link
Copy Markdown

codecov Bot commented Nov 15, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.83%. Comparing base (3dc1cd6) to head (e013e64).
⚠️ Report is 210 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4415      +/-   ##
==========================================
- Coverage   99.21%   98.83%   -0.38%     
==========================================
  Files          42       44       +2     
  Lines        3182     2924     -258     
==========================================
- Hits         3157     2890     -267     
- Misses         17       21       +4     
- Partials        8       13       +5     
Flag Coverage Δ
?
--ldflags="-checklinkname=0" -tags sonic 98.82% <100.00%> (?)
-tags go_json 98.75% <100.00%> (?)
-tags nomsgpack 98.81% <100.00%> (?)
go-1.18 ?
go-1.19 ?
go-1.20 ?
go-1.21 ?
go-1.24 98.83% <100.00%> (?)
go-1.25 98.83% <100.00%> (?)
macos-latest 98.83% <100.00%> (-0.38%) ⬇️
ubuntu-latest 98.83% <100.00%> (-0.38%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread literal_colon_test.go Outdated
@pawannn pawannn requested a review from appleboy November 15, 2025 15:41
@appleboy appleboy changed the title Fix: Literal colon routes not working with engine.Handler() and direct http.Handler usage fix(gin): literal colon routes not working with engine.Handler() and direct http.Handler usage Nov 16, 2025
@appleboy appleboy changed the title fix(gin): literal colon routes not working with engine.Handler() and direct http.Handler usage fix(gin): literal colon routes not working with engine.Handler() Nov 16, 2025
@appleboy appleboy merged commit 5fad976 into gin-gonic:master Nov 16, 2025
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/bug Found something you weren't expecting? Report it here!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Literal colon routes don't work properly in non-Run() scenarios

6 participants