James Thomas

Notes on software.

OpenWhisk Web Action Errors With Sequences

This week, I came across an interesting problem when building HTTP APIs on IBM Cloud Functions.

How can Apache OpenWhisk Web Actions, implemented using action sequences, handle application errors that need the sequence to stop processing and a custom HTTP response to be returned?

This came from wanting to add custom HTTP authentication to existing Web Actions. I had decided to enhance existing Web Actions with authentication using action sequences. This would combine a new action for authentication validation with the existing API route handlers.

When the HTTP authentication is valid, the authentication action becomes a ”no-op”, which passes along the HTTP request to the route handler action to process as normal.

But what happens when authentication fails?

The authentication action needs to stop request processing and return a HTTP 401 response immediately.

Does Apache OpenWhisk even support this?

Fortunately, it does (phew) and I eventually worked out how to do this (based on a combination of re-reading documentation, the platform source code and just trying stuff out!).

Before explaining how to return custom HTTP responses using web action errors in sequences, let’s review web actions, actions sequences and why developers often use them together…

Web Actions

Web Actions are OpenWhisk actions that can be invoked using external HTTP requests.

Incoming HTTP requests are provided as event parameters. HTTP responses are controlled using attributes (statusCode, body, headers) in the action result.

Web Actions can be invoked directly, using the platform API, or connected to API Gateway endpoints.

example

Here is an example Web Action that returns a static HTML page.

1
2
3
4
5
6
7
8
9
function main() {
  return {
    headers: {
      'Content-Type': 'text/html'
    },
    statusCode: 200,
    body: '<html><body><h3>hello</h3></body></html>'
  }
}

exposing web actions

Web actions can be exported from any existing action by setting an annotation.

This is handled automatically by CLI using the —web configuration flag when creating or updating actions.

1
wsk action create ACTION_NAME ACTION_CODE --web true

Action Sequences

Multiple actions can be composed together into a “meta-action” using sequences.

Sequence configuration defines a series of existing actions to be called sequentially upon invocation. Actions connected in sequences can use different runtimes and even be sequences themselves.

1
wsk action create mySequence --sequence action_a,action_b,action_c

Input events are passed to the first action in the sequence. Action results from each action in the sequence are passed to the next action in the sequence. The response from the last action in the sequence is returned as the action result.

example

Here is a sequence (mySequence) composed of three actions (action_a, action_b, action_c).

1
wsk action create mySequence --sequence action_a,action_b,action_c

Invoking mySequence will invoke action_a with the input parameters. action_b will be invoked with the result from action_a. action_c will be invoked with the result from action_b. The result returned by action_c will be returned as the sequence result.

Web Actions from Action Sequences

Using Action Sequences as Web Actions is a useful pattern for externalising common HTTP request and response processing tasks into separate serverless functions.

These common actions can be included in multiple Web Actions, rather than manually duplicating the same boilerplate code in each HTTP route action. This is similar to the ”middleware” pattern used by lots of common web application frameworks.

Web Actions using this approach are easier to test, maintain and allows API handlers to implement core business logic rather than lots of duplicate boilerplate code.

authentication example

In my application, new authenticated web actions were composed of two actions (check_auth and the API route handler, e.g. route_handler).

Here is an outline of the check_auth function in Node.js.

1
2
3
4
5
6
7
8
9
10
11
const check_auth = (params) => {
  const headers = params.__ow_headers
  const auth = headers['authorization']

  if (!is_auth_valid(auth)) {
    // stop sequence processing and return HTTP 401?
  }

  // ...else pass along request to next sequence action
  return params
}

The check_auth function will inspect the HTTP request and validate the authorisation token. If the token is valid, the function returns the input parameters untouched, which leads the platform the invoke the route_handler to generate the HTTP response from the API route.

But what happens if the authentication is invalid?

The check_auth action needs to return a HTTP 401 response immediately, rather than proceeding to the route_handler action.

handling errors - synchronous results

Sequence actions can stop sequence processing by returning an error. Action errors are indicated by action results which include an “error” property or return rejected promises (for asynchronous results). Upon detecting an error, the platform will return the error result as the sequence action response.

If check_auth returns an error upon authentication failures, sequence processing can be halted, but how to control the HTTP response?

Error responses can also control the HTTP response, using the same properties (statusCode, headers and body) as a successful invocation result, with one difference: those properties must be the children of the error property rather than top-level properties.

This example shows the error result needed to generate an immediate HTTP 401 response.

1
2
3
4
5
6
{
   "error": {
      "statusCode": 401,
      "body": "Authentication credentials are invalid."
    }
}

In Node.js, this can be returned using a synchronous result as shown here.

1
2
3
4
5
6
7
8
9
10
11
const check_auth = (params) => {
  const headers = params.__ow_headers
  const auth = headers['authorization']

  if (!is_auth_valid(auth)) {
    const response = { statusCode: 401, body: "Authentication credentials are invalid." }
    return { error: response }
  }

  return params
}

handling errors - using promises

If a rejected Promise is used to return an error from an asynchronous operation, the promise result needs to contain the HTTP response properties as top-level properties, rather than under an error parent. This is because the Node.js runtime automatically serialises the promise value to an error property on the activation result.

1
2
3
4
5
6
7
8
9
10
11
const check_auth = (params) => {
  const headers = params.__ow_headers
  const auth = headers['authorization']

  if (!is_auth_valid(auth)) {
    const response = { statusCode: 401, body: "Authentication credentials are invalid." }
    return Promise.reject(response)
  }

  return params
}

conclusion

Creating web actions from sequences is a novel way to implement the “HTTP middleware” pattern on serverless platforms. Surrounding route handlers with pre-HTTP request modifier actions for common tasks, allows route handlers to remove boilerplate code and focus on the core business logic.

In my application, I wanted to use this pattern was being used for custom HTTP authentication validation.

When the HTTP request contains the correct credentials, the request is passed along unmodified. When the credentials are invalid, the action needs to stop sequence processing and return a HTTP 401 response.

Working out how to do this wasn’t immediately obvious from the documentation. HTTP response parameters need to included under the error property for synchronous results. I have now opened a PR to improve the project documentation about this.

Comments