Skip to content

Instantly share code, notes, and snippets.

@leafstorm
Created January 22, 2011 21:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save leafstorm/791517 to your computer and use it in GitHub Desktop.
Save leafstorm/791517 to your computer and use it in GitHub Desktop.
A draft of a Lua gateway interface to replace WSAPI.

Lua Application/Server Interface version 1.0

Author

Matthew "LeafStorm" Frazier <leafstormrush@gmail.com>

Date

February 4, 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.

Warning: As of now, this spec is completely unstable and may be changed at any time.

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 and connectors 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.

Notes

The key words "MUST" and "MUST NOT" are to be interpreted as described in RFC 2119.

A callable is defined as either (a) a value of type function or (b) a table or userdata value with a __call metamethod. Servers should take care to check for both of these cases where this specification requires a callable.

When "number" is used, it refers specificially to the Lua number type, not to a string that contains a numeric value, unless the term "string" is also used to describe that value.

Interface

A LASI request handler is a Lua callable that accepts a request environment 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:

Environment

This is the sole argument passed to the request handler. It is a Lua table whose keys are strings. They are described in the following section, Environment Table.

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-]*. Any headers whose values are not strings should be converted with tostring. Any headers whose names start with X-LASI (case-insensitive) MUST NOT be sent by the server, and may be used by middleware or by the server to trigger additional functionality.

Response Body

This can be a table or function, and it contains the actual content of the response. See the section Response Body for details on how it is interpreted.

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.

This specification defines the keys:

method

This is the HTTP method for the request, as a string (for example, GET).

headers

A table that maps the names of the request headers to their values, both as strings. Headers are normalized to lower case, but keep the dashes (i.e. ["content-type"]).

prefix

The "request prefix", as explained in the section Dispatching. This path is not URL-decoded (as in CGI) if possible.

path

The path below the request prefix, as explained in Dispatching. This path is not URL-decoded if possible.

query

The query string from the URL. This should not be URL-decoded.

scheme

The URL scheme the request came in over. This should be https for secure HTTP requests, and http for normal HTTP.

read

This is the input reader function. It can accept either no arguments or one argument (a number), i.e. env.read() or env.read(256). Without an argument, it returns the entire contents of the request body that have not been read up to this point. With an argument, it returns up to the given number of bytes from the request body.

The server MUST ensure that the request handler cannot read beyond the content length of the request body. If the request body is completely exhausted and the client attempts to read, the reader should return nil.

logerror

This is a function that is used to log errors. It must be callable with an error message, which should be logged to a server log file or something of the sort, i.e. env.logerror("Something went completely wrong."). If the server or gateway is incapable of logging errors, it should provide a no-op logerror function.

server

This is a table with information about the server environment. It should contain the keys:

  • software: The name of the server, i.e. Skipray/1.0.
  • connector: If a connector is being used with a server that does not natively "speak" LASI, its name and version should be given here (i.e. LASI-FastCGI/1.0).

In addition, the following keys should be set to true if applicable, and set to false or simply not set if not.

  • 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). This does not guarantee that the handler will only be run once, and the handler MUST still respond to additional requests even with this set.
serverport

The port number that the server is listening on, as a number (i.e. 80). If the server is listening on multiple ports, this should be the one that the request came in on.

remoteaddr

The IP address of the connected client.

remotehost

The hostname of the connected client. If this is not available, it should be set to nil and the value of remoteaddr should be used instead.

remoteport

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

lasi_version

The version of LASI being used. It is represented as a number - 1.0 in this case. Minor changes in the spec increment the version number by .01, and major changes move it to the next whole number (i.e. from 1.x to 2.0).

Middleware, servers, and other components may add their own keys to the environment. They should include at least one underscore, and be prefixed uniquely (for example, myserver_version).

Keys beginning with lasi_, on the other hand, are 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=32         "/"             "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=32         "/wiki/"        ""
http://server/wiki/Ninja?p=32   "/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).

Response Body

The response body can take two forms: a table of strings, or a function.

The table response should be an array-like table, where all keys from 1 to the length of the table contain string values. When this is returned from a request handler, each value is sent to the client, in sequence, without any separators. For example:

{
    "<!doctype html>\n",
    "<p>Hello, world!\n"
}

The function response is an iterator-like function. When called, it should return strings, each of which is sent to the client. Once it returns nil, the server should stop sending content. (The server may wish to use a chunked transfer encoding for this.) For example:

coroutine.wrap(function ()
    coroutine.yield("<!doctype html>\n")
    coroutine.yield("<p>Hello, world!\n")
end)

(There are no reasons you would have to use coroutines, but coroutine.wrap is the easiest way to get this kind of behavior.)

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 with the environment 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 CGI meta-variables used by other interface specs like CGI, WSGI, and WSAPI translate to the LASI environment.

CGI meta-variable LASI environment
HTTP_* headers["*"]
CONTENT_LENGTH headers["content-length"]1
CONTENT_TYPE headers["content-type"]2
GATEWAY_INTERFACE lasi_version
PATH_INFO path
QUERY_STRING query
REMOTE_ADDR remoteaddr
REMOTE_HOST remotehost
REQUEST_METHOD method
SCRIPT_NAME prefix
SERVER_PORT serverport
SERVER_SOFTWARE server.software

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.

In addition, for specific contributions to the spec, I would like to thank:

  • Stuart P. Bentley, for the idea of having a closure for the input reader instead of a full stream, and for the "Transitioning" table.

  1. These headers must be provided whenever the request is accompanied by a body.

  2. These headers must be provided whenever the request is accompanied by a body.

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