Working with HTTP in Oracle Functions using the Fn Project Python FDK

When working with both the Fn Project and the Oracle Functions service, it's possible to access information about the invocation, function, and execution environment from within a running function - including HTTP properties such as custom HTTP headers and HTTP query parameters.

In this blog post, I'll show you how to work with HTTP requests when building your Fn functions using the Python Function Development Kit (FDK).

Scenario

I was recently working on an IoT implementation where devices had been configured to remotely invoke an Oracle Function in order to retrieve configuration data.

When invoked by an IoT device, the Oracle Function was configured to connect to a downstream system to retrieve configuration data specific to the requesting device, and in-turn respond with a JSON payload.

In order to meet its functional requirement, the Oracle Function (in this case, implemented in Python) required at runtime, access to a custom HTTP header submitted by the requesting device.

Along the lines of the example scenario - accessing information about the invocation, function, and execution environment when working with serverless functions is a typical requirement.

The Fn Project implements a number of features, including Function Development Kits (FDKs) to simplify and standardise the developer experience when working with such data.

About the Fn Project and Oracle Functions

Oracle Functions is a fully managed, highly scalable, on-demand, Functions-as-a-Service (FaaS) platform, built on enterprise-grade Oracle Cloud Infrastructure, and powered by the Fn Project open source engine.

With Oracle Functions, you can deploy your code, call it directly or trigger it in response to events, and get billed only for the resources consumed during the execution.

Oracle Functions are "container-native". This means that each function is a completely self-contained Docker image that is stored in your OCIR Docker Registry and pulled, deployed and invoked when you invoke your function.

Fn Project Function Development Kits

The Fn Project provides Function Development Kits (FDKs) with support for a variety of programming languages, including: Java, Python, Ruby, Node, & Go.

FDKs are designed to abstract away from developers the requirement to interact directly with underlying low-level constructs, or to perform complex work such as protocol framing.

At runtime FDKs execute in three phases, and in the following order:

  • Request: with request data deserialised into a request context and request data
  • Execute: with request context and request data
  • Respond: with response data being rendered into a formatted response

At the time you create an Fn function, you specify a handler, which is a function in your code, that Oracle Functions can invoke when the service executes your code.

The handler accepts a callable object: fdk.handle({callable_object}), and the callable object implements a signature with the format: (context, data).

The following general syntax structure is used when creating a handler function in Python.

def handler(ctx, data):
    ...
    return response

The request data is obtained from the request used to trigger the function. In the case of an HTTP invocation, data is an HTTP request body.

When Oracle Functions invokes your function, it passes a context object ctx to the handler. This object provides methods and properties that provide information about the invocation, function, and execution environment.

FDK Request Context

The following describes the range of data exposed by the attributes of a request context object when working with the Python FDK:

Config
   Class: os._Environ
   Configuration data for the current application and current function.
Headers
   Class: dict
   HTTP headers included with the request submitted to invoke the current function.
AppID
   Class: str
   Unique Oracle Cloud ID (OCID) assigned to the application.
FnID
   Class: str
   Unique Oracle Cloud ID (OCID) assigned to the function.
CallID
   Class: str
   Unique ID assigned to the request.
Format
   Class: str
   The function’s communication format - the interaction protocol between Fn and the function.
Deadline
   Class: str
   How soon function the will be aborted, including timeout date and time.
RequestURL
   Class: str
   Request URL that was used to invoke the current function.
Method
   Class: str
   HTTP method used to invoke the current function.

Example Function

Here's an example of a simple hello world function which will include within the response data the full set of attributes exposed by the request context object:

import io
import json
from fdk import response
 
def handler(ctx, data: io.BytesIO=None):
    name = "World"
    try:
        body = json.loads(data.getvalue())
        name = body.get("name")
    except (Exception, ValueError) as ex:
        print(str(ex))
    return response.Response(
        ctx, response_data=json.dumps(
            {"Message": "Hello {0}".format(name),
            "ctx.Config" : dict(ctx.Config()),
            "ctx.Headers" : ctx.Headers(),
            "ctx.AppID" : ctx.AppID(),
            "ctx.FnID" : ctx.FnID(),
            "ctx.CallID" : ctx.CallID(),
            "ctx.Format" : ctx.Format(),
            "ctx.Deadline" : ctx.Deadline(),
            "ctx.RequestURL": ctx.RequestURL(),
            "ctx.Method": ctx.Method()},
            sort_keys=True, indent=4),
        headers={"Content-Type": "application/json"}
    )

