Pragmatism in the real world

OpenWhisk web actions

The first way that you learn to call your OpenWhisk action over HTTP is a POST request that is authenticated using your API key. This key allows all sorts of write access to your account, so you never release it.

If you want to access the action over HTTP without the API key, you have two choices: Web Actions or API Gateway.

This article discusses how to use Web Actions as they are very useful and easy to get going with.

Enabling Web Actions

Web Actions provide an HTTP endpoint to your action. The format of the URL is:

https://openwhisk.ng.bluemix.net/api/v1/web/{fully qualified action name}.{type}

The fully qualified name for your action can be found using wsk action list. For my ping action, this is /19FT_dev/P1/ping.

Note that if your action is in the default package, e.g. it’s name is /19FT_dev/hello, then you need to use /19FT_dev/default/hello.

The type is one of: http, json, text

Enable access

To enable an action for web access, you need to inform OpenWhisk by using the --web switch to wsk action update (or wsk action create):

$ wsk action update P1/ping ping.swift --web true

That’s all that we need to do, so we can now test it:

Calling via Curl

$ curl -i \
  https://openwhisk.ng.bluemix.net/api/v1/web/19FT_dev/P1/ping.json

HTTP/1.1 200 OK
X-Backside-Transport: OK OK
Connection: Keep-Alive
Transfer-Encoding: chunked
Server: nginx/1.11.1
Date: Sun, 26 Feb 2017 17:50:24 GMT
Content-Type: application/json; charset=UTF-8
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type
X-Global-Transaction-ID: 441895695
Set-Cookie: DPJSESSIONID=PBC5YS:1376290542; Path=/; Domain=.whisk.ng.bluemix.net

{
  "ack": "2017-03-02 17:50:24"
}

Success! As we used the .json extension, OpenWhisk automatically set the status code to 200, sent our returned dictionary as JSON and set the correct Content-Type header. What about if we need to change the status code though?

Sending data to the client

To set your own status code and HTTP headers, you need to use the .http extension on the URL and change the data array that you return from your action.

To recap from my previous post, our action currently looks like this:

ping.swift

func main(args: [String:Any]) -> [String:Any] {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let now = formatter.string(from: Date())

    return ["ack": now]
}

In this action we simply return a dictionary of the data we want to appear as JSON in the body. To use the http web action type, we return a dictionary with three keys: statusCode, headers and body:

statusCode Number of the HTTP status code e.g. 200.
headers Dictionary of HTTP headers to send. The key is the header’s name and the value is the header’s value.
body HTTP body as a string. The format depends on the Content-Type header that you have set. If the format is a binary one, then the string must be base64 encoded. OpenWhisk uses Spray, so consult their list to find out which are considered binary and which are text.

Most importantly, note that JSON content types are considered binary by Spray!

Let’s change our action to send XML:

ping.swift

func main(args: [String:Any]) -> [String:Any] {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let now = formatter.string(from: Date())

    return [
        "body": "<ack>\(now)</ack>",
        "statusCode": 200,
        "headers": [
            "Content-Type": "application/xml",
        ],
    ]
}

(Don’t forget to update your action whenever you change it using wsk action update)

As the XML content types are considered text, we can just set the string containing our XML for the body element of our dictionary.

Let’s test it:

$ curl -i -H "Accept: application/xml" \
  https://openwhisk.ng.bluemix.net/api/v1/web/19FT_dev/P1/ping.http

HTTP/1.1 200 OK
X-Backside-Transport: OK OK
Connection: Keep-Alive
Transfer-Encoding: chunked
Server: nginx/1.11.1
Date: Sun, 26 Feb 2017 19:48:35 GMT
Content-Type: application/xml
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type
X-Global-Transaction-ID: 2289807007
Set-Cookie: DPJSESSIONID=PBC5YS:1376290542; Path=/; Domain=.whisk.ng.bluemix.net

<ack>2017-02-26 19:48:35:lt;/ack>

Note, that if you the Accept header doesn’t contain the content type that is set in your headers, then the Web Action controller will send an error back to the client.

