Thank you for taking a look at it. The code isn’t posted anywhere, but here is the HttpClient that I use in the driver. This is an example usage:
local HttpClient = require 'httpclient'
local client = HttpClient:new({
ip = ip,
port = port
})
local request = client:request({
method = 'POST',
route = 'api',
body = {
devicetype = driver.NAME .. '#' .. dni,
generateclientkey = true
}
})
This is the module code:
local log = require 'log'
local json = require 'st.json'
local ltn12 = require 'ltn12'
local utils = require 'st.utils'
local collection = require "collection"
local socket = require 'cosock.socket'
local cosock = require 'cosock'
-- `cosock.asyncify` works like `require` but swaps out any sockets
-- created to be cosock sockets. This is important because we
-- are running inside of a command handler.
local http = cosock.asyncify 'socket.http'
local HttpClient = {
scheme = 'https',
ip = nil,
port = nil,
headers = {}
}
function HttpClient:new(o)
return setmetatable(o or {}, {
__index = self,
__tostring = self.toString
})
end
function HttpClient.isSuccess(code)
local ncode = tonumber(code)
return ncode ~= nil and ncode >= 200 and ncode < 300
end
function HttpClient.isTruncated(code)
return code ~= nil and type(code) == "string" and
(code:match('socket.*closed') ~= nil or code:match('socket.*does not exist') ~= nil)
end
function HttpClient.isTimeout(code)
return code == 'timeout'
end
function HttpClient.isUnreachable(code)
return code ~= nil and type(code) == "string" and (
code:match('HostUnreachable') ~= nil or
code:match('could not parse host') ~= nil
)
end
function HttpClient:isReady()
return self.scheme ~= nil and self.ip ~= nil
end
-- @param args.method (required)
-- @param args.port (optional)
-- @param args.headers (optional)
-- @param args.route (optional)
-- @param args.body (optional)
function HttpClient:request(args)
local host = self.ip
local port = args.port or self.port
if port then
host = string.format('%s:%s', host, port)
end
local url = string.format('%s://%s/%s', self.scheme, host, args.route or '')
-- overlay headers (client --> args --> content)
local encoded = json.encode(args.body or {})
local headers = utils.merge(utils.merge({
['Content-Type'] = 'application/json',
['Content-Length'] = encoded:len()
}, args.headers or {}), self.headers or {})
local result = {}
local logRequest = function(level, method, url, code)
level(string.format('%s %s %s', method, url, code))
end
local lastAttempt = 2
for attempt = 0, lastAttempt do
local raw = {}
local _, code, _, status = http.request {
method = args.method,
url = url,
headers = headers,
source = ltn12.source.string(encoded),
sink = ltn12.sink.table(raw),
create = function()
local sock = socket.tcp()
-- modify receive call for prefix bug in ST tcp implementation
-- allows code prior to a fix to work with cosock asyncify
-- https://github.com/cosock/cosock/issues/26
function sock:receive(pattern, prefix)
local data = socket.tcp.receive(self, pattern)
if prefix ~= nil and data ~= nil then
return prefix .. data
else
return data
end
end
return sock
end
}
result.code = code
result.status = status
result.raw = raw
if code == 200 then
logRequest(log.debug, args.method, url, code)
break
elseif self.isSuccess(code) then
logRequest(log.warn, args.method, url, code)
break
elseif self.isUnreachable(code) then
logRequest(log.error, args.method, url, code)
break
elseif self.isTimeout(code) and attempt < lastAttempt then
logRequest(log.warn, args.method, url, code)
-- loop and retry
elseif self.isTruncated(code) and attempt < lastAttempt then
logRequest(log.warn, args.method, url, code)
-- loop and retry after exponential backoff
socket.sleep(math.floor(2 ^ attempt))
else
logRequest(log.error, args.method, url, code)
break
end
end
local response = {}
if not self.isTruncated(result.code) and #result.raw > 0 then
response = json.decode(table.concat(result.raw))
if response and response.errors and #response.errors > 0 then
collection(response.errors):each(function(_, err)
log.error(err.description)
end)
end
end
return {
code = result.code,
status = result.status,
response = response
}
end
if require then
return HttpClient
end