I've been exploring new APIs that deviate from what nodejs has had
historically with great success. I wrote a simple web module that
replaces the http module. It's easier to use and a *lot* faster.
(granted it's not 100% done and will probably not as fast in the end).
## Proposed "web" Interface
I propose we remove the "http" module from core and maintain it as a
third-party library for people wanting the node API.
I want to make a new module called "web". We can possible keep this
external as well. Ideally it only depends on a socket connection
(duplex stream) and the http_parser binding. Currently I get the
stream by using uv.Tcp directly.
The main interface to the web module is the "app" function. This API
is borrowed heavily from wsgi, rack, and stratajs. An app is defined
as a function.
local function app(request, respond)
local body = "You requested " .. request.url.path .. "\n"
respond(200, {
["Content-Type"] = "text/plain",
["Content-Length"] = #body
}, body)
end
The `request` table will contain the output of the http_parser as well
as the original socket.
- socket: The raw duplex socket over which we're doing http. This
implements the stream interface. Websocket implementors will want to
use this.
- method: The http method as a string ("GET", "POST", etc..)
- url: A table that's the result of http_parser's parseUrl function.
- url.path - The path with query string removed ("/foo/bar")
- ... other properties from parseUrl
- shouldKeepAlive: A flag set by http_parser
- upgrade: A flag set by http_parser
- ... other properties set by http_parser's info table
Also, the `request` table itself will be a readable stream that
outputs the http body data (chunked encoding, etc will be already
decoded)
As seen in the example, `respond` is a function that accepts the http
response status code, the response headers as a table, and the body.
The body can be either a string or a readable stream.
Using this new interface, making middleware modules is trivially easy.
If, for example, you want to wrap an app with a middleware that logs
all requests, it can be defined like this:
-- The "log" module
return function (app)
return function (req, res)
app(req, function (code, headers, body)
print(req.method .. ' ' .. req.url.path .. ' ' .. code)
res(code, headers, body)
end)
end
end
And used like this:
-- Replace our app function with a new function that logs all requests.
app = require('log')(app)
Instead of creating the tcp server automatically, the http module will
just provide a way to wrap the app into a stream handler function.
(not implemented yet)
-- Example using the net module to get the duplex stream over tcp
connections
-- Assume `app` from above
local handler = web.createStreamHandler(app)
net.createStream(handler):listen(8080)
I haven't implemented that last part, but I think decoupling http from
tcp is a good thing. I'll leave it up to web frameworks to create the
convenience functions for auto-creating the tcp/tls servers.
In initial benchmarking, this new interface is extremely fast. About
10x faster than our current http module. Part of this is because I'm
skipping the net module and using uv.Tcp directly. And part is
because the new API is easier to implement and requires a lot less lua
code. (See the examples/web-app/ folder in luvit's source for my
prototype.) Initial Benchmarks at
https://gist.github.com/3517969
## Proposed readable stream Interface.
The current readable stream interface that we borrowed from node has
two events ("data" and "end") and two methods (.pause() and
.resume()). The problem with this interface is data events may emit
before you've had a chance to start listening for them and you will
lose data. This is especially common in a middleware system where a
user is trying to upload a file, but we first need to make some async
calls against a db to validate the upload. While the auth layer is
checking the request, the upload data gets emitted. The upload module
never gets to see the events!
The new interface works instead by pulling data out of the stream.
local stream = fs.createReadStream("myfile.txt")
local function onRead(chunk)
if chunk then
-- This was a "data" event
-- read the next chunk
stream.read(onRead)
else
-- We're done! This would have been an "end" event.
end
end
stream.read(onRead)
Backpressure is exerted if you don't call stream.read in the same tick
as the onRead event. It will pause the upstream source and resume it
later when .read() is called again.
WIth this new interface, middleware modules that don't care about the
input stream, won't have to care about it. The data won't start
flowing till the first middleware asks for it.
---------------------
I have other ideas as well, but let's get these two right first. They
will form a solid base for streaming http servers that are easy to
write and compose libraries.
Please give feedback.
-Tim Caswell