Skip to content

Match API #2786

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
pcfreak30 opened this issue May 29, 2025 · 12 comments
Open

Match API #2786

pcfreak30 opened this issue May 29, 2025 · 12 comments

Comments

@pcfreak30
Copy link

I would like to see something like this in echo. If the maintainers are ok with it, I can submit a PR. I am in need to know if a router has a route blindly, from the outside. I am using it as part of higher abstractions.

package echo

import (
	"net/http"
)

// Match finds a route for the given request without executing the handler or middleware.
// It returns true if a route is found, false otherwise.
// It returns an error if the request is invalid or other issues occur during the matching process.
func (e *Echo) Match(req *http.Request) (bool, error) {
	// Acquire a context from the pool.
	c := e.pool.Get().(*context)
	defer e.pool.Put(c) // Ensure the context is released back to the pool

	// Reset the context with the provided request and a nil ResponseWriter.
	// This allows Echo's internal logic to use the Host, URL, Method, etc.
	c.Reset(req, nil) // Reset with the actual request and nil response writer

	// Find the appropriate router based on the request's Host using the existing method.
	router := e.findRouter(req.Host)

	// Call the Find method on the selected router.
	// Use the existing GetPath function to get the clean path.
	router.Find(req.Method, GetPath(req), c)

	// Check if the context's handler was set by the Find method.
	// If c.handler is not nil, it means Find found a matching route and handler.
	return c.Handler() != nil, nil
}
@aldas
Copy link
Contributor

aldas commented May 29, 2025

This seems at little bit too specific. What are you doing with this logic? I would like to understand the use-case.

You can achieve same today with

func match(e *echo.Echo, req *http.Request) (bool, error) {
	router, ok := e.Routers()[req.Host]
	if !ok {
		router = e.Router()
	}
	c := e.AcquireContext()
	defer e.ReleaseContext(c)
	router.Find(req.Method, echo.GetPath(req), c)
	return c.Handler() != nil, nil
}

@pcfreak30
Copy link
Author

This seems at little bit too specific. What are you doing with this logic? I would like to understand the use-case.

You can achieve same today with

func match(e *echo.Echo, req *http.Request) (bool, error) {
router, ok := e.Routers()[req.Host]
if !ok {
router = e.Router()
}
c := e.AcquireContext()
defer e.ReleaseContext(c)
router.Find(req.Method, echo.GetPath(req), c)
return c.Handler() != nil, nil
}

I am not using echos host map. though its also not clear that echo supports host based routing based on prev issues and lack of docs.
I have some high layer code in a branch atm in my repos.

func (r *Router[HandlerFunc, MiddlewareFunc, Route]) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if r.rootRouter != r {
		http.Error(w, "Internal Server Error: ServeHTTP called on non-root router", http.StatusInternalServerError)
		return
	}

	host := req.Host
	var handlerRouter *Router[HandlerFunc, MiddlewareFunc, Route]

	if hostRouter, ok := r.hostRouters[host]; ok && hostRouter != nil {
		handlerRouter = hostRouter
	} else {
		handlerRouter = r.defaultRouter
	}

	if handlerRouter == nil {
		http.Error(w, "Not Found", http.StatusNotFound)
		return
	}

	if req.URL.Path == handlerRouter.jsonDocumentationPath || req.URL.Path == handlerRouter.yamlDocumentationPath {
		if handler, ok := handlerRouter.router.Router().(http.Handler); ok {
			handler.ServeHTTP(w, req)
			return
		}
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	// First try the host router if it exists
	if handlerRouter != r.defaultRouter {
		if handlerRouter.router.HasRoute(req.Method, req.URL.Path) {
			if handler, ok := handlerRouter.router.Router().(http.Handler); ok {
				handler.ServeHTTP(w, req)
				return
			}
		}
	}

	// Fall back to default router
	if r.defaultRouter.router.HasRoute(req.Method, req.URL.Path) {
		if handler, ok := r.defaultRouter.router.Router().(http.Handler); ok {
			handler.ServeHTTP(w, req)
			return
		}
	}

	http.Error(w, "Not Found", http.StatusNotFound)
}

Im basically needing to have a set of api routes globally available, which would be on a default router echo instance. And they get routed to if the host specific router doesn't match...

but I currently cant know if it does or not...

@aldas
Copy link
Contributor

aldas commented May 29, 2025

though its also not clear that echo supports host based routing based

your first example is using router := e.findRouter(req.Host) so you probably have found out that host based routing is there.

but

And they get routed to if the host specific router doesn't match...
fallbacking to default router when host router did not find the match - this feature is not there.

one way to achieve this requirement would be middleware that decides if current middleware chain should be executed or start executing default router chain.

