Functions guide

High level functions

Webhook Relay Functions provide an easy way to extract values from webhooks and transform HTTP requests before passing them to their final destination. Function can be reused by any number of Inputs and/or Outputs. Since user doesn’t have to run a server, this type of function service is also known as FaaS (Function as a Service) or Serverless.

Functions on Inputs transform webhooks when they enter the system and also allows you to customize both response that Webhook Relay will return and webhook request that will be passed along to all outputs.

Functions on Outputs allow you to customize webhooks for each destination. This way you can have a custom webhook arriving at each destination (Slack, Mattermost or some CRM system like HubSpot or Zoho).

Currently, you can write functions in:

Managing functions

Functions can be managed using relay CLI or through the web dashboard. You can create, edit and delete functions:

Edit functions via UI

Managing functions via CLI

CLI allows to quickly iterate and functions and automate function updates from your CI/CD tool such as Jenkins, Gitlab, GitHub, etc.

Creating a function

To create a new function, simply use relay function create command:

relay function create <file name>.lua

You can also specify name, source and driver using optional flags:

Function example (Lua)

In this example we will use a Lua 5.1 function that takes webhook JSON as an input and converts it to a Slack message JSON to display an alert for us in chat.

Mailgun JSON (shortened version, usually it’s a bit bigger):

{
"signature": {
"timestamp": "xx",
"token": "xx",
"signature": "xxx"
},
"event-data": {
"severity": "permanent",
"log-level": "error",
"recipient": "[email protected]",
"delivery-status": {
"tls": true,
"mx-host": "srv2.yyy.ccc",
"attempt-no": 1,
"description": "",
"session-seconds": 2.601992130279541,
"code": 550,
"message": "High probability of spam",
"certificate-verified": true
}
}
}

Below is a Lua function that converts Mailgun webhook into a Slack webhook:

-- mailgun_to_slack.lua
local json = require("json")

local body, err = json.decode(r.RequestBody)
if err then error(err) end

local message = "Mailgun mail delivery error! Severity: " .. body["event-data"]["severity"] .. " | Reason: " .. body["event-data"]["reason"] .. " | Resp: " .. body["event-data"]["delivery-status"]["message"] .. " | Recipient: " .. body["event-data"]["recipient"]

-- Preparing Slack payload
local slack = {
response_type= "in_channel",
text= message}

local result, err = json.encode(slack)
if err then error(err) end

-- Set request header to application/json for Slack API
r:SetRequestHeader("Content-Type", "application/json")
-- Set modified request body
r:SetRequestBody(result)

This function transforms it into a JSON that can be accepted by Slack incomming webhook API:

{
"response_type": "in_channel",
"text": "Mailgun mail delivery error! Severity: permanent | Reason: generic | Resp: High probability of spam | Recipient: [email protected]"
}

To add this function to your account so we can use it later, type:

relay function create mailgun_to_slack.lua

Using functions

The easiest way to start using Functions is with the forward command:

relay forward --bucket mailgun-to-slack --function mailgun_to_slack --type public https://hooks.slack.com/services/xxx

This command will create a new bucket, input and output. Function will be attached to the output that’s forwarding to https://hooks.slack.com/services/xxx endpoint.

Alternatively, you can attach Functions to existing Inputs:

relay input update INPUT_ID -b BUCKET_NAME --function mailgun_to_slack

or Outputs

relay output update OUTPUT_ID -b BUCKET_NAME --function mailgun_to_slack

To stop using some particular function in your Input or Output, just update Function to “”:

relay input update INPUT_ID -b BUCKET_NAME --function ""

Viewing functions

You can view your uploaded functions via relay function ls command:

relay function ls
ID NAME DRIVER SIZE AGE UPDATED AGO
a298cf54-480b-4c50-b275-a4336f75f26f mailgun_to_slack lua 703 B 4 days 2 days
a1a29b1c-7415-4983-94d5-0472113efeb1 set_response_body wasm 464 kB 8 days 8 days
db5e5b91-cdb2-4712-8413-c4f335218e3f transform_to_slack wasm 501 kB 10 days 10 days

Updating functions

To update a function, you have to specify the name and source location:

relay function update mailgun_to_slack --source mailgun_to_slack.lua

Deleting functions

To delete a function, first ensure that any Input or Output doesn’t use the function anymore. Once it’s done:

relay function rm mailgun_to_slack

Lua functions reference

Lua functions use r object that provide access to request data (headers, query, method and body) and can then update any HTTP request details.

Available data to access:

Method name Type Description
r:RequestBody String Request body.
r:RequestMethod String Request method (PUT, POST, DELETE, etc.).
r:RequestPath String Request path.
r:RequestRawQuery String Request query, for example if the request was made with /api?category=electronics, then the query will be category=electronics.
r:RequestHeader Table A table [foo]bar of headers.

