Please note that there is an update to this blog post. Read it here: https://www.raymondcamden.com/2017/07/03/designing-an-openwhisk-action-for-web-action-support-take-two/.

Before I begin - a few words of caution. The feature I'm discussing today is - for the most part - bleeding edge for OpenWhisk. It is really new and most likely will change between now and when it is "formally" part of the platform. Secondly, what I built may not actually be the best idea either. Regular readers know that I'll often share code that is fun, but not exactly practical, so this isn't anything new per se, but I want to point out that what I demonstrate here may not be a good idea. I'm still extremely new to Serverless in general, so read with caution!

Alright, so first off, a quick reminder. "Web Actions" are a new feature of OpenWhisk that allow you to return non-JSON responses from OpenWhisk actions. There are docs you can, and should, read to understand the basics as well as examples (here is my post which links to even more examples) of it in use.

One thing kinda bothered me though. It wasn't very clear how I could take a simple action and make it support web action results as well as "normal" serverless requests. Most of the demos assume you are only using it as a web action, but I wanted to see if I could support both. Here is what I came up with.

I began by writing my action. In this case, the action generates a random cat. It creates random names, gender, ages, and more.


function getRandomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
 
function randomName() {
    var initialParts = ["Fluffy","Scruffy","King","Queen","Emperor","Lord","Hairy","Smelly","Most Exalted Knight","Crazy","Silly","Dumb","Brave","Sir","Fatty"];
    var lastParts = ["Sam","Smoe","Elvira","Jacob","Lynn","Fufflepants the III","Squarehead","Redshirt","Titan","Kitten Zombie","Dumpster Fire","Butterfly Wings","Unicorn Rider"];
    return initialParts[getRandomInt(0, initialParts.length-1)] + ' ' + lastParts[getRandomInt(0, lastParts.length-1)]
};
 
function randomColor() {
    var colors = ["Red","Blue","Green","Yellow","Rainbow","White","Black","Invisible"];
    return colors[getRandomInt(0, colors.length-1)];
}
 
function randomGender() {
    var genders = ["Male","Female"];
    return genders[getRandomInt(0, genders.length-1)];
}
 
function randomAge() {
    return getRandomInt(1, 15);
}
 
function randomBreed() {
    var breeds = ["American Shorthair","Abyssinian","American Curl","American Wirehair","Bengal","Chartreux","Devon Rex","Maine Coon","Manx","Persian","Siamese"];
    return breeds[getRandomInt(0, breeds.length-1)];
}

function randomPic() {
    var w = getRandomInt(100,400);
    var h = getRandomInt(100,400);
    return `http://placekitten.com/${w}/${h}`;
}

function main(args) {
   return {
        cat:{
            name:randomName(),
            color:randomColor(),
            gender:randomGender(),
            age:randomAge(),
            breed:randomBreed(),
            picture:randomPic()
        }
    };
}

I assume this is all pretty trivial, but as always, let me know in the comments if it doesn't make sense. So I took this, pushed it up to OpenWhisk, and confirmed it work as a 'regular' simple action.

Demo

With that done, I then began thinking about how I'd support web actions. First I enabled it by updating the action: wsk action update caas cat.js --annotation web-export true.

I thought I'd first start with - how do I tell a "regular" call versus a "Web Action" call. According to the docs, a "Web Action" call will send along additional arguments representing the HTTP request. These exist in args with the prefix __ow. So with that, I wrote this function as a shorthand to detect the request:


function isWebInvocation(request) {
	if(request.__ow_meta_verb) return true;
	return false;
}

This feels a bit risky, but if a better way comes around, I can fix it up later.

The next bit was figuring out exactly how I'd support web actions. I decided on 3 different ways.

  • If an HTML request is made, I'd return an HTML string representing the cat.
  • If a JPG request is made, I'd return just the image.
  • If a JSON response is requested, I'd return the JSON string.

You can determine all of this by looking at the headers sent with the request. Web Actions pass this as __ow_meta_headers and the accept header would tell me what kind of response was desired. I wrote this as a shorthand way of parsing it:


function getRequestType(request) {
	//basically sniff the Accept header
	let header = request.__ow_meta_headers.accept;
	console.log('header', header);
	//try to normalize a bit, just caring about a few
	if(header.indexOf('text/html') >= 0) return 'html';
	if(header.indexOf('image/jpeg') >= 0) return 'image';
	if(header.indexOf('application/json') >= 0) return 'json';
}

Notice I have no result for an unknown request - I should fix that. So the final part was to just support all this. Here is my modified main function.


function main(args) {
	console.log('cats args: '+JSON.stringify(args));

	let cat = {
		name:randomName(),
		color:randomColor(),
		gender:randomGender(),
		age:randomAge(),
		breed:randomBreed(),
		picture:randomPic()
	};

	if(!isWebInvocation(args)) {
		return { cat: cat};
	}

	console.log('not a regular invoke');
	let type = getRequestType(args);
	console.log('type is '+type);

	switch(type) {

		case 'html': 
			return {html:`
				<h1>${cat.name}</h1>
				My cat, ${cat.name}, is ${cat.gender} and ${cat.age} years old.<br/>
				It is a ${cat.breed} and has a ${cat.color} color.<br/>
				<img src="${cat.picture}">
				`
			};
			break;


		case 'image':
			return {
				headers:{location:cat.picture},
				code:302
			};
			break;

		case 'json':
			return cat;
			break;
	
	}

}

So how do I test? First I figured out my URL (I added a space to make it wrap nicer):

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/ rcamden@us.ibm.com_My%20Space/default/caas

And then I tested the HTML version:

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/ rcamden@us.ibm.com_My%20Space/default/caas.html

which worked fine:

To test the JSON and JPG versions, I used Postman, which is a damn handy tool for testing various API calls. Yes, I could have done everything in Curl, but honestly, my memory for CLI args is somewhat flakey, and I love the UI and ease of use of Postman in general. To support an image response, I use this URL:

https://openwhisk.ng.bluemix.net/api/v1/experimental/web/ rcamden@us.ibm.com_My%20Space/default/caas.http

and here it is running in Postman:

And the JSON works well too - it uses the same URL, just a different Accept header.

So - as I said at the beginning - I'm not entirely sure this is a good idea, but I dig how my one action can have multiple views of the same data. Any thoughts on this approach?