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.