Tutorial: Build a Serverless API backend for Slack [part 2]
Tags: serverless, cloud, java, showdev
Published: 2019-09-04 13:40:40

In the [previous blog](https://dev.to/azure/funcy-a-serverless-slack-app-using-azure-functions-4m84), we explored how to deploy a serverless backend for a Slack slash command on to [Azure Functions](https://azure.microsoft.com/en-in/services/functions/?wt.mc_id=servsept_devto-blog-abhishgu). As promised, it's time to walk through the code to see how the function was implemented. The code is [available on GitHub](https://github.com/abhirockzz/funcy-azure-functions/) for you to grok.

> If you are interested in learning Serverless development with [Azure Functions](https://azure.microsoft.com/en-in/services/functions/?wt.mc_id=servsept_devto-blog-abhishgu), simply [create a free Azure account](https://azure.microsoft.com/en-us/free/?wt.mc_id=servsept_devto-blog-abhishgu) and get started! I would highly recommend checking out the [quickstart guides, tutorials and code samples](https://docs.microsoft.com/en-in/azure/azure-functions/?wt.mc_id=servsept_devto-blog-abhishgu) in the documentation, make use of the [guided learning path](https://docs.microsoft.com/en-in/learn/paths/create-serverless-applications/?wt.mc_id=servsept_devto-blog-abhishgu) in case that's your style or download the [Serverless Computing Cookbook](https://azure.microsoft.com/en-in/resources/azure-serverless-computing-cookbook/?wt.mc_id=servsept_devto-blog-abhishgu).



### Structure

The code structure is as follows:

- Function logic resides in the `Funcy` class (package `com.abhirockzz.funcy`)
- and, the rest of the stuff includes model classes aka POJOs - packages `com.abhirockzz.funcy.model.giphy` and `com.abhirockzz.funcy.model.slack` for GIPHY and Slack respectively

### Function entry point

The `handleSlackSlashCommand` method defines the entry point for our function. It is decorated with the `@FunctionName` annotation (and also defines the function name - `funcy`)

    @FunctionName(funcy)
    public HttpResponseMessage handleSlackSlashCommand(
            @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context)

It is triggered via a HTTP request (from Slack). This is defined by the `@HttpTrigger` annotation.

    @HttpTrigger(name = "req", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS)

Note that the function:

- responds to HTTP `POST`
- does not need explicit authorization for invocation

> For details, check out the [Functions Runtime Java API](https://docs.microsoft.com/java/api/overview/azure/functions/runtime?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu) documentation
- [`@FunctionName`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.annotation.functionname?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu)
- [`@HttpTrigger`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.annotation.httptrigger?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu)
- [`HttpMethod (Enum)`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.httpmethod?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu)


#### Method parameters

The `handleSlackSlashCommand` consists of two parameters - `HttpRequestMessage` and `ExecutionContext`.

`HttpRequestMessage` is a helper class in the Azure Functions Java library. It's instance is injected at runtime and is used to access HTTP message body and headers (sent by Slack). 

`ExecutionContext` provides a hook into the functions runtime - in this case, it's used to get a handle to `java.​util.​logging.Logger`.

> For details, check out the [Functions Runtime Java API](https://docs.microsoft.com/java/api/overview/azure/functions/runtime?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu) documentation
- [`HttpRequestMessage`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.httprequestmessage?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu)
- [`ExecutionContext`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.executioncontext?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu)

### From Slack Slash Command to a GIPHY GIF - core logic

From an end user perspective, the journey is short and sweet! But here is a gist of what's happening behind the scenes.

The Slack request is of type `application/x-www-form-urlencoded`, and is first decoded and then converted to a Java `Map` for ease of use.

        Map<String, String> slackdataMap = new HashMap<>();
        try {
            decodedSlackData = URLDecoder.decode(slackData, "UTF-8");
        } catch (Exception ex) {
            LOGGER.severe("Unable to decode data sent by Slack - " + ex.getMessage());
            return errorResponse;
        }

        for (String kv : decodedSlackData.split("&")) {
            try {
                slackdataMap.put(kv.split("=")[0], kv.split("=")[1]);
            } catch (Exception e) {
                /*
                probably because some value in blank - most likely 'text' (if user does not send keyword with slash command).
                skip that and continue processing other attrbiutes in slack data
                 */
            }
        }

> see https://api.slack.com/slash-commands#app_command_handling

GIPHY API key and Slack Signing Secret are mandatory for the function to work. These are expected via environment variables - we fail fast if they don't exist.

        String signingSecret = System.getenv("SLACK_SIGNING_SECRET");

        if (signingSecret == null) {
            LOGGER.severe("SLACK_SIGNING_SECRET environment variable has not been configured");
            return errorResponse;
        }
        String apiKey = System.getenv("GIPHY_API_KEY");

        if (apiKey == null) {
            LOGGER.severe("GIPHY_API_KEY environment variable has not been configured");
            return errorResponse;
        }

#### Signature validation

Every Slack HTTP request sent to our function includes a signature in the `X-Slack-Signature` HTTP header. It is created by a combination of the the body of the request and the Slack application signing secret using a standard `HMAC-SHA256` keyed hash.

The function also calculates a signature (based on [the recipe](https://api.slack.com/docs/verifying-requests-from-slack#a_recipe_for_security)) and confirms that it the same as the one sent by Slack - else we do not proceed. This is done in the `matchSignature` method


    private static boolean matchSignature(String signingSecret, String slackSigningBaseString, String slackSignature) {
        boolean result;

        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(signingSecret.getBytes(), "HmacSHA256"));
            byte[] hash = mac.doFinal(slackSigningBaseString.getBytes());
            String hexSignature = DatatypeConverter.printHexBinary(hash);
            result = ("v0=" + hexSignature.toLowerCase()).equals(slackSignature);
        } catch (Exception e) {
            LOGGER.severe("Signature matching issue " + e.getMessage());
            result = false;
        }
        return result;
    }

Once the signatures match, we can be confident that our function was indeed invoked by Slack.

#### Invoking the GIPHY API

The [GIPHY Random API](https://developers.giphy.com/docs/) is invoked with the search criteria (keyword) sent by user (along with the Slash command) e.g. `/funcy cat` - `cat` is the keyword. This taken care of by the `getRandomGiphyImage` and is a simple HTTP GET using the [Apache HTTPClient library](https://hc.apache.org/). The JSON response is marshalled into a `GiphyRandomAPIGetResponse` POJO using [Jackson](https://github.com/FasterXML/jackson).

Note that the `org.apache.http.impl.client.CloseableHttpClient` (`HTTP_CLIENT` in the below snippet) object is created lazily and there is only one instance per function i.e. a new object is not created every time a function is invoked.

> For details, please refer to [this section in the Azure Functions How-to guide](https://docs.microsoft.com/azure/azure-functions/manage-connections?WT.mc_id=servsept_devto-blog-abhishgu#static-clients)

    ....
    private static CloseableHttpClient HTTP_CLIENT = null;
    ....
    private static String getRandomGiphyImage(String searchTerm, String apiKey) throws IOException {
        String giphyResponse = null;
        if (HTTP_CLIENT == null) {
            HTTP_CLIENT = HttpClients.createDefault();
            LOGGER.info("Instantiated new HTTP client");
        }
        String giphyURL = "http://api.giphy.com/v1/gifs/random?tag=" + searchTerm + "&api_key=" + apiKey;
        LOGGER.info("Invoking GIPHY endpoint - " + giphyURL);

        HttpGet giphyGETRequest = new HttpGet(giphyURL);
        CloseableHttpResponse response = HTTP_CLIENT.execute(giphyGETRequest);

        giphyResponse = EntityUtils.toString(response.getEntity());

        return giphyResponse;
    }
    ....
    GiphyRandomAPIGetResponse giphyModel = MAPPER.readValue(giphyResponse, GiphyRandomAPIGetResponse.class);

#### Sending the response back to Slack

We extract the required information - `title` of the image (e.g. cat thanksgiving GIF) and its `URL` (e.g. https://media2.giphy.com/media/v2CaxWLFw4a5y/giphy-downsized.gif). This is used to create an instance of `SlackSlashCommandResponse` POJO which represents the [JSON payload returned to Slack](https://api.slack.com/slash-commands#responding_immediate_response). 

        String title = giphyModel.getData().getTitle();
        String imageURL = giphyModel.getData().getImages().getDownsized().getUrl();

        SlackSlashCommandResponse slackResponse = new SlackSlashCommandResponse();
        slackResponse.setText(SLACK_RESPONSE_STATIC_TEXT);

        Attachment attachment = new Attachment();
        attachment.setImageUrl(imageURL);
        attachment.setText(title);

        slackResponse.setAttachments(Arrays.asList(attachment));

Finally, we create an instance of `HttpResponseMessage` (using a fluent builder API). The function runtime takes care of converting the POJO into a valid JSON payload which Slack expects.

    return request.createResponseBuilder(HttpStatus.OK).header("Content-type", "application/json").body(slackResponse).build();


> For details on the [`HttpResponseMessage`](https://docs.microsoft.com/java/api/com.microsoft.azure.functions.httpresponsemessage?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu) interface, check out the [Functions Runtime Java API](https://docs.microsoft.com/java/api/overview/azure/functions/runtime?view=azure-java-stable&WT.mc_id=servsept_devto-blog-abhishgu) documentation

Before we wrap up, let's go through the possible error scenarios and how they are handled. 

### Error handling

#### General errors

As per [Slack recommendation](https://api.slack.com/slash-commands#responding_with_errors), exceptions in the code are handled and a retry response is returned to the user. The `SlackSlashCommandErrorResponse` POJO represents the JSON payload which requests the user to retry the operation and is returned in the form of a `HttpResponseMessage` object. 

    return request.createResponseBuilder(HttpStatus.OK).header("Content-type", "application/json").body(slackResponse).build();

> The user gets a `Sorry, that didn't work. Please try again.` message in Slack

#### User error

It is possible that the user does not include a search criteria (keyword) along with the Slash command e.g. `/funcy`. In this case, the user gets an explicit response in Slack - `Please include a keyword with your slash command e.g. /funcy cat`. 

To make this possible, the request data (`Map`) is checked for the presence of a specific attribute (`text`) - if not, we can be certain that the user did not send the search keyword.

        ....
        HttpResponseMessage missingKeywordResponse = request
                .createResponseBuilder(HttpStatus.OK)
                .header("Content-type", "application/json")
                .body(new SlackSlashCommandErrorResponse("ephemeral", "Please include a keyword with your slash command e.g. /funcy cat")).build();
       ....
       if (!slackdataMap.containsKey("text")) {
            return missingKeywordResponse;
        }
       ....

#### Timeouts

If the call times out (which will if the function takes > than 3000 ms), Slack will automatically send an explicit response - `Darn - that slash command didn't work (error message: Timeout was reached). Manage the command at funcy-slack-app.`

### Resources

The below mentioned resources were leveraged specifically for developing the demo app presented in this blog post, so you're likely to find them useful as well!

- [Azure Functions developers guide (general)](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference/?wt.mc_id=servsept_devto-blog-abhishgu) and the [Azure Functions Java developer guide](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-java/?wt.mc_id=servsept_devto-blog-abhishgu)
- [Quickstart on how to use Java to create and publish a function to Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven/?wt.mc_id=servsept_devto-blog-abhishgu)
- [Maven Plugin for Azure Functions](https://docs.microsoft.com/en-us/java/api/overview/azure/maven/azure-functions-maven-plugin/readme?view=azure-java-stable/?wt.mc_id=servsept_devto-blog-abhishgu)
- [How to code and test Azure Functions locally](https://docs.microsoft.com/en-us/azure/azure-functions/functions-develop-local/?wt.mc_id=servsept_devto-blog-abhishgu)
- [How to manage connections in Azure Functions](https://docs.microsoft.com/en-us/azure/azure-functions/manage-connections/?wt.mc_id=servsept_devto-blog-abhishgu)
- [Maven Plugins for Azure Services on GitHub](https://github.com/microsoft/azure-maven-plugins)

I really hope you enjoyed and learned something from this article! Please like and follow if you did. Happy to get feedback via [@abhi_tweeter](https://twitter.com/abhi_tweeter) or just drop a comment.