package main

import (
	"errors"
	"github.com/labstack/echo/v4"
	"net/http"
)

func main() {
	e := echo.New()

	// first middleware in chain will decide if default router should be executed instead
	e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// path is empty when there is no route match (404/405 routes have path value)
			// when we are already executing "___default___" use next(c) not do get into infinite loop
			if c.Request().Host == "___default___" || c.Path() != "" {
				return next(c)
			}
			// set host to some value that will not match any defined "host" values
			c.Request().Host = "___default___"
			e.ServeHTTP(c.Response(), c.Request())
			return nil
		}
	})

	e.RouteNotFound("/", func(c echo.Context) error {
		return c.String(http.StatusNotFound, "not found")
	})
	e.GET("/test", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello from default host!\n")
	})

	h1 := e.Host("host1")
	h1.GET("/test", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello from host1!\n")
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Fatal(err)
	}
}

test with

curl -H "host:host1" http://localhost:8080/test
curl -H "host:host1" http://localhost:8080/testa
curl http://localhost:8080/test
curl http://localhost:8080/

@pcfreak30
Copy link
Author

Thanks though that would not really work easily in my design. need to have testing from the outside, not from within. im working with the example you gave though im also getting some odd outcomes.

@pcfreak30
Copy link
Author

Ive ended up at

func (r echoRouter) HasRoute(req *http.Request) bool {
	router := r.router.Router()
	c := r.router.AcquireContext()
	defer r.router.ReleaseContext(c)
	c.Reset(req, nil)

	router.Find(req.Method, echo.GetPath(req), c)

	handler := c.Handler()
	if handler == nil {
		return false
	}

	// Get the pointer values (uintptr) for comparison
	handlerPtr404 := reflect.ValueOf(echo.NotFoundHandler).Pointer()
	handlerPtr405 := reflect.ValueOf(echo.MethodNotAllowedHandler).Pointer()
	handlerPtr := reflect.ValueOf(handler).Pointer()

	// Compare the pointer values
	if handlerPtr == handlerPtr404 || handlerPtr == handlerPtr405 {
		return false
	}

	return true
}

I tried doing reflect.ValueOf().Equals() but for some reason it doesn't match?

@aldas
Copy link
Contributor

aldas commented May 29, 2025

use IDE debugger to check what is being compared.

@pcfreak30
Copy link
Author

use IDE debugger to check what is being compared.

I am, all the time...

Right now im running into issues with the wildcard catch all routes kind of interfering?

@aldas
Copy link
Contributor

aldas commented May 29, 2025

:) wildcard routes are meant to catch all.

@pcfreak30
Copy link
Author

:) wildcard routes are meant to catch all.

What im seeing though is they may be taking priority over more specific routes.

@aldas
Copy link
Contributor

aldas commented May 29, 2025

it is not impossible but highly unlikely, there are small gotchas here and there definitively. Without small example it is not possible comment more.

@pcfreak30
Copy link
Author

it is not impossible but highly unlikely, there are small gotchas here and there definitively. Without small example it is not possible comment more.

np, im stopping for the day, so ill be having to deal with my left over routing issues tomorrow. Thanks for the fast responses.

@pcfreak30
Copy link
Author

So my issue is that I have 2 isolated routers and one has /* catch all to serve static files and the other has catch all but more specific.

func (r echoRouter) HasRoute(req *http.Request) (bool, string) {
	router := r.router.Router()
	c := r.router.AcquireContext()
	defer r.router.ReleaseContext(c)
	c.Reset(req, nil)

	router.Find(req.Method, echo.GetPath(req), c)

	handler := c.Handler()
	if handler == nil {
		return false, ""
	}

	// Get the pointer values (uintptr) for comparison
	handlerPtr404 := reflect.ValueOf(echo.NotFoundHandler).Pointer()
	handlerPtr405 := reflect.ValueOf(echo.MethodNotAllowedHandler).Pointer()
	handlerPtr := reflect.ValueOf(handler).Pointer()

	// Compare the pointer values
	if handlerPtr == handlerPtr404 || handlerPtr == handlerPtr405 {
		return false, ""
	}

	return true, ""
}

this solves basic detection, but the rest requires checking if the path is a wildcard. ive landed on the route to pass an escape hatch to a library I have forked to use a servehttp callback so i have more explicit control on the routing logic.

Since the routers are unaware I have to decide who gets what routing request and that is tricky because I need some routes to accessable across vhosts,and the root, and some to only be the vhost when either might have a wildcard, and not all routes are registered in every router.

So it seems an abstraction to decide that above echo is needed. and since echos host support is a simple map of echo's.... it wouldn't solve that either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants