Luncheon

HTTP types for Lua

Luncheon Logo

Install

This library is published on luarocks

luarocks install luncheon

Usage

Luncheon provides Lua tables that represent HTTP Requests and Responses and a way to parse or build them.

Parsing

Both Request and Response expose a constructor source, which expects the only argument to be a function that will return a line when called (at least until the end of the headers see the source docs for more details). So a simple example of that might look like this.

local Request = 'luncheon.request'
local req_lines = {
  'GET / HTTP/1.1',
  'Content-Length: 0',
  ''
}
local req = Request.source(function()
  return table.remove(req_lines, 1)
end)
assert(req.method == 'GET')
assert(req.url.path == '/')
assert(req.http_version == 1.1)
assert(req:get_headers():get_one('content_length') == 0)

local Response = 'luncheon.response'
local res_lines = {
  'HTTP/1.1 200 Ok',
  'Content-Length: 0',
  ''
}
local res = Response.source(function()
  return table.remove(res_lines, 1)
end)
assert(res.status == 200)
assert(res.status_msg == 'Ok')
assert(res.http_version == 1.1)
assert(res:get_headers():get_one('content_length') == 0)

Notice how in both of the above examples, the lines do not contain a new line character, this is because Lua’s normal methods for reading IO, will omit a trailing new line. For example io.open('README.md'):read('*l') returns '# Luncheon' with no trailing new line.

To handle some common use cases, the utils module provides a source wrapper around luasocket’s tcp and udp sockets.

Building

Both Request and Response expose a constructor new along with the serialize and iter methods for building them and then converting them into “hypertext”.

local Request = require 'luncheon.request'
local req = Request.new('GET', '/')
  :add_header('Host', 'example.com')
  :append_body('I am a request body')
for line in req:iter() do
  print(string.gsub(line, '\r?\n$', ''))
end
local Response = require 'luncheon.response'
local res = Response.new(200)
  :add_header('age', '2000')
  :append_body('I am a response body')
print(res:serialize())

Notice how the req:iter() loop has to remove new lines before printing. That means we already get the CRLF line endings required for the start line and headers.

Examples

Basic echo server

This example uses luasocket to receive incoming HTTP Requests and echo them back out as Responses.

Request = require 'luncheon.request'
Response = require 'luncheon.response'
utils = require 'luncheon.utils'
socket = require 'socket'
tcp = assert(socket.tcp())
assert(tcp:bind('0.0.0.0', 8080))
assert(tcp:listen())
while true do
  local incoming = assert(tcp:accept())
  local req = assert(Request.tcp_source(incoming))
  print('Request')
  print('url', req.url.path)
  print('method', req.method)
  print('body', req:get_body())
  local res = Response.new(200, incoming)
    :add_header('Server', 'Luncheon Echo Server')
    :append_body(req:get_body())
    :send()
  incoming:close()
end

See the examples directory for more