VOOZH about

URL: https://dev.to/tomiloba2/debugging-dynamic-route-swallowing-in-go-and-how-to-fix-it-4da6

⇱ Go HTTP Routing Conflicts: How to Fix Dynamic Route Swallowing - DEV Community


While working on a backend project recently, I ran into a bug that initially made no sense.

I had business routes that looked something like this:

business := router.Group("/businesses")

// Business Routes
business.Get("/:id", handler.GetBusiness)
// Services
business.Get("/services", handler.GetServices)
business.Get("/services/:id", handler.GetService)

// Products
business.Get("/products", handler.GetProducts)
business.Get("/products/:id", handler.GetProduct)

Everything looked correct.

Or so I thought.

Then something strange happened.

Requests like:

GET /businesses/services

started behaving unexpectedly.

Instead of reaching:

business.Get("/services", handler.GetServices)

the router interpreted:

services

as:

:id

meaning:

/businesses/services

became:

/businesses/:id

with:

id = "services"

At first, I thought something was wrong with my handlers.

The problem was actually route ordering.


Understanding Why This Happens

Routers generally evaluate routes in the order they are registered.

When the router sees:

business.Get("/:id", handler.GetBusiness)

it creates a very flexible matching pattern.

This route says:

"Accept anything after /businesses/ and treat it as an id."

That means:

/businesses/123

matches.

But unfortunately:

/businesses/services

also matches.

And:

/businesses/products

matches too.

Your dynamic route becomes too greedy.


Dynamic Routes Can Swallow Static Routes

Consider this ordering:

business.Get("/:id", handler.GetBusiness)

business.Get("/services", handler.GetServices)

business.Get("/products", handler.GetProducts)

The router reads from top to bottom.

Request:

/businesses/services

Router evaluation:

Step 1:

Does /:id match?

Yes.

Router stops searching.

Static route never executes.

This is sometimes called:

Dynamic Route Swallowing

because dynamic patterns capture requests intended for more specific routes.


The Fix

The simplest fix is:

Register specific routes before dynamic routes.

Instead of:

business.Get("/:id", handler.GetBusiness)

business.Get("/services", handler.GetServices)

Use:

business.Get("/services", handler.GetServices)

business.Get("/products", handler.GetProducts)

business.Get("/:id", handler.GetBusiness)

Now:

/businesses/services

matches:

/services

first.

Only requests that fail earlier matches reach:

/:id

Moving Forward

After debugging this, I learnt a simple rule:

Routes should move from most specific to least specific.

Good:

/services

/services/:id

/products

/products/:id

/:id

Bad:

/:id

/services

/products

Think about dynamic routes as catch-all nets.

The wider the net, the later you should place it.


Lessons Learned

Initially, I thought I had a handler bug.

What I actually had was:

A routing problem.

This bug taught me something important:

Backend bugs are not always business logic bugs. Sometimes the framework is behaving exactly as designed.

Now whenever I add dynamic routes, I immediately ask:

"Can this route accidentally capture requests that belong somewhere else?"

Because eventually:

/:id

tries to become everything.


Have you ever encountered routing bugs like this?

What backend issue took you longer to debug than you expected?