As some recall, I have already expressed my views of certain golang URL routers before, and
I have been interested in a new one lately, the router that is built into the Echo web framework.
Why Echo?
Echo is very interesting to me, as, well honestly, it has one of the fastest Web Router implementation out there, which
is awesome, and backed up by this fork of julienschmidt’s go http router benchmark suite.
Here is a shortened performance report of the top url routers using this performance test agains’t the huge github api:
As you can see Echo is a top contender for complex api routing, moreover it is much faster than the standard
library implementation, and has a smaller memory footprint (did you see the 0B alloc/op??) which makes it an awesome choice for a
small fast url router implementation.
Sounds Great, What is the problem?
Well, like anything else, you can’t have great performance without robbing from somewhere else. In Echo’s case
we have the following issues from what I can see:
No Regular Expression Matching on URL parameters
Required to use the Echo Context Scheme to get url parameters
Reports back 404 when 405 is more correct
No capability for Requesting of the Web Service what it is capable of using OPTIONS
Basically, a lot of the issues I have laid out in my prior blog on url routers. But, I decided to see if I couldn’t help
correct some of these issues, as I really like the potential within this implementation.
How does Echo’s URL Router Work?
Echo has followed a good design on how to handle routing from a high level. The router is created as a radix tree datastructure allowing
for efficient string searching capabilities. This implementation currently contains a radix tree per HTTP method as seen here.
When a route is searched on, the first thing that is done in Router.Find, is isolating the correct method radix tree to use:
As you can see in the above snip-it, We are finding the tree to use based on method. This immediately causes item #3 and #4 to become extremely hard to do, as
within the POST tree, we have no insight as to what is available in the GET tree. Imagine we have one endpoints shown below:
What happens when we do the following:
We end up getting a HTTP Status Code of 404, or “Resource Not Found” as a response. This isn’t really true, as the resource /hi exists, we just aren’t allowed to use
that method…. We should be getting back a 405, or “Method Not Allowed” response back. Moreover, to point #4 above, we also get a 404 if we use an OPTIONS HTTP method
as the OPTIONS tree isn’t populated with this route. To these ends I have submitted a PR which is still being fleshed out to improve the speed implications.
What does this PR do?
This PR basically eliminates the per method tree structure in favor of a full tree combined for all routes, which contains all methods applicable for each leaf node route
that is specified. Below is a snip-it of the visualization of the Github API loaded into this new single tree. You can pretty clearly see the available methods on particular routes.
The result of combining all the method trees into a single tree wasn’t inconsequential as can be seen below (about 8.5K ns slower), but then again i am working on ways to speed up the implementation, by
possibly bringing back the per method tree in conjunction with a full tree. When the router is instantiated, it will populate the full tree with all of the routes and related methods into one
tree, then create per method trees, and when a leaf node is created, perform a find on the full tree for that url path and grab all of the applicable method handlers. This will give
the benefit of the per method tree speed, with the ability to stay within the HTTP specification, and hopefully allow for self documenting apis.
Next Steps
I believe I am going to take a look at what it would take to put the URL variables (/hi/*:name*/*:action*) into the HTTP request itself, in order to not have the router tied specifically
to the context concept. I feel like this will be an easy task, and allow for the router itself to become portable, without having to bend your handlers to include this particular context implementation.