An example of accessing request body and decoding it:

local json = require("json")

-- request body is in r.RequestBody, use it as any other string:
local body, err = json.decode(r.RequestBody)
if err then error(err) end

Available methods to update request:

Method name Parameter Type Description
r:SetRequestBody(“string”) String Update request body
r:SetRequestMethod(“string”) String Update request method
r:SetRequestRawQuery(“foo=bar”) String Update request raw query
r:SetRequestPath(“/extra/path”) String Set additional extra path
r:SetRequestHeader(“key”, “value”) String, String Set new header key/value pair

An example how to update request object:

-- set body
r:SetRequestBody("new body")
-- set method
r:SetRequestMethod("string")
-- set raw query
r:SetRequestRawQuery("foo=bar")
-- set extra path
r:SetRequestPath("/extra/path")
-- set header
r:SetRequestHeader("Content-Type", "application/json")

Available methods to set customized response:

Method name Parameter Type Description
r:SetResponseBody(“string”) String Set response body
r:SetResponseStatusCode(201) Integer Set response status code
r:SetResponseHeader(“key”, “value”) String, String Set response header key/value pair

Customized responses only applicable if function is attached to the Input and not bucket Output.

Decoding & encoding JSON in Lua functions

To decode or encode JSON in a Lua function, import ‘json’ package:

local json = require("json")

local result, err = json.decode(r.RequestBody)
if err then error(err) end

r:SetRequestBody(result.user)

To encode:

local json = require("json")
local new_payload = {
action= "hello",
message= "world"}

local encoded_payload, err = json.encode(new_payload)
if err then error(err) end

Filtering requests

To filter requests based on body, headers or some external conditions, you can use r:StopForwarding() method. This will set webhook status to rejected and this webhook will not be forwarded:

local json = require("json")

local requestBody, err = json.decode(r.RequestBody)
if err then error(err) end

if requestBody["action"] == "not_important" then
-- Request is not important, don't forward it
r:StopForwarding()
return
end
-- Otherwise, continue forwarding

Making HTTP requests from Lua functions

To make an HTTP request from Lua function, import ‘http’ package:

local http = require("http")

-- Making HTTP call
response, error_message = http.request("GET", "https://example.com")
if error_message then error(error_message) end

-- Parsing response body from the API
local api_response, err = json.decode(response.body)
if err then error(err) end

You can also make a POST, PUT, DELETE requests to a 3rd party APIs:

local json = require("json")
local http = require("http")

local payload, err = json.encode({
action = "create_record",
user = "example",
})
if err then error(err) end

local headers = {}
headers["Authorization"] = "Basic " .. "123456"

local resp, err = http.request("POST", "http://example.com/api", { headers = headers, body = payload})
if err then error(err) end

Lua HTTP client API

http.delete(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.get(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.head(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.patch(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
body String Request body.
form String Deprecated. URL encoded request body. This will also set the Content-Type header to application/x-www-form-urlencoded
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.post(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
body String Request body.
form String Deprecated. URL encoded request body. This will also set the Content-Type header to application/x-www-form-urlencoded
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.put(url [, options])

Attributes

Name Type Description
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
body String Request body.
form String Deprecated. URL encoded request body. This will also set the Content-Type header to application/x-www-form-urlencoded
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.request(method, url [, options])

Attributes

Name Type Description
method String The HTTP request method
url String URL of the resource to load
options Table Additional options

Options

Name Type Description
query String URL encoded query params
cookies Table Additional cookies to send with the request
body String Request body.
form String Deprecated. URL encoded request body. This will also set the Content-Type header to application/x-www-form-urlencoded
headers Table Additional headers to send with the request

Returns

http.response or (nil, error message)

http.response

The http.response table contains information about a completed HTTP request.

Attributes

Name Type Description
body String The HTTP response body
body_size Number The size of the HTTP reponse body in bytes
headers Table The HTTP response headers
cookies Table The cookies sent by the server in the HTTP response
status_code Number The HTTP response status code
url String The final URL the request ended pointing to after redirects

Encode/decode Base64

If you need to work with base64 encoding, import crypto:

local crypto = require('crypto')

local encoded = crypto.base64_encode("some value")

-- do something with encoded value

To decode some value:

local crypto = require('crypto')

local decoded = crypto.base64_decode("aGVsbG8gd29ybGQ=")

Parsing multipart/form-data

Webhook Relay detects multipart/formdata and parses it so your function can use it. Parsed form data can be accessed through r.RequestFormData variable. For example if the payload fragment looks like this:

...
--------------------------5683f7544dff7b07
Content-Disposition: form-data; name="username"

John
--------------------------5683f7544dff7b07
...

Then to get username value (which is John) you will need to:

local username = r.RequestFormData.username[1]

-- do something with the username