HTTP endpoints can be used to listen events from third party systems that we officially do not support. Since HTTP endpoints optionally allow to send back a response you can also use it for more ad-hoc purposes other than just listening events, such as exposing your custom APIs that others can consume, or build GUI based applications by responding with HTML.

Getting started

First start by going into Connections menu in the main page and configure yourself a new HTTP endpoint connection. Currently the only thing you can specify is a connection name. In the future we are planning to add additional options, such as enabling common authentication schemes, or enabling IP whitelisting for improved security. Currently, if you need more security other than the unique URL that gets generated, you will need to implement the authentication or IP whitelisting logic within your code block.

Once you have the connection configured import HTTP Endpoint library into your workspace. That library exposes only one trigger. Drag that trigger to your workspace and attach a code block to it. You should also assign a event name for your trigger and specify whether you need to send back a response or not. By default the option is without sending a response, which means the trigger will be invoked asynchronously and generic response is sent back right away, also known as fire and forget behaviour.

However, if you need to send back a response, select the other option, in which case the HTTP endpoint will wait for the function to return a response or will time out.

Once you have configured your triggers and have written your logic, you can go ahead and deploy the workspace. Once deployed you should see a message in feedback that says “Configure HTTP endpoints“, click on it which will open a dialog that exposes unique URLs for each of your configured trigger events.

Note

  • Configuring multiple triggers with the same name will only expose a single URL. Invoking that URL will invoke all configured triggers simultaneously.
  • URL won’t be generated for a trigger that has empty event name.
  • Allowed HTTP methods are: GET, POST, PUT, DELETE.

Endpoints without response

You don’t have to return anything from a function if you configured the endpoint to not send back a response.

You can use the following snippet to get started:

import { HttpEndpointRequest } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<void> {
    // Your code goes here
}
JS

Note

  • Endpoints that won’t send back a response are allowed to run up to 15 minutes. The actual time is slightly less due to fluctuations in internal pipeline. You can get the exact time that your function is allowed to run by checking the property timeout from context argument.
  • In the future you should be able to attach long running triggers to extend the 15 minute time limit.

Endpoints with response

You have to return a response if you configured the trigger to send back a response.

You can use the following snippet to get started:

import { HttpEndpointRequest, HttpEndpointResponse } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse> {
    // Your code goes here
}
JS

Function response must be in following shape:

{
  status: number; // Status code of the response
  headers?: Record<string, string>; // Headers to send back with the response
  body?: string; // Body to send back with response, use base64 encoding to send back binary data
  isBase64?: boolean // Indicates whether the body is base64 encoded or in plain text
}
JS

Note

  • Failing to send back a response in above-mentioned shape will result in HTTP response that says Malformed response.
  • Functions that are expected to send back a response are allowed to run up to 25 seconds. Actual timeout is slightly less due to internal pipeline fluctuations. Failing to send back a response in time will result in response that says Invocation timeout. Timeout will also be triggered even if you explicitly returned undefined from a function.

  • HTTP response is returned as soon as the function manages to return the response, and not when the actual function invocation finishes. Which means you can send back a response as soon as you need to and then continue with asynchronous function execution. This is useful if you have some cleanup logic or something else to perform in your function that the actual HTTP response does not depend on.

Following example demonstrates this behaviour:

import { HttpEndpointRequest, HttpEndpointResponse, buildPlainTextResponse } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse> {
    setTimeout(() => console.log('Timeout triggered'), 5000);
    return buildPlainTextResponse('Hello World!!!');
}
JS

In this case Hello World!!! is returned with HTTP response before Timeout triggered is printed to console.

Caution!

  • Configuring multiple triggers that send back a response for a the event results in race condition, meaning that the first function that manages to report its response gets sent back with HTTP response, all other function responses will be ignored.

  • Configuring a trigger that does not need to send back a response, with a trigger that does, with the same event name, causes the function timeout that does not need to send back a response to be also limited to 25 seconds.

Incoming request

First argument of the function is a request that triggered the event. You can extract request information from that property, which is in following shape:

{
  body?: any; // Request body, in case the request contains JSON content, the body is already parsed JSON object. In case it is text content then the body is string. In case it is binary then it is in base64. If the body is empty then this property is undefined.
  bodyType?: 'base64' | 'text' | 'json'; // Determines in which shape the body is in. If the body is empty then this propety is also undefined.
  headers: Record<string, string>; // Headers that were sent with the request.
  method: string; // HTTP request method.
  queryStringParams: Record<string, string>; // Record of query string params, if there were any.
  sourceIp: string; // IP of the user/system that triggered the event. Can be used to implement IP whitelisting, as long as your're not worried about IP spoofing.
}
JS

Body type mapping

Following logic is used to determine the body and bodyType:

  • If the content type is application/json then bodyType is json and body is parsed JSON object, no need to do another JSON.parse on it.

  • If the content type is either text/plain, text/html, text/xml or application/xhtml+xml then the bodyType is text and the body is string.

  • For anything else the bodyType is base64 and the body is base64 encoded string, in which case you have to convert base64 to your desired format, when needed. You can use @avst-lib/convert utility library to perform various conversions.

Note

You should explicitly check for body type and not assume the body type to be always determined by the same logic, since we will be improving mapping logic over time.

Detecting and working with various body types

@avst-lib/http-endpoint exports following functions to detect and implicitly infer body types:

  • isBase64

  • isJSON

  • isText

You obviously don’t have to rely on these functions and can build your own detection and conversion logic.

Following example demonstrates how to work with these functions:

import { HttpEndpointRequest, HttpEndpointResponse, buildPlainTextResponse, isBase64, isJSON, isText } from '@avst-lib/http-endpoint';
import { base64ToText } from '@avst-lib/convert';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse | void> {
    if (isBase64(request)) { // Check if body is base64 encoded
        // Convert base64 to text
        const body = base64ToText(request.body);
    } else if (isText(request)) { // Check if body is string
        // Body is already string, no need for explicit conversion
        const body = request.body;
    } else if (isJSON<MyType>(request)) { // Check if body is already parsed JSON object and optionally specify the type you wish it to be in
        // Body is already object that in a shape of MyType, no need for explicit conversion
        const body = request.body;
    } else {
        // If none of the aboce checks were true, this should never happen
    }
}

// Shape of my JSON object type
interface MyType {
    prop1: string;
    prop2: number;
    prop3: boolean;
}
JS

Building common HTTP responses

@avst-lib/http-endpoint exports following functions to build various HTTP responses:

  • buildPlainTextResponse

  • buildJSONResponse

  • buildHTMLResponse

You obviously don’t have to rely on these functions and can build your own response manually.

Check the examples below to laern how to work with these functions.

Examples

Sending back random number as plain text

This example generates a random number (between 0 and 1) and sends it back as plain text. This example demonstrates how to send back plain text response using convenience function to build the response.

import { HttpEndpointRequest, HttpEndpointResponse, buildPlainTextResponse } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse | void> {
    return buildPlainTextResponse(Math.random());
}
JS

Sending back random kitten image

This example fetches random kitten image from third party site and send it back. This example demonstrates how to send back binary data without using a convenience function to build the response.

import { HttpEndpointRequest, HttpEndpointResponse } from '@avst-lib/http-endpoint';
import { bufferToBase64 } from '@avst-lib/convert';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse | void> {
    return {
        status: 200,
        headers: {
            'Content-Type': 'image/jpeg'
        },
        body: bufferToBase64(await getRandomKittenImage()),
        isBase64: true
    };
}

async function getRandomKittenImage() {
    const dimension = (Math.round(Math.random() * 10) * 100) + 300;
    console.log(`Getting kitten image with dimensions: ${dimension}`);

    const response = await fetch(`https://placekitten.com/${dimension}/${dimension}/`);
    if (response.ok) {
        return await response.arrayBuffer();
    } else {
        throw Error(`Unexpected response code: ${response.status}`);
    }
}
JS

Sending back Jira users as JSON

This example fetches Jira Cloud users list, picks out 4 fields from user object, and sends back the list as JSON. This example demonstrates how to send back JSON data using convenience function to build the response.

import JiraCloud from './connections/elrond5';
import { HttpEndpointRequest, HttpEndpointResponse, buildJSONResponse } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse | void> {
    const users = await JiraCloud.User.getUsers();
    const filteredUsers = users.map(({ displayName, emailAddress, active, accountId }) => ({
        displayName,
        emailAddress,
        active,
        accountId
    }));
    return buildJSONResponse(filteredUsers);
}
JS

Sending back Jira users as HTML + search capability

This example fetches Jira Cloud users list, picks out 4 fields from user object, and sends it back as HTML content. Additionally it also provides filtering option. This example demonstrates how to send back HTML data and how to build simple interactive GUI application.

import JiraCloud from './connections/elrond5';
import { HttpEndpointRequest, HttpEndpointResponse, buildHTMLResponse } from '@avst-lib/http-endpoint';

export async function run(request: HttpEndpointRequest, context: Context): Promise<HttpEndpointResponse | void> {
    const users = await JiraCloud.User.getUsers({});
    let filteredUsers = users.map(({ displayName, emailAddress, accountId, active }) => ({
        displayName,
        emailAddress,
        accountId,
        active
    } as User));

    const searchTerm = request.queryStringParams.search ?? '';
    console.log(`Search term: ${searchTerm}`);

    // If search is requested
    if (searchTerm) {
        filteredUsers = filteredUsers.filter(u => u.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
    }

    return buildHTMLResponse(getHtml(filteredUsers, searchTerm));
}

function getHtml(users: User[], searchTerm: string) {
    return `
        <form>
            <label for="search">Search by name:</label>
            <input type="text" id="search" name="search" value="${searchTerm}">
            <input type="Submit" value="Search">
        </form>
        <table>
            <tr>
                <td>Display Name</td>
                <td>Email</td>
                <td>Account ID</td>
                <td>Active</td>
            </tr>
            ${users.map(getUserRow).join('\n')}
        </table>
    `;
}

function getUserRow({displayName, emailAddress, accountId, active}: User) {
    return `
        <tr>
            <td>${displayName}</td>
            <td>${emailAddress ?? ''}</td>
            <td>${accountId}</td>
            <td>${active}</td>
        </tr>
    `;
}

interface User {
    displayName: string;
    emailAddress?: string;
    accountId: string;
    active: boolean;
}
JS