JSON

For application/json, we have to convert our dictionary to a JSON string and then base64 encode it. OpenWhisk’s Swift environment includes SwiftyJSON, so this isn’t too hard:

ping.swift

import SwiftyJSON

func main(args: [String:Any]) -> [String:Any] {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let now = formatter.string(from: Date())

    let data = ["ack": now]

    let json_body = WhiskJsonUtils.dictionaryToJsonString(jsonDict: data) ?? ""
    let base64_body = Data(json_body.utf8).base64EncodedString()

    return [
        "body": base64_body,
        "statusCode": 200,
        "headers": [
            "Content-Type": "application/json",
        ],
    ]
}

OpenWhisk’s Swift envionment includes the WhiskJsonUtils class that has some useful JSON related methods. In our case, we use the dictionaryToJsonString() method to create the JSON string from our dictionary. We then convert this to a base64 encoded string using the Foundation Data class’s base64EncodedString() method.

Proving that it works:

$ curl -i -H "Accept: application/json" \
  https://openwhisk.ng.bluemix.net/api/v1/web/19FT_dev/P1/ping.http 
HTTP/1.1 200 OK
X-Backside-Transport: OK OK
Connection: Keep-Alive
Transfer-Encoding: chunked
Server: nginx/1.11.1
Date: Sun, 26 Feb 2017 20:52:12 GMT
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type
X-Global-Transaction-ID: 891112175
Set-Cookie: DPJSESSIONID=PBC5YS:1376290542; Path=/; Domain=.whisk.ng.bluemix.net

{
  "ack": "2017-02-26 20:52:12"
}

Receiving data & reading headers

We’ve talked about sending data from our action to the HTTP client, but what about receiving data from the client? This information is provided to you in the args parameter that is passed to your main() method. You can inspect args with this code:

env.swift:

func main(args: [String:Any]) -> [String:Any] {
    return args
}

We can send information to the web action using POST with a JSON payload or via query parameters in a GET request.

For a POST request, with a very simple JSON payload:

$ wsk action update P1/args args.swift --web true
ok: updated action P1/args
$ curl -H "Content-type: application/json" -d '{"foo":"bar"}' \
  https://openwhisk.ng.bluemix.net/api/v1/web/19FT_dev/P1/args.json"
{
  "__ow_headers": {
    "accept": "*/*",
    "user-agent": "curl/7.51.0",
    "x-client-ip": "86.138.48.38",
    "x-forwarded-proto": "https",
    "host": "10.155.72.21:10001",
    "content-length": "13",
    "content-type": "application/json",
    "via": "1.1 EwAAAGAW5wA-",
    "x-global-transaction-id": "2130413383",
    "connection": "close",
    "x-forwarded-for": "86.138.48.38"
  },
  "__ow_path": "",
  "__ow_method": "post",
  "foo": "bar"
}

As you can see, our body data ({"foo": "bar}) is just an element in the args dictionary. However, OpenWhisk has also given us some __ow_* properties with useful information. __ow_method tells us the METHOD of the HTTP message that the client sent and __ow_headers contains the HTTP headers. Note that the keys are normalised to lower case.

Armed with this knowledge, it’s possible to ensure that your action works with the HTTP method(s) that you want it to and by reading the headers, you can implement things like authentication via the Authorization header or read the Accept header to ensure you return data in the right format.

Fin

That’s it. OpenWhisk Web Actions provide a very easy way to create HTTP endpoints that can be called by clients via the GET, PUT, POST or DELETE methods without needing your API key.

In comparison to API Gateway, you have less control over the name of the URL, but in exchange you are able to set the HTTP status code and custom headers, which isn’t possible in API Gateway today. Over time, I expect API Gateway to gain more features, but until it supports the ability to set the status code, I recommend Web Actions.

Note: This article was updated on 16 July 2017 as web actions are no longer experimental. This means that the URL non longer has an experimental segment and that the annotation has been promoted to --web

One thought on “OpenWhisk web actions

Comments are closed.