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:
- 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 - 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 - 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:
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
Links
- Reference for underlying HTTP API: https://nodejs.org/api/http.html
- API for request: https://nodejs.org/api/http.html#http_class_http_clientrequest
- API for response: https://nodejs.org/api/http.html#http_class_http_serverresponse
- Spec for status codes: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- Spec for status code 401: https://tools.ietf.org/html/rfc7235#section-3.1
- Spec for status code 403: https://tools.ietf.org/html/rfc7231#section-6.5.3
- Spec for status code 405: https://tools.ietf.org/html/rfc7231#section-6.5.5
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.`
};
Hi Carlos Roggan,
I am curious why you believe calling serverless function from http client is 'probably not necessary'? Do you think it's not one of serverless function's major use case?
Thanks,Guoping
Hi Guoping,
correct, probably the most useful serverless scenario would be if function is triggered by an event, asynchronously.
In an HTTP scenario, you would probably have an app somewhere, and you would place your endpoint there.
But anyways, it is possible, in case you know that your endpoint is not frequently used
Kind Regards,
Carlos
Hi Carlos,
Thanks for the great series. Having used serverless with other cloud providers, there are certain things i look for but am having trouble finding.
I would like to add some external dependencies to my package.json to (for example be able to access a database )
The package.json is read only and empty, which is a bit counter intuitive. And i can't seem to find where to add my dependencies.
Any ideas?
Thanks in advance
Hi David Sooter ,
you're right, it is read only in the UI, for technical reasons, but you can work locally with your FaaS project as described here:
https://blogs.sap.com/2020/06/24/writing-function-as-a-service-2-local-development/
and here:
https://blogs.sap.com/2020/06/29/writing-function-as-a-service-3-real-local-development/
Locally, you can (almost) work like in any other node project and add dependencies etc.
Hope this helps,
Cheers,
Carlos