Skip to content

Instantly share code, notes, and snippets.

@stuartpb
Forked from leafstorm/lasi.rst
Created Jan 25, 2011
Embed
What would you like to do?

Lua Application/Server Interface version 0.3.0

Authors:Matthew "LeafStorm" Frazier <leafstormrush@gmail.com>, Stuart P. Bentley <stuart@testtrack4.com>
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.

Rationale

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.

Terminology

callable

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

Interface

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:

status code

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).

headers

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.

response body

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>

string

A string will be used byte-for-byte as the response body.

"<!doctype html><html><body><p>Hello, world!</p></body></html>"

table

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>"
}

function

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.

Request environment table

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.

Examples

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

method

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")

headers

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")

prefix

The "request prefix", as explained in the section Dispatching. Similar to CGI's SCRIPT_NAME.

assert(environ.prefix=="/wiki/")

path

The path below the request prefix, as explained in Dispatching. Similar to CGI's PATH_INFO.

assert(environ.path=="Ninja+Ca%24h")

query

The query string from the URL. Corresponds to CGI's QUERY_STRING meta-variable.

assert(environ.query=="action=edit")

url_scheme

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")

log

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.

info(message)

This function is for standard information logging.

environ.log.info("Handler started "..os.date())

debug(message)

This function is for information used in debugging.

environ.log.debug("Entering avatar generation function")

warn(message)

This function is for logging warnings.

environ.log.warn("Optimal image library not found, resorting to fallback")

error(message)

This function is for logging errors that should not occur but are recoverable.

environ.log.error("Function called for user that doesn't exist")

fatal(message)

This function is for logging errors that cause the script to die.

environ.log.fatal("No database server")

readbody([n])

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")

execution

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)

server

This is a table with information about the server environment.

software

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")

connector

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)")

port

The port that the server is listening on. Analogous to CGI's SERVER_PORT.

assert(environ.server.port == "80")

remote

A table containing data about the client.

addr

The IP address of the client. Corresponds to the CGI metavariable REMOTE_ADDR.

assert(environ.remote.addr == "127.0.0.1")

port

The port that the client is connected from, if available. If not, this should be nil.

assert(environ.remote.port == "8080")

_VERSION

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.)

Dispatching

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

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).

Transitioning

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.

Thanks

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment