Skip to main content

How to query a REST API effectively

Having the possibility to query APIs for information is an important feature in a flexible and modern infrastructure. Halon MTA has been designed to be able to consume data from external sources (eg. HTTP) from its scripting language and then react upon them, that can be eg.

  • Recipient filtering
  • SASL Authentication
  • Customer settings (in terms of email/antispam/blocklist behavior)
  • Routing information

When you depend on having data fetched from an API it's important to build a resilient system that handles all kinds on unexpected behaviors such as API's being down, API's being down during cache expiration, API's giving faulty responses, API's giving slow responses, concurrent cache misses etc. All this can easily be addressed and handled with our scripting language. Below follows a caching example with a longterm cache implemented.

The combination of json_decode(), http() and the cache's update_function allows us to create very fast and resilient API call functions. The example below uses a normal (short) cache for performance, and a "long term" cache for resilience, in case the API is down. It uses the rate() function to determine if the API takes too long to answer, in order to ensure high performance even if the API is slow. If more than 10 requests during the last 5 minutes took more than 1 second to finish, or it fails to verify the response with is_array(), the value in the long-term cache is returned instead. In other words; the code below assumes that the result is always a JSON array.

src/files/modules/api.hsl
// API call function
function api_call($query, $args)
{
$apiurl = "http://api.example.com/halon?api-key=Zu97uDyS9QeufBId";
$httpoption = ["timeout" => 5];

return cache [
"ttl_function" => function($result) {
if (!is_array($result))
return 60; /* cache short time */
return 3600;
},
"size" => 100000,
"update_function" => function($old, $new) {
if (is_array($new)) /* expect array from API */
return $new;
cache [] syslog("crit", "api_call: Invalid response, using long-term cache");
return $old;
}
] timed_call(http, $apiurl.$query, $httpoption, $args);
}

// The timed call (helper) function, it expects the execution to take less than 1 sec
// if it exceeds that for more than 10 times in the last 5 minutes, it should
// return an empty response.
function timed_call($function, ...$args)
{
if (rate("timed_call:" . gethostname(), string($function), 0, 300) >= 10)
return; // call was slow in the past 5 minutes

$t1 = executiontime();
$ret = $function(...$args); // call the function
$time = executiontime() - $t1;

if ($time > 1)
rate("timed_call:" . gethostname(), string($function), 10, 300);
return json_decode($ret);
}