Skip to Content
Technical Articles

Writing Function-as-a-Service [11]: How and Why access HTTP API

This blog is part of a series of tutorials explaining how to write serverless functions using the Functions-as-a-Service offering in SAP Cloud Platform Serverless Runtime

Quicklinks:
Quick Guide
Sample Code

Introduction

In this blog we’re talking about functions with HTTP trigger only. These are functions that are invoked with HTTP request.
In such scenarios, the function returns a value which ends up in the response body of the calling client (e.g. browser)
Sometimes, the function handler code needs more information that it gets from the FaaS runtime
Also, it may need to write custom info to the response

For these cases, the FaaS runtime allows access to the underlying native node.js HTTP API
For those of you who require such functionality, I’m providing a silly example, to make your life easier

First of all, 2 basic info that we have to understand:

  1. In most cases, access to the underlying HTTP API is not needed.
    As such, if we need it, we have to enable it
    That’s done in faas.json, for each function definition
  2. The underlying HTTP API is the standard node.js http module
    As such, no special tutorial needed here, please refer to the standard documentation
    Here: https://nodejs.org/api/http.html
  3. And there’s one more basic info
    We cannot mix both approaches
    Either use the standard FaaS convenience methods OR use the HTTP API
    With “FaaS convenience methods” I mean the following
    Use return value to set the response:
    return ‘Error, invalid function invocation’
    Use convenience method to set the status
    event.setUnauthorized()
    With other words: if we want to add custom header to the response, we cannot use event.setUnauthorized() to set the status. We have to set the status with response.writeHead(400)

Overview

Prerequisites

If you’re new to Serverless Runtime, Function-as-a-Service, you should check out the overview of blogs and read all of them…

How to use HTTP API

First of all, the access to the API needs to be enabled.
This is done in a function definition in  faas.json

