httprouter icon indicating copy to clipboard operation
httprouter copied to clipboard

httprouter 2.0 status and Routing priority over wildcards

Open solarfly73 opened this issue 8 years ago • 2 comments

Hi Julien,

I read the other issues that point to a 2.0 version where you intend to support routing priority such that more specific routes will take precedence over wildcards. Is this code in progress? Any way I can help out? If not, I have been working on the following:

  1. If a node has two or more children, and one is a wildcard, it receives the worst priority.
  2. Only one wildcard is allowed per node within the group of children.

Can you share your thoughts on the strategy you feel is necessary to implement this and keep the router fast?

solarfly73 avatar Jun 27 '17 23:06 solarfly73


package main

import (
	"fmt"
	"net/http"
	"strings"
)

// Node represents a trie node in the routing tree
type Node struct {
	segment     string          // Path segment (e.g., "users", "{id}", "*")
	isWildcard  bool            // True for wildcard nodes (e.g., "*")
	priority    int             // Higher priority for specific routes
	handler     http.Handler    // Handler for the node (if any)
	children    []*Node         // Child nodes
	method      string          // HTTP method (e.g., "GET")
}

// Router is a custom Chi-like router
type Router struct {
	trees map[string]*Node // Trees per HTTP method (e.g., "GET" -> root node)
}

// NewRouter creates a new router
func NewRouter() *Router {
	return &Router{
		trees: make(map[string]*Node),
	}
}

// Get registers a GET route
func (r *Router) Get(pattern string, handler http.Handler) {
	r.addRoute("GET", pattern, handler)
}

// addRoute adds a route to the trie
func (r *Router) addRoute(method, pattern string, handler http.Handler) {
	if pattern == "" || pattern[0] != '/' {
		panic("pattern must start with /")
	}

	// Get or create the method's tree
	root, exists := r.trees[method]
	if !exists {
		root = &Node{}
		r.trees[method] = root
	}

	// Split pattern into segments
	segments := strings.Split(strings.Trim(pattern, "/"), "/")
	current := root

	for _, segment := range segments {
		// Find or create child node
		var child *Node
		isWildcard := segment == "*" || strings.HasPrefix(segment, "{") && strings.Contains(segment, ":.*")

		// Check for existing child with same segment
		for _, c := range current.children {
			if c.segment == segment {
				child = c
				break
			}
		}

		// Enforce single wildcard per node
		if isWildcard {
			for _, c := range current.children {
				if c.isWildcard {
					panic(fmt.Sprintf("multiple wildcards not allowed at node %s", current.segment))
				}
			}
		}

		// Create new child if not found
		if child == nil {
			child = &Node{
				segment:    segment,
				isWildcard: isWildcard,
				priority:   1, // Default priority for specific routes
			}
			if isWildcard {
				child.priority = 0 // Wildcards get lowest priority
			}
			current.children = append(current.children, child)
		}

		current = child
	}

	// Set handler at leaf node
	if current.handler != nil {
		panic(fmt.Sprintf("handler already registered for %s %s", method, pattern))
	}
	current.handler = handler
	current.method = method

	// Sort children by priority (non-wildcards first)
	for i, child := range current.children {
		for j := i + 1; j < len(current.children); j++ {
			if current.children[j].priority > child.priority {
				current.children[i], current.children[j] = current.children[j], current.children[i]
			}
		}
	}
}

// ServeHTTP handles incoming requests
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	node := r.findNode(req.Method, req.URL.Path)
	if node != nil && node.handler != nil {
		node.handler.ServeHTTP(w, req)
		return
	}
	http.NotFound(w, req)
}

// findNode looks up a node for the given method and path
func (r *Router) findNode(method, path string) *Node {
	root, exists := r.trees[method]
	if !exists {
		return nil
	}

	segments := strings.Split(strings.Trim(path, "/"), "/")
	current := root

	for _, segment := range segments {
		var next *Node
		// Try matching non-wildcard children first (higher priority)
		for _, child := range current.children {
			if !child.isWildcard && child.segment == segment {
				next = child
				break
			}
		}
		// If no specific match, try wildcard
		if next == nil {
			for _, child := range current.children {
				if child.isWildcard {
					next = child
					break
				}
			}
		}
		if next == nil {
			return nil
		}
		current = next
	}

	return current
}

// hello creates a handler that outputs "Hello <text>"
func hello(text string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello %s\n", text)
	})
}

funct main() {
	r := NewRouter()

	// Specific routes
	r.Get("/users/{id}", hello("User ID"))
	r.Get("/users/profile", hello("Profile"))
	
	// Wildcard route
	r.Get("/users/*", hello("Wildcard"))

	// Start server
	fmt.Println("Server listening on :8080")
	if err := http.ListenAndServe(":8080", r); err != nil {
		fmt.Printf("Server error: %v\n", err)
	}
}

ljluestc avatar May 20 '25 01:05 ljluestc

这是来自QQ邮箱的假期自动回复邮件。   您好,我最近正在休假中,无法亲自回复您的邮件。我将在假期结束后,尽快给您回复。

letmestudy avatar May 20 '25 01:05 letmestudy