Skip to content

Conversation

@pawannn
Copy link

@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
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 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
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).

@pawannn
Copy link
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
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.

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
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.

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

4 participants