Authors: | Matthew "LeafStorm" Frazier <[email protected]>, Stuart P. Bentley <[email protected]> |
---|---|
Date: | January 26, 2011 |
Status: | Early draft |
This document specifies an interface between Web servers and Web applications written in the Lua programming language, to promote interoperability between Web servers and the development of an ecosystem of reusable components for Lua on the Web.
Lua has had an API like this developed for it before, known as WSAPI (Web Server Application Programming Interface). WSAPI's development was largely motivated by the Kepler Project. However, the interface never really "took off" in the sense that WSGI and Rack did in the Python and Ruby worlds. In addition, the specification was undermaintained by the Kepler Project team, with WSAPI's implementation being largely bound to the WSAPI library and servers maintained by the Kepler Project.
This specification is intended to be easier to implement for server developers and application developers, and to encourage a wider spread of development in new servers, applications, and components.
A callable is defined as a value that returns true
when passed to this function:
function callable(v) return type(v)==function or callable(getmetatable(v).__call) end
A LASI request handler is a callable that takes a Request environment table (specified below) and returns three values - a status code, a table of headers, and the response body. For example:
function handler (environ) local data = "Hello, world!" return 200, {["Content-Type"] = "text/plain"}, data end
The components are as follows:
This can be a string or a number. If it is a string, it must match the Lua pattern ^([1-5]%d%d) (%a[%w ]*)$
, where the first capture is the status code and the second capture is the reason. If it is a number, it must be greater than 99, less than 600, and be a whole number. It is used as the status code and the server must supply an appropriate reason (e.g. "OK" for 200 or "Not Found" for 404).
A table of headers to send back to the client. The keys of the table are the header names, and the values are the headers' values. Header names should match the pattern %a[%a%d-_]*
. Underscores in header names will be converted to dashes in the response.
Any headers whose values are not strings should be converted with tostring. Any headers whose names start with X-LASI
MUST NOT be sent by the server, and may be used by middleware or by the server to trigger additional functionality.
The response body can take three forms: a string, a table of strings, or an iterator callable. Each type description here includes an example that would result in the following result body:
<!doctype html><html><body><p>Hello, world!</p></body></html>
A string will be used byte-for-byte as the response body.
"<!doctype html><html><body><p>Hello, world!</p></body></html>"
When using a table, the result body will be equivalent to the result of calling table.concat
with the table returned for the body.
{ "<!doctype html>", "<html>", "<body>", "<p>Hello, world!</p>", "</body>" }
When called, it should return strings, each of which is sent to the client. Once it returns nil, the server should finish sending the response.
coroutine.wrap(function () coroutine.yield("<!doctype html>") coroutine.yield("<html>") coroutine.yield("<body>") coroutine.yield("<p>Hello, world!</p>") coroutine.yield("</body>") coroutine.yield("</html>") end)
The server may choose to use a chunked transfer encoding when reading from a callable.
The environment table is a table whose keys are strings. It contains all the data about the incoming request that the server can provide.
Every value is as true to the original HTTP request as possible, with no processing (such as URL-decoding) applied.
Each item includes example code for handling this sample HTTP request.
- The server is:
- at 192.0.2.1
- listening on port 80
- calling the handler for requests prefixed with /wiki/ (see Dispatching)
- running a server called "ExampleServer", version 2.2.3
- using a CGI connector called "ExampleCC", version 0.3.0
- The client is:
- requesting from 127.0.0.1
- listening on port 8080
- using a client called "ExampleBrowser", version 2.0.2
The request:
POST /wiki/Ninja+Ca%24h?action=submit HTTP/1.1 Host: server.example.com Connection: close User-Agent: ExampleBrowser/2.0.2 Content-Type: application/x-www-form-urlencoded content=This+is+unencoded.%2E%0D%0A%0D%0AThis+is+encoded%2E&user=nobody
This is the HTTP method for the request, as a string (for example,``GET``). It provides the same information as the CGI variable REQUEST_METHOD.
assert(environ.method=="POST")
A table that maps the names of the request headers to their values, both as strings. Headers are normalized to lower case, and all dashes are changed to underscores (i.e. ["content_type"]
). Keys in this table provide the same information as meta-variables prefixed with HTTP_
in CGI.
assert(environ.headers.host="server.example.com") assert(environ.headers.user_agent="ExampleBrowser/2.0.2") assert(environ.headers.connection="close") assert(environ.headers.content_type="application/x-www-form-urlencoded")
The "request prefix", as explained in the section Dispatching. Similar to CGI's SCRIPT_NAME.
assert(environ.prefix=="/wiki/")
The path below the request prefix, as explained in Dispatching. Similar to CGI's PATH_INFO.
assert(environ.path=="Ninja+Ca%24h")
The query string from the URL. Corresponds to CGI's QUERY_STRING meta-variable.
assert(environ.query=="action=edit")
If the request came via insecure http, this is "http". If it came in via SSL/TLS secured HTTPS, this is "https".
assert(environ.url_scheme=="http")
A table containing functions which take a single string parameter for a message to write to log files (or similar). If the server or gateway does not support logging, the functions will do nothing.
This function is for standard information logging.
environ.log.info("Handler started "..os.date())
This function is for information used in debugging.
environ.log.debug("Entering avatar generation function")
This function is for logging warnings.
environ.log.warn("Optimal image library not found, resorting to fallback")
This function is for logging errors that should not occur but are recoverable.
environ.log.error("Function called for user that doesn't exist")
This function is for logging errors that cause the script to die.
environ.log.fatal("No database server")
A function that takes the number of bytes to read from the request body (or all of it, if n
is omitted). The server MUST ensure that the application cannot read beyond the content length.
assert(environ.readbody()=="content=This+is+unencoded.%2E%0D%0A%0D%0AThis+is+encoded%2E&user=nobody")
A table containing values of true
for various properties of the execution environment of the request handler.
multithread
: The request handler may be simultaneously invoked by another OS-level thread (not coroutine) in the same process.multiprocess
: The request handler may be simultaneously invoked by another process.multicoroutine
: The request handler may be simultaneously invoked by another coroutine in the same process.nonblocking
: The request handler is in a nonblocking event loop.runonce
: The request handler is probably going to only be run once before its process is shut down (like in CGI).
assert(string.find(environ.server.connector,"CGI") and environ.execution.runonce)
This is a table with information about the server environment.
The name and version of the server. For a CGI connector, this would be defined to the value of SERVER_SOFTWARE.
assert(environ.server.software == "ExampleServer/2.2.1")
The name and version of the connector. For a CGI connector, this should identify the connector as well as GATEWAY_INTERFACE.
assert(environ.server.software == "ExampleCC/0.3.0 (CGI/1.1)")
The port that the server is listening on. Analogous to CGI's SERVER_PORT.
assert(environ.server.port == "80")
A table containing data about the client.
The IP address of the client. Corresponds to the CGI metavariable REMOTE_ADDR
.
assert(environ.remote.addr == "127.0.0.1")
The port that the client is connected from, if available. If not, this should be nil.
assert(environ.remote.port == "8080")
The version of LASI being used. It is represented as a number - 0.3.0 in this case. Clarifications in the spec, where all implementations are by definition backwards-compatible with earlier versions, increment the tertiary number. Minor changes, where earlier versions are forwards-compatible but not the other way around, increment the secondary number. Major changes, where compatible code cannot be shared between both, increment the primary version.
In pre-working drafts where the primary version number is 0, both major and minor changes increment the secondary number.
assert(environ._VERSION == "LASI 0.3.0")
Middleware, servers, and other components may add their own tables to the environment. They should have the name of that middleware. (The lasi
table is reserved for use by this specification.)
The "prefix" and "path" keys are used to dispatch requests. For example, assume that a request handler is listening on the server root.
URL Prefix Path http://server/ "/" "" http://server/wiki "/" "wiki" http://server/wiki/ "/" "wiki/" http://server/wiki/Ninja "/" "wiki/Ninja" http://server/wiki/Ninja/ "/" "wiki/Ninja/" http://server/wiki/Ninja/edit "/" "wiki/Ninja/edit" http://server/wiki?p=42 "/" "wiki"
If the request handler was instead listening at /wiki/
, these values would be used:
URL Prefix Path http://server/ (doesn't reach the request handler) http://server/wiki "/wiki/" "" http://server/wiki/ "/wiki/" "" http://server/wiki/Ninja "/wiki/" "Ninja" http://server/wiki/Ninja/ "/wiki/" "Ninja/" http://server/wiki/Ninja/edit "/wiki/" "Ninja/edit" http://server/wiki?p=42 "/wiki/" "" http://server/wiki/Ninja?p=42 "/wiki/" "Ninja" http://server/wiki//Ninja "/wiki/" "/Ninja"
The prefix ALWAYS begins and ends with a slash. The path NEVER begins with a slash (unless there were two slashes in a row at the prefix/path split), and only ends with a slash if there was a trailing slash on the end of the path.
The prefix and the path may be modified by "dispatcher" middleware (i.e. middleware that takes multiple applications and splits requests among them depending on the path).
Middleware is essentially a request handler that wraps other request handlers, to provide services to the server or the interior handlers. A simple example of middleware:
function hello (environ) return 200, {["Content-Type"] = "text/plain"}, {"Hello, world!"} end function middleware (handler) return function (environ) local status, headers, body = handler(environ) headers["X-Powered-By"] = "LASI" return status, headers, body end end
You could also implement middleware as a table with a __call metamethod, or really however you want. However, just like a normal request handler, middleware MUST be callable and return the status, headers, and response body.
Middleware may manipulate the environment, or modify the returned response. However, if they modify the response body, and it is a function, they MUST exhaust it completely (i.e. iterate until it returns nil).
The following table describes how the standard CGI meta-variables, used by several web frameworks as well as CGI itself, translate into LASI.
CGI Meta-Variable LASI equivalent AUTH_TYPE (None) CONTENT_LENGTH headers.content_length
[1]CONTENT_TYPE headers.content_type
[1]GATEWAY_INTERFACE server.connector
PATH_INFO path
PATH_TRANSLATED (None) QUERY_STRING query
REMOTE_ADDR remote.addr
REMOTE_HOST (None) REMOTE_IDENT None. ident is almost universally blocked anyway. REQUEST_METHOD method
SCRIPT_NAME prefix
HOST_NAME
- For the name of the host machine:
server.name
- For the host as requested:
headers.host
(this corresponds to CGI's HTTP_HOST)SERVER_PORT server.port
SERVER_SOFTWARE server.software
HTTP_*<header>* headers.*<header>*
[1] | (1, 2) These headers must be defined if the request was accompanied by a body. |
Parts of the LASI specification were adapted from WSAPI, WSGI, PSGI, JSGI, and Rack. In addition, Armin Ronacher's Python Web API 1.0 draft (aka "Super Secret WSGI Replacement") was a major influence on the overall design of the spec. I would like to thank everyone who has worked on Web programming with any one of those efforts.