lua-resty-route
URL Routing Library for OpenResty Supporting Pluggable Matching Engines
$ opm get DevonStrawn/lua-resty-route
lua-resty-route
lua-resty-route is a URL routing library for OpenResty supporting multiple route matchers, middleware, and HTTP and WebSockets handlers to mention a few of its features.
Matchers
lua-resty-route
supports multiple different matchers on routing. Right now we support these:
Prefix (case-sensitive and case-insensitive)
Equals (case-sensitive and case-insensitive)
Match (using Lua's
string.match
function)Regex (case-sensitive and case-insensitive)
Simple (case-sensitive and case-insensitive)
Matcher is selected by a prefix in a route's pattern, and they do somewhat follow the Nginx's location
block prefixes:
Prefix | Matcher | Case-sensitive | Used by Default ---------|---------|----------------|---------------- [none]
| Prefix | ✓ | ✓ *
| Prefix | | =
| Equals | ✓ | =*
| Equals | | #
| Match | ¹ | ~
| Regex | ✓ | ~*
| Regex | | @
| Simple | ✓ | @*
| Simple | |
¹ Lua string.match
can be case-sensitive or case-insensitive.
Prefix Matcher
Prefix, as the name tells, matches only the prefix of the actual location. Prefix matcher takes only static string prefixes. If you need anything more fancy, take a look at regex matcher. Prefix can be matched case-insensitively by prefixing the prefix with *
, :-). Let's see this in action:
route "/users" (function(self) end)
This route matches locations like:
/users
/users/edit
/users_be_aware
But it doesn't match location paths like:
/Users
/USERS/EDIT
But those can be still be matched in case-insensitive way:
route "*/users" (function(self) end)
Equals Matcher
This works the same as the prefix matcher, but with this we match the exact location, to use this matcher, prefix the route with =
:
route "=/users" {
get = function(self) end
}
This route matches only this location:
/users
Case-insensitive variant can be used also:
route "=*/users" {
get = function(self) end
}
And this of course matches locations like:
/users
/USERS
/usErs
Match Matcher
This matcher matches patters using Lua's string.match
function. Nice thing about this matcher is that it accepts patterns and also provides captures. Check Lua's documentation about possible ways to define patterns. Here are some examples:
route "#/files/(%w+)[.](%w+)" {
get = function(self, file, ext) end
}
This will match location paths like:
/files/test.txt
etc.
In that case the provided function (that answers only HTTP GET
requests in this example), will be called also with these captures: "test"
(function argument file
) and txt
(function argument ext
).
For many, the regular expressions are more familiar and more powerfull. That is what we will look next.
Regex Matcher
Regex or regular expressions is a common way to do pattern matching. OpenResty has support for PCRE compatible regualar expressions, and this matcher in particular, uses ngx.re.match
function:
route [[~^/files/(\w+)[.](\w+)$]] {
get = function(self, file, ext) end
}
As with the Match matcher example above, the end results are the same and the function will be called with the captures.
For Regex matcher we also have case-insensitive version:
route [[~*^/files/(\w+)[.](\w+)$]] {
get = function(self, file, ext) end
}
Simple Matcher
This matcher is a specialized and limited version of a Regex matcher with one advantage. It handles type conversions automatically, right now it only supports integer conversion to Lua number. For example:
route:get "@/users/:number" (function(self, id) end)
You could have location path like:
/users/45
The function above will get 45
as a Lua number
.
Supported simple capturers are:
:string
, that is equal to this regex[^/]+
(one or more chars, not including/
):number
, that is equal to this regex\d+
(one or more digits that can be turned to Lua number usingtonumber
function)
In future, we may add other capture shortcuts.
Of course there is a case-insensitive version for this matcher as well:
route:get "@*/users/:number" (function(self, id) end)
The simple matcher always matches the location from the beginning to end (partial matches are not considered).
Routing
There are many different ways to define routes in lua-resty-route
. It can be said that it is somewhat a Lua DSL for defining routes.
To define routes, you first need a new instance of route. This instance can be shared with different requests. You may create the routes in init_by_lua*
. Here we define a new route instance:
local route = require "resty.route".new()
Now that we do have this route
instance, we may continue to a next section, "HTTP Routing".
Note: Routes are tried in the order they are added when dispatched. This differs from how Nginx itself handles the location
blocks.
HTTP Routing
HTTP routing is the most common thing to do in web related routing. That's why HTTP routing is the default way to route in lua-resty-route
. Other types of routing include e.g. "WebSockets routing".
The most common HTTP request methods (sometimes referred to as verbs) are:
Method | Definition ---------|----------- GET
| Read POST
| Create PUT
| Update or Replace PATCH
| Update or Modify DELETE
| Delete
While these are the most common ones, lua-resty-route
is not by any means restricted to these. You may use whatever request methods there is just like these common ones. But to keep things simple here, we will just use these in the examples.
The General Pattern in Routing
route(...)
route:method(...)
or
route(method, pattern, func)
route:method(pattern, func)
e.g.:
route("get", "/", function(self) end)
route:get("/", function(self) end)
Only the first function argument is mandatory. That's why we can call these functions in a quite flexible ways. For some methods
, e.g. websocket, we can pass a table
instead of a function
as a route handler. Next we look at different ways to call these functions.
Defining Routes as a Table
route "=/users" {
get = function(self) end,
post = function(self) end
}
local users = {
get = function(self) end,
post = function(self) end
}
route "=/users" (users)
route("=/users", users)
Using Lua Packages for Routing
route "=/users" "controllers.users"
route("=/users", "controllers.users")
These are same as:
route("=/users", require "controllers.users")
Defining Multiple Methods at Once
route { "get", "head" } "=/users" (function(self) end)
Defining Multiple Routes at Once
route {
["/"] = function(self) end,
["=/users"] = {
get = function(self) end,
post = function(self) end
}
}
Routing all the HTTP Request Methods
route "/" (function(self) end)
route("/", function(self) end)
The Catch all Route
route(function(self) end)
Going Crazy with Routing
route:as "@home" (function(self) end)
route {
get = {
["=/"] = "@home",
["=/users"] = function(self) end
},
["=/help"] = function(self) end,
[{ "post", "put"}] = {
["=/me"] = function(self)
end
},
["=/you"] = {
[{ "get", "head" }] = function(self) end
},
[{ "/files", "/cache" }] = {
-- requiring controllers.filesystem returns a function
[{"get", "head" }] = "controllers.filesystem"
}
}
As you may see this is pretty freaky. But it doesn't actually stop here. I haven't even mentioned things like callable Lua tables (aka tables with metamethod __call
) or web sockets routing. They are supported as well.
WebSockets Routing
File System Routing
File system routing is based on a file system tree. This could be considered as a routing by a convention. File system routing depends on either LuaFileSystem module or a preferred and LFS compatible ljsyscall.
As an example, let's consider that we do have this kind of file tree:
/routing/
├─ index.lua
├─ users.lua
└─ users/
│ ├─ view@get.lua
│ ├─ edit@post.lua
│ └─ #/
│ └─ index.lua
└─ page/
└─ #.lua
This file tree will provide you with the following routes:
@*/
→index.lua
@*/users
→users.lua
@*/users/view
→users/view@get.lua
(only GET requests are routed here)@*/users/edit
→users/edit@post.lua
(only POST requests are routed here)@*/users/:number
→users/#/index.lua
@*/page/:number
→page/#.lua
The files could look like this (just an example):
index.lua
:
return {
get = function(self) end,
post = function(self) end
}
users.lua
:
return {
get = function(self) end,
post = function(self) end,
delete = function(self) end
}
users/view@get.lua
:
return function(self) end
users/edit@post.lua
:
return function(self) end
users/#/index.lua
:
return {
get = function(self, id) end,
put = function(self, id) end,
post = function(self, id) end,
delete = function(self, id) end
}
page/#.lua
:
return {
get = function(self, id) end,
put = function(self, id) end,
post = function(self, id) end,
delete = function(self, id) end
}
To define routes based on file system tree you will need to call route:fs
function:
-- Here we assume that you do have /routing directory
-- on your file system. You may use whatever path you
-- like, absolute or relative.
route:fs "/routing"
Using file system routing you can just add new files to file system tree, and they will be added automatically as a routes.
Named Routes
You can define named route handlers, and then reuse them in actual routes.
route:as "@home" (function(self) end)
(the use of @
as a prefix for a named route is optional)
And here we actually attach it to a route:
route:get "/" "@home"
You can also define multiple named routes in a one go:
route:as {
home = function(self) end,
signin = function(self) end,
signout = function(self) end
}
or if you want to use prefixes:
route:as {
["@home"] = function(self) end,
["@signin"] = function(self) end,
["@signout"] = function(self) end
}
Named routes must be defined before referencing them in routes. There are or will be other uses to named routers as well. On todo list there are things like reverse routing and route forwarding to a named route.
Middleware
Middleware in lua-resty-route
can be defined on either on per request or per route basis. Middleware are filters that you can add to the request processing pipeline. As lua-resty-route
tries to be as unopionated as possible we don't really restrict what the filters do or how they have to be written. Middleware can be inserted just flexible as routes, and they actually do share much of the logic. With one impotant difference. You can have multiple middleware on the pipeline whereas only one matchin route will be executed. The middleware can also be yielded (coroutine.yield
), and that allows code to be run before and after the router (you can yield a router as well, but that will never be resumed). If you don't yield, then the middleware is considered as a before filter.
The most common type of Middleware is request level middleware:
route:use(function(self)
-- This code will be run before router:
-- ...
self.yield() -- or coroutine.yield()
-- This code will be run after the router:
-- ...
end)
Now, as you were already hinted, you may add filters to specific routes as well:
route.filter "=/" (function(self)
-- this middleware will only be called on a specific route
end)
You can use the same rules as with routing there, e.g.
route.filter:post "middleware.csrf"
Of course you can also do things like:
route.filter:delete "@/users/:number" (function(self, id)
-- here we can say prevent deleting the user who
-- issued the request or something.
end)
All the matching middleware is run on every request, unless one of them decides to exit
, but we do always try to run after filters for those middleware that already did run, and yielded. But we will call them in reverse order:
middleware 1 runs and yields
middleware 2 runs (and finishes)
middleware 3 runs and yields
router runs
middleware 3 resumes
middleware 1 resumes
The order of middleware is by scope:
request level middleware is executed first
router level middleware is executed second
If there are multiple requet or router level middleware, then they will be executed the same order they were added to a specific scope. Yielded middleware is executed in reverse order. Yielded middleware will only be resumed once.
Internally we do use Lua's great coroutines
.
We are going to support a bunch of predefined middleware in a future.
Events
Events allow you to register specialized handlers for different HTTP status codes or other predefined event codes. There can be only one handler for each code or code group.
You can for example define 404
aka route not found handler like this:
route:on(404, function(self) end)
Some groups are predefined, e.g.:
info
, status codes 100 – 199success
, status codes 200 – 299redirect
, status codes 300 – 399client error
, status codes 400 – 499server error
, status codes 500 – 599error
, status codes 400 – 599
You may use groups like this:
route:on "error" (function(self, code) end)
You can also define multiple event handlers in a one go:
route:on {
error = function(self, code) end,
success = function(self, code) end,
[302] = function(self) end
}
Then there is a generic catch-all event handler:
route:on(function(self, code) end)
We will find the right event handler in this order:
if there is a specific handler for a specific code, we will call that
if there is a group handler for specific code, we will call that
if there is a catch-all handler, we will call that
Only one of these is called per event.
It is possible that we will add other handlers in a future where you could hook on.
Router API
You may have seen in previous examples functions get as a first parameter a self
. The self
represents a router
that contains many nice functions documented below.
While the above so called Route API
is for defining the routes, the Router API
is actually about running the routes.
router.context
This is really powerful concept here to share data between different routes and functions. Many middleware will be inserted to context.
E.g. a redis middleware could add redis
object to context
so that you could just:
local ok, err = self.redis:set("cat", "tommy")
Opening and closing the Redis connection is something that the middleware does automatically before scenes. It means that you don't need to initiate or close the connections to Redis server, but this small framework
takes care of this. As you see, this self
parameter is automatically passed around different layers of this framework, and this context makes it easy to pass data between them.
router.yield()
Is similar to coroutine.yield()
but as you have seen above in middlewares section, it is quite nice to just call self.yield()
instead to split middleware to before and after filters
, it also makes us possible to add e.g. debugging / profiling code in a future. self.yield()
is more self explaining what happens and makes code easier to read (may be subjective opinion).
router:redirect(uri, code)
Similar to ngx.redirect
but runs redirect event handler and after filters before actually calling ngx.redirect
with code
(or ngx.HTTP_MOVED_TEMPORARILY
if not specified) and ending the handler.
router:exit(uri, code)
Similar to ngx.exit
but runs event handler and after filters before actually calling ngx.exit
with code
(or ngx.OK
if not specified) and ending the handler.
router:exec(uri, args)
Similar to ngx.exec
but runs event handler and after filters before actually calling ngx.exec
and ending the handler.
router:done()
Similar to ngx.exit
with ngx.HTTP_OK
but runs event handler and after filters before actually calling ngx.exit
and ending the handler.
router:abort()
This is reserved for ngx.on_abort
usage (NYI). Right now only calls ngx.exit(499)
after running event handler and after filters.
router:fail(error, code)
If error
is a string, then logs it to error log. Otherwise it is similar to ngx.exit(code)
(by default the code
is ngx.HTTP_INTERNAL_SERVER_ERROR
) but runs event handler and after filters before actually calling ngx.exit
and ending the handler.
router:to(location, method)
Allows you to execute another route (defined by route
).
router:render(content, context)
Writes content to output stream. If there is a context.template
then it will call context.template.render(content, context or self.context)
.
router:json(data)
Encodes data as JSON, adds application/json
content-type header and outputs the JSON.
router:*
A lot more can be added here to make writing code less repetive, but a lot can be done with injecting into self.context
as well.
Roadmap
This is a small collection of ideas that may or may not be implemented as a part of lua-resty-route
.
Add documentation
Add tests
Rewrite current middleware and add new ones
Rewrite current websocket handler
Add route statistics
Add an automatic route cleaning and redirecting (possibly configurable) (clean function is already written)
Add an automatic slash handling and redirecting (possibly configurable)
Add a more automated way to define redirects
Add a support for route caching
Add a support to route by host
Add a support to route by headers
Add a support for Nginx phases
Add a support for easy way to define Web Hooks routes
Add a support for easy way to define Server Sent Events routes
Add a support for "provides", e.g. renderers (?)
Add a support for conditions, e.g. content negotiation
Add a support for route grouping (already possible on Nginx at config level)
Add a support for reverse routing
Add a support for form method spoofing
Add a support for client connection abort event handler (
ngx.on_abort
)Add a support for host (and possibly) other headers filtering
Add a support for basic authentication
Add a support for JWT / OpenID Connect authentication
Add bootstrapping functionality from Nginx configs
Add support for resources (or view sets) (a more automated REST-routing)
Add filesystem routing support for resources (or view sets)
See Also
lua-resty-reqargs — Request arguments parser
lua-resty-session — Session library
lua-resty-template — Templating engine
lua-resty-validation — Validation and filtering library
License
lua-resty-route
uses two clause BSD license.
Copyright (c) 2015 – 2017, Aapo Talvensaari
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES`
Authors
Aapo Talvensaari (@bungle)
License
2bsd
Versions
-
URL Routing Library for OpenResty Supporting Pluggable Matching Engines 2020-01-27 23:15:31