When invoked, the hello world function response data is returned as JSON formatted:

    "Message": "Hello World",
    "ctx.AppID": "ocid1.fnapp.oc1.iad.aaaaaaaaafkiyvdtalsn6aako2i6jttllk7tgaj4v4hgpnccwggd00000000",
    "ctx.CallID": "01E3BCBYFR1BT163GZ00000000",
    "ctx.Config": {
        "FN_APP_ID": "ocid1.fnapp.oc1.iad.aaaaaaaaafkiyvdtalsn6aako2i6jttllk7tgaj4v4hgpnccwggd00000000",
        "FN_CPUS": "100m",
        "FN_FN_ID": "ocid1.fnfunc.oc1.iad.aaaaaaaaadjx7atmmfcbm6ipfw67bykoh2lniadadurqiex2p3d500000000",
        "FN_FORMAT": "http-stream",
        "FN_LISTENER": "unix:/tmp/iofs/lsnr.sock",
        "FN_MEMORY": "256",
        "FN_TYPE": "sync",
        "GPG_KEY": "0D96DF4D4110E5C43FBFB17F2D347EA600000000",
        "HOME": "/home/fn",
        "HOSTNAME": "71c2e839ca59",
        "LANG": "C.UTF-8",
        "OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM": "/.oci-credentials/private.pem",
        "OCI_RESOURCE_PRINCIPAL_REGION": "us-ashburn-1",
        "OCI_RESOURCE_PRINCIPAL_RPST": "/.oci-credentials/rpst",
        "OCI_RESOURCE_PRINCIPAL_VERSION": "2.2",
        "PATH": "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        "PYTHONPATH": "/python",
        "PYTHON_GET_PIP_SHA256": "b86f36cc4345ae87bfd4f10ef6b2dbfa7a872fbff70608a1e43944d283fd0eee",
        "PYTHON_GET_PIP_URL": "https://github.com/pypa/get-pip/raw/ffe826207a010164265d9cc807978e3604d18ca0/get-pip.py",
        "PYTHON_PIP_VERSION": "19.3.1",
        "PYTHON_VERSION": "3.6.9",
        "card": "not_set"
    },
    "ctx.Deadline": "2020-03-14T02:01:58Z",
    "ctx.FnID": "ocid1.fnfunc.oc1.iad.aaaaaaaaadjx7atmmfcbm6ipfw67bykoh2lniadadurqiex2p3d500000000",
    "ctx.Format": "http-stream",
    "ctx.Headers": {
        "accept": "*/*",
        "accept-encoding": "gzip",
        "content-type": "application/octet-stream",
        "date": "Sat, 14 Mar 2020 02:01:03 GMT",
        "fn-call-id": "01E3BCBYFR1BT163GZ00000000",
        "fn-deadline": "2020-03-14T02:01:58Z",
        "fn-http-method": "GET",
        "fn-http-request-url": "/gtc/ml?card=CC50E3CCBFFF",
        "fn-intent": "httprequest",
        "fn-invoke-type": "sync",
        "host": "localhost",
        "oci-subject-id": "ocid1.apigateway.oc1.iad.amaaaaaap7nzmjiajosozyrcbvtwhtqdd4zvlojl4qn4teauugge00000000",
        "oci-subject-tenancy-id": "ocid1.tenancy.oc1..aaaaaaaac3l6hgylozzuh2bxhf3557quavpa2v6675u2kejplzal00000000",
        "oci-subject-type": "resource",
        "opc-request-id": "/7FAB0BCD835B93B731AF16E754880DA2/01E3BCBYD01BT163GZ00000000",
        "orwarded": "for=111.111.111.111",
        "ost": "b65alubkumgiuzhn3400000000.apigateway.us-ashburn-1.oci.customer-oci.com",
        "transfer-encoding": "chunked",
        "user-agent": "curl/7.47.0",
        "x-content-sha256": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZ00000000",
        "x-forwarded-for": "111.111.111.111",
        "x-forwarded-host": "ggd00000000.us-ashburn-1.functions.oci.oraclecloud.com:443",
        "x-forwarded-port": "443",
        "x-forwarded-proto": "https",
        "x-real-ip": "111.111.111.111",
        "x-device-id": "CC50E3CCB000"
    },
    "ctx.Method": "GET",
    "ctx.RequestURL": "/gtc/ml?device-id=CC50E3CCB000"

With the IoT scenario in mind, and in reference to the JSON response data - it's apparent that we're able to obtain the requesting IoT device unique ID from the custom HTTP header x-device-id, which is available via the request context object attribute Headers.

Another option for submitting custom data to the function to be available via the request context object is to include HTTP request parameters within the HTTP request submitted to invoke a function.

HTTP request parameters are exposed by the request context object attribute requestURL. Per the example response data, the IoT device ID was also submitted via HTTP request parameter device-id, and exposed by the request context object attribute requestURL.

There's a wealth of contextual and environmental data exposed by the request context object, and through the context object it's easily available for a developer to work with when constructing functions in Python.

If you're not already working with Oracle Functions on OCI, get started today by heading over to https://www.oracle.com/cloud/free/ to access a free trial, and unlock access to the always free services.

Cover Photo by Steve Johnson on Unsplash.