DEV Community

Cover image for Go HTTP Routing Conflicts: How to Fix Dynamic Route Swallowing
Omojola Tomiloba David
Omojola Tomiloba David

Posted on

Go HTTP Routing Conflicts: How to Fix Dynamic Route Swallowing

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)

Enter fullscreen mode Exit fullscreen mode

Everything looked correct.

Or so I thought.

Then something strange happened.

Requests like:

GET /businesses/services
Enter fullscreen mode Exit fullscreen mode

started behaving unexpectedly.

Instead of reaching:

business.Get("/services", handler.GetServices)
Enter fullscreen mode Exit fullscreen mode

the router interpreted:

services
Enter fullscreen mode Exit fullscreen mode

as:

:id
Enter fullscreen mode Exit fullscreen mode

meaning:

/businesses/services
Enter fullscreen mode Exit fullscreen mode

became:

/businesses/:id
Enter fullscreen mode Exit fullscreen mode

with:

id = "services"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

it creates a very flexible matching pattern.

This route says:

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

That means:

/businesses/123
Enter fullscreen mode Exit fullscreen mode

matches.

But unfortunately:

/businesses/services
Enter fullscreen mode Exit fullscreen mode

also matches.

And:

/businesses/products
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The router reads from top to bottom.

Request:

/businesses/services
Enter fullscreen mode Exit fullscreen mode

Router evaluation:

Step 1:

Does /:id match?
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Use:

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

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

business.Get("/:id", handler.GetBusiness)
Enter fullscreen mode Exit fullscreen mode

Now:

/businesses/services
Enter fullscreen mode Exit fullscreen mode

matches:

/services
Enter fullscreen mode Exit fullscreen mode

first.

Only requests that fail earlier matches reach:

/:id
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Bad:

/:id

/services

/products
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

tries to become everything.


Have you ever encountered routing bugs like this?

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

Top comments (0)