"functions": {
   "function-with-httpapi": {
      "httpApi": true

Once enabled, the http property of event will be filled with request and response objects

module.exports = async function (event, context) {
    const request = event.http.request
    const response = event.http.response

The request object represents the class http.ClientRequest 
and the response object the http.ServerResponse

And that’s it about accessing the HTTP API

Example use cases

Now let’s see why and how we might need to use it

A few example use cases where we need access to HTTP API:

  • HTTP method
  • Query parameters
  • Request Headers
  • Trigger name
  • Response Headers
  • Response Body
  • Response Status

HTTP method

Sometimes we need to know with which HTTP verb we’ve been invoked. For instance, if our function should distinguish between GET or POST, etc
We can throw an error, or react accordingly

const httpMethod = request.method
if (httpMethod != 'POST'){
    response.writeHead(405, {
        'Content-Type': 'text/plain',
        'Allow': 'POST'
    });
    response.write('Request failed. Method not allowed. See response headers for hint');
    response.end();

Query parameters

Query parameters are the params that can be added to a URL after the ?
Multiple parameters are appended with & 
E.g.
xxxxx?customerName=otto&userid=123

They’re accessed via the query property

const name = event.http.request.query.customerName

It returns the value of the param

Request Headers

We can find the headers sent with the request in the headers property

request.headers

Trigger name

Two interesting headers are those provided by the FaaS runtime, giving info about the used trigger

const triggerName = request.headers['sap-faas-http-trigger-name']
const triggerPath = request.headers['sap-faas-http-trigger-path'] 

What is the difference?
/
Yes, the slash makes the path
In our example, the trigger name is customlogin and the path  customlogin/

Response Headers

In case of response object, we’re interested in modifying it.
For instance, we might want to send some additional information in the response header, as it might be required by the caller of our function
To add own headers to the response:

response.set('MyHeader', 'MyValue')

response.append('CustomHeader', 'CustomValue')

Alternatively, set multiple headers and status at once

response.writeHead(403, {
    'Content-Type': 'text/plain',
    'FailureHint': 'Authorization required, see docu'
});

Response Body

In most cases, we set the response body using the standard way in FaaS: as return value
However, we might need to switch to http API, due to requirements of caller who might need some custom text in the response body in case of failure, etc
If this is the case we can use standard way of http.ServerResponse class

response.write('Error. Don't ask admin. Don't see log for no info')

Note:
As mentioned, we cannot mix the used APIs.
If we set a header in the response, then we have to use this way of writing response

Response Status

There can be several reasons why we might wish to set the response status code.
For instance, if we don’t support all the HTTP methods, then we have to set the response status to 405, which means Method not allowed
Also, we might have special requirements to the incoming call, so we would have to decide on our own to set the status code to 400, Bad Request
Please see Links section for reference

Setting the response status code to a custom value can be again done in several ways

E.g. directly setting the property value:

response.statusCode = 405

Or again using the convenience method, where we can set a custom header at the same time:

response.writeHead(405, {
   'Content-Type': 'text/plain',

Create sample Function

To make things less theoretical, you can find here a reusable sample project, which is meant to showcase how the HTTP API can be used.
As usual, it is a small silly sample, without real use case, but focusing on demonstrating some capabilities for your convenience, so you can easily copy&paste and adapt for your own needs

Please refer to the Appendix for the full sample code

Note:
In case that it doesn’t suit your needs, you can send me a personal message

In this example, we’re simulating a kind of strange special login process for a customer app
The user of the function is required to pass a couple of pieces of login information:

Customer Name as query param
Customer Password as request header
Authorization scope as body in a POST request
As such, only POST is supported
In addition, our function simulates usage of multiple endpoints and only one of them is meant for productive usage with login
As such, the function code has to access request header to determine the used trigger

Code walkthrough

Our function does nothing than accessing the HTTP API and using it for some login-checks
In case of success, it does nothing, just return some silly text

Preparation

First we declare the usage of the HTTP API in faas.json

"functions": {
   "function-with-httpapi": {
      "httpApi": true

Then we can access the HTTP API in function code.
Otherwise, the object would be empty

const request = event.http.request
const response = event.http.response

Now, in our function, we can implement custom checks which wouldn’t be possible without the HTTP API.
Our function contains 4 checks:

1. Check for correct HTTP verb:

const httpMethod = request.method
if (httpMethod != 'POST'){
    response.writeHead(405, {
        'Content-Type': 'text/plain',
        'Allow': 'POST'
    });
    response.write('Request failed. Method not allowed. See response headers for hint');

2. Check for desired productive endpoint

In faas.json we’ve defined 2 triggers.
The only reason for the second one (nologin) is to be able to run this check

const triggerName = request.headers['sap-faas-http-trigger-name']
if(triggerName != 'customlogin'){
    response.statusCode = 400

3. Check for authentication

Note that we don’t verify the customer name, only password – for the sake of simplicity
We need to access the request header

const customerAuth = request.headers['customer-auth']
if(! customerAuth){
    response.writeHead(401, {
        'Content-Type': 'text/plain',
        'FailureHint': 'Required: request header customer-auth containing customer password'
    });

4. Check scope

We use this to show how to access the request body

if((! request.body) || (JSON.parse(request.body).customerAccess != 'true')){
    response.writeHead(403, {
        'FailureHint': 'Authorization required...'
        

Success response

If the request has passed all checks, then we do nothing,
Just note that here we’re using the standard way of using a function:
The response body is sent as return value of the function
When I mentioned earlier that we cannot mix the HTTP API with standard API, I meant we cannot mix in one response definition. But here, in case of success, we don’t do any custom header setting, etc, so we can just return a text

const customerName = request.query.customerName 
return `Function called successfully. Customer '${customerName}' is welcome.`

Run the sample Function

Let’s deploy and run the sample action, to see our HTTP API implementation in action
After deploy, we want to see if your checks are executed properly and if we get the expected results in response body , status and header

➡️1. Wrong HTTP verb

In our first example, we choose to use a wrong HTTP method, to see our first check working

Request
URL https://…faas…/customlogin/
Verb GET
Header
Body
Response
Body Error text
Status 405
Header allow

See result:

 

➡️2. Wrong endpoint

Now we use the correct HTTP method, but the wrong trigger

Request
URL https://…faas…/nologin/
Verb POST
Header
Body
Response
Body Error text
Status 400
Header

See result:

➡️3. Missing authentication

Next try: correct endpoint, but we don’t send the required authentication data

Request
URL https://…faas…/customlogin/
Verb POST
Header
Body
Response
Body Error text
Status 401
Header failurehint

See result:

➡️4. Missing authorization

We send a request with mostly correct settings, only the scope, to be passed in the request body, is  missing

Request
URL https://…faas…/customlogin/
Verb POST
Header customer-auth : abc123
Body
Response
Body Error text
Status 403
Header failurehint

See result:

➡️5. Successful call

Finally we send a request with correct settings, including the name parameter in the URL

Request
URL https://…faas…/customlogin/?customerName=otto
Verb POST
Header customer-auth : abc123
Body { “customerAccess” : “true” }
Response
Body Success text
Status 200
Header

See result:

Summary

In this tutorial, we’ve learned which steps are required to access the HTTP API
This is probably not necessary in most use cases of serverless function
But sometimes it required, so we need to know how it works

We’ve learned that we need to enable it first in faas.json
Then we can access it via the event.http
property
And we’ve understood that we can use the standard node.js http methods 

Quick Guide

faas.json:

"my-function": {
   "httpApi": true

Function code:

const request = event.http.request
const response = event.http.response

Appendix: All Project Files

Here you can find the project files used for the sample, ready for copy&paste

faas.json

{
  "project": "httpapiproject",
  "version": "1",
  "runtime": "nodejs10",
  "library": "./src",
  "functions": {
    "function-with-httpapi": {
      "module": "mymodule.js",
      "httpApi": true
    }
  },
  "triggers": {
    "customlogin": {
      "type": "HTTP",
      "function": "function-with-httpapi"
    },
    "nologin": {
      "type": "HTTP",
      "function": "function-with-httpapi"
    }
  }
}

package.json

Adding dev dependency for local testing

{
  "devDependencies": {
    "@sap/faas": ">=0.7.0"
  }
}

mymodule.js

module.exports = async function (event, context) {

    // to access HTTP API, add param to function definition in faas.json: "httpApi": true
	const request = event.http.request
	const response = event.http.response
    
    // check request method
    const httpMethod = request.method
    if (httpMethod != 'POST'){
        response.writeHead(405, {
            'Content-Type': 'text/plain',
            'Allow': 'POST'
        });
        response.write('Request failed. Method not allowed. See response headers for hint');
        response.end();
        return
    }

	// check which HTTP trigger was used
    const triggerName = request.headers['sap-faas-http-trigger-name']
    if(triggerName != 'customlogin'){
        response.statusCode = 400
        response.write("Endpoint not supported. Use 'customlogin' endpoint")
        response.end();
        return
    }

	// check authentication via request header
    const customerAuth = request.headers['customer-auth']
    if(! customerAuth){
        response.writeHead(401, {
            'Content-Type': 'text/plain',
            'FailureHint': 'Required: request header customer-auth containing customer password'
        });
        response.write('Request failed. Unauthorized. Customer login data missing. See hint for more info');
        response.end();
        return
    }

    // check authorization via request body 
    if((! request.body) || (JSON.parse(request.body).customerAccess != 'true')){
        response.writeHead(403, {
            'Content-Type': 'text/plain',
            'FailureHint': 'Authorization for customer required: Request body with customer JSON access must be true'
        });
        response.write('Request failed. Forbidden. Customer authorization not sufficient. See hint for more info');
        response.end();
        return    
    }

    // use standard way of writing response
    const customerName = request.query.customerName 
    return `Function called successfully. Customer '${customerName}' is welcome.`
};
Be the first to leave a comment
You must be Logged on to comment or reply to a post.