All posts by Rob

Calling an OpenWhisk action in Swift

As OpenWhisk is a Functions as a Service system, it makes sense to create actions that do one thing and call other actions when they want other work done. As an example, in DrinksChooser, the choose action calls the incrementDrinkCount action which increments the count of the recommended drink in Redis. This way, choose doesn't have to know anything about Redis as that's not its job.

In OpenWhisk's Swift environment, there's the Whisk.invoke() method to do this. This is how we do it.

The action's name

To invoke an action we need it's fully qualified name. This is your OpenWhisk namespace concatenated with the action's name, including it's package name if it has one.

Let's start with the namespace:

Conveniently, the namespace is held in an environment variable called __OW_NAMESPACE. In Swift, we can retrieve environment variables from the ProcessInfo.processInfo.environment which will return an Optional String. As we're lazy, we convert the Optional to a concrete String using ?? "". In a proper application, we'd implement Swift's error handling and do it properly.

Note that the namespace doesn't start with a leading /, but our fully qualified action name does, so we create our action name like this:

The action's name is incrementDrinkCount and it's in the DC package, so we add those in to create our action name.

Invoke the action

Invoking the action is easy enough:

We call Whisk.invoke() with our action name and a dictionary of parameters if we have any. In our case, we pass in the name of the drink who's count we want to increment.

The action is executed and the result is returned as a dictionary of type [String:Any].

Data returned from Whisk.invoke()

You get a dictionary back from Whisk.invoke() with lots of interesting information:

The interesting information is in the response dictionary and the first thing to check is the success key which is a boolean and and so is either true or false:

As you can see, we have to downcast a lot

As the type of the dictionary is [String:Any], we have to downcast all the time!

To make this easier, we can use the SwiftyJSON library which handles the casting for us:

This becomes even more useful as we delve deeper into a nested dictionary!

Passing secrets to your OpenWhisk action

There is only one way to pass data into your OpenWhisk action: parameters. Your function receives a dictionary of parameters and returns one. Let's explore how you can set these parameters.

Parameters passed to the action

You can see this by creating this action:

test.swift

Add the action to OpenWhisk with: wsk action update params params.swift -a web-export true and test:

If you use a Web Action (unauthenticated HTTP requests), then OpenWhisk will add some additional parameters related to the request:

From now on, I'll skip the __ow_ parameters from the output I display as they are just noise in the context of this article.

Default parameters

You can also set default parameters that are always passed to an action. This is done when you update or create an action by using the -p flag:

We have now added the default parameter foo to our list of args as you can see:

We can therefore use default parameters to pass secrets such as Redis credentials to our action which is a very useful thing to be able to do! You can also add default parameters to packages using the same -p flag which is really useful when we have a group of actions that need the same credentials.

Overriding default parmeters

One problem however is that the defaults can be overridden:

Sometimes, this could be really useful behaviour, for example the canonical hello world action could have a default paramter of name set to world which is then overridden when calling the action. However, if the default parameters contain credentials or other operational data, then we don't want them overridden as it lead to some unexpected behaviour.

The "final" annotation

To prevent overriding of default parameters, we use the final annotation.

OpenWhisk actions can be annotated using the -a flag when creating and updating. We've already used the web-export annotation to inform OpenWhisk that this action is accessible to the WebActions system and so can be called without authorisation.

The final annotation makes all the parameters that are already defined on the action immutable. This means that they cannot be overridden.

To do this, we pass in -a final true when we update/create our action:

When we now call our action, we get an error:

Problem solved!

There's only one caveat. As of the time of writing, if you invoke the action using the authenticated API (i.e. via the wsk tool or a POST request with the Authorisation header), then the final annotation is ignored. I understand that this will change in the future.

Fin

The argument dictionary that you receive into your action is the only way to pass credential data into the action and so the best way to do this is with default parameters on the action or package and then setting the final annotation to true.

You may also want to consider using a JSON dictionary for all your config data, so that there's only one parameter in the argument's list that will clash with anything passed in by the action's callers.

Quick tip: OpenWhisk autocompletion

I've just discovered how to enable Bash autocompletion for the wsk command line tool!

$ cd /usr/local/bin
$ wsk sdk install bashauto

This will create a file called wsk_cli_bash_completion.sh in your /usr/local/bin directory.

Now, source this file within your .bash_profile or equivalent:

$ echo -e "\n# OpenWhisk autocompletion\nsource ~/bin/wsk_cli_bash_completion.sh" >> ~/.bash_profile

Start a new terminal window, (or source ~/.bash_profile in your current one) and you can now press the tab key after typing wsk to see the available options.

Error handling in OpenWhisk actions

With a standard OpenWhisk action, we return a dictionary of the data and let OpenWhisk deal with converting it to JSON etc. OpenWhisk will also set the correct 200 status code.

How do we handle an error though?

It turns out that if there is a key called "error" in our returned dictionary, then all the other data is ignored and an error is sent back.

To show this, consider this action:

If we call it from the command line:

For more details, we can look at the full response by leaving out the --result parameter:

A lot of data is returned, but we're only interested in the response section.

  • result contains the returned data. Note that all other keys are stripped, so only error remains.
  • success is a boolean and is only set to true if the action executed and returned a dictionary that didn't have an "error" key.
  • status is a string that can be one of:
    • "success": everything is okay (status is true)
    • "application error": Action ran, but there was an error that was handled by OpenWhisk (status is false)
    • "action developer error": A container error occurred (e.g. failed to start action) (status is false)
    • "whisk internal error": An internal system error occurred (status is false)

If we access the action via the API Gateway, then we also get the same output:

This gives us the same error message, but the HTTP status is 502 and we can't change it (yet?!).

Web Actions behave differently though:

However, we can control the HTTP status code and message with a web action as I've discussed, by sending back a dictionary with code, headers and body keys.

As a result, I think that the additional control that Web Actions give you make it compelling to use as the external interface to your actions (i.e. anything that a web hook calls).

Whatever you do, don't use a key called error with a Web Action!

Using ngrok to test on a mobile

To test a website that you're developing on your local computer on a mobile device such as a phone or tablet use ngrok.

This is the way to do it:

  1. Start up ngrok: $ ngrok http my-dev-site.local:80
    This will start up ngrok and give you a "Forwarding" URL such as http://24f55bf5.ngrok.io.
    In this case, it will direct all traffic to that URL to http://my-dev-site.local. If you run
    your website on a different port, such as http://localhost:8888, then use $ ngrok http 8080
    instead.
  2. If your website's base url is configured in a config file, then update it to be the Forwarding URL.
  3. Go to the Forwarding URL (http://24f55bf5.ngrok.io, in this example) on your mobile and test!

It's not hard to do this, but this will save me having to look it up next time!

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 more useful today.

Enabling Web Actions

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

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

The fully qualified name for your action can be found using ask 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 annotate the action with the web-export key. This is done using the --annotation switch to ask action update:

$ wsk action update P1/ping ping.swift --annotation web-export 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/experimental/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: code, headers and body:

code

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>",
        "code": 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/experimental/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,
        "code": 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/experimental/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"
}

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
}

Run it as POST request, with a very simple JSON payload:

$ wsk action update P1/args args.swift -a web-export true
ok: updated action P1/args
$ curl -H "Content-type: application/json" -d '{"foo":"bar"}' \
  https://openwhisk.ng.bluemix.net/api/v1/experimental/web/19FT_dev/P1/args.json"
{
  "__ow_meta_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_meta_path": "",
  "__ow_meta_verb": "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_meta_verb tells us the METHOD of the HTTP message that the client sent and __ow_meta_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.

Serverless Swift on OpenWhisk

I'm interested in serverless computing and as I write Swift, the OpenWhisk platform comes up high when you Google. This turns out to be a really good choice as OpenWhisk is Open Source so I can read the source code (and have done!). In principle, I can also run my own instance of it if I need to to for regulatory reasons, or just to avoid vendor lock-in.

Commercially, the whole point of Serverless (aka Functions as a Service) is that it deal with everything infrastructure related other than the function I am writing, and so I actually host my OpenWhisk functions with IBM's Bluemix.

In a serverless environment we write separate functions that are event driven. Each function can even be in a different language as they are all independent. Also, our functions are stateless which, as an API person, I'm comfortable with. There's more than one way to trigger a function, but I've started with the simplest: an HTTP request.

Far more information is in the docs and I can't recommend the OpenWhisk-Team Slack channel enough; very helpful people on there.

This is my intro post on getting going with OpenWhisk mainly so that all the info I need is in one place!

Notes on Getting Started

There's plenty of blog posts about getting started with OpenWhisk on Bluemix, so this is mostly an aide-memoir for myself as I had to go back on a couple of things that I didn't understand the first time.

Create a Bluemix account:

  • Log into your Bluemix account or create one.
  • OpenWhisk is only provisioned in the US South region
  • Make a note of your organisation and space, you'll need them later!
    (If you're setting up Bluemix for the first time, call your first space "dev")

Set up OpenWhisk:

You should now have a working wsk command line tool. wsk is remarkably helpful. Add -h and it'll give you help.

First Swift action

As OpenWhisk is serverless, we have a single entry function to an action. In Swift has this signature:

This means that we receive a dictionary of arguments (which are called parameters elsewhere in OpenWhisk) and must return a dictionary. (In Swift, a dictionary is what is called an associative array or hash in other languages.). The returned dictionary is the data returned to the caller.

First action

To create an action, we need a swift source file. This can have any name, but my general rule of thumb is to name it the same as the action name. As OpenWhisk looks quite APIish, we'll create a "ping" action and so our file is called ping.swift

ping.swift

Packages

Always put your actions in packages. These work a little like namespaces in that you can group together related actions, triggers, rules and what not. You can also attach default parameters to packages that are then available to every action which can be very useful. Interestingly, you can "import" a namespace into another one (known as binding) and when you do so, you can override the parameter values for that package for this specific binding.

To create a package called "P1":

Upload to OpenWhisk

To upload your function to OpenWhisk:

(You can also use create in place of update, but as update will create the action if it doesn't
exist, you may as well just always use update.)

Viewing all actions

To view your actions, list them:

In this case, I have one action, list. It's fully qualified name is "/19FT_dev/P1/ping" and as it's part of a private package, it's private and written in Swift 3. The language information is provided as OpenWhisk supports Java, NodeJS and Python actions in addition to Swift.

Running the action

There are many ways to run the action. The first way is to use the wsk tool's action invoke command:

The blocking parameter tells the command to wait until the action completes before returning. If you leave it out, then the action is invoked, but you don't get the result as it's in "fire and forget" mode.

Alternatively, you can use curl.

To do this, you make a POST request to https://openwhisk.ng.bluemix.net/api/v1/namespaces/{NAMESPACE}/actions/{ACTION}?blocking=true with your API key in the Authorization header. As Basic Auth require the credentials to be Base64 encoded, the easiest way to get the information in the right format is:

You also need your namespace, which has the format of {organisation name}_{space name} as you can see in the fully qualified action name in the output of wsk action list. In my case, this is: 19FT_dev.

We can then use this with our curl command:

As you can see, you get a lot of info back, but the key bit is in the response -> result property:

As you don't want to share your API key with anyone, there are other ways to call this action via HTTP: Web Action and API Gateway. We'll explore these in a separate post.

Something wrong? Viewing the logs

If something goes wrong, the place to look is the logs. To get an ongoing up to date view, open a new terminal window and run this in it:

The works a lot like tail -f. Invoke your action and you'll see the information for it.

Alternatively, to view the last log, read LornaJane's "One-Line Command For Newest OpenWhisk Logs" article.

The command you need is:

That's a bit of mouthful, so put it in a script or alias it.

Fin

That's it. Getting started with Swift actions on OpenWhisk is remarkably easy and lots of fun. If you want to poke around a more fully featured app, have a look at my DrinkChooser project.

Keyboard shortcut to resize Finder columns

I like to use Finder in Column mode (⌘+3). i.e. this view:

Column view

One feature of this view is that you can resize all the columns to fit by alt+double clicking on the move handle between each column. There doesn't appear to be a keyboard shortcut for this operation though, so I created one using Keyboard Maestro.

Keyboard Maestro can move the mouse around the screen and click with it which is exactly what I need. There's a "Click at Found Image" action which seemed like it was just what I needed. I took a screenshot of the handle and set up the action to look for it in the topmost window and then discovered that Click at Found Image fails if it finds more than one image. Most of the time there's at least two columns visible in my Finder windows and so this was never going to work. I needed a different solution.

The solution was to use the "For Each" action iterates over a collection and one of the choices for the collection is found images. I have set up this KM macro:

Resize finder columns

The image I am looking for is dragged into the image well and set to be found in the front window, searched from left to right. Once the image is found, a list of actions can be run. In this case, I double click with the alt modifier slightly to the right and down a bit from the top left hand corner of my screenshot of the handle, which automatically right-sizes all the columns in the window for me. As I only need this done one, I break the loop after double clicking the first handle.

Finally, I assigned it to the keyboard shortcut ⌥⌘R so that I can run it whenever I need to.

Right-sized column view

Stand-alone usage of Zend-InputFilter

Any data that you receive needs to be checked and validated. There are number of ways to do this including PHP's filter_var, but I prefer Zend-InputFilter. This is how to use it as a stand-alone component.

Installation

Firstly, we install it using Composer:

$ composer require zendframework/zend-inputfilter
$ composer require zendframework/zend-servicemanager

You don't have to have ServiceManager, but it makes working with InputFilter much easier, so it's worth installing.

Create the InputFilter

The easiest way to create an InputFilter is to use the provided Factory class. Let's consider an Author entity that has the properties: author_id, name, biography & date_of_birth. We can create an input filter like this:

use Zend\InputFilter\Factory as InputFilterFactory;

class Author
{
    protected $author_id;
    protected $name;
    protected $biography;
    protected $date_of_birth;

    // ...

    protected function createInputFilter()
    {
        $factory = new InputFilterFactory();
        $inputFilter = $factory->createInputFilter([
            'author_id' => [
                'required' => true,
                'validators' => [
                    ['name' => 'Uuid'],
                ],
            ],
            'name' => [
                'required' => true,
                'filters' => [
                    ['name' => 'StringTrim'],
                    ['name' => 'StripTags'],
                ],
            ],
            'biography' => [
                'required' => false,
                'filters' => [
                    ['name' => 'StringTrim'],
                    ['name' => 'StripTags'],
                ],
            ],
            'date_of_birth' => [
                'required' => false,
                'validators' => [
                    ['name' => 'Date'],
                    [
                        'name' => 'LessThan',
                        'options' => [
                            'max' => date('Y-m-d'),
                            'inclusive' => true,
                        ],
                    ],
                ],
            ],
        ]);

        return $inputFilter;
    }
}

The createInputFilter() method takes an associative array where the key is the name of the input and then the value is a specification. There are a number of elements in the specification, but we usually just specify required, filters and validators.

required This can be either true or false. If false, then the validators do not execute, but the filters do.
filters An optional array of Zend-Filters. A filter modifies the supplied data before it is passed to the validators (if any). The filtered data is used by the rest of the application. In this example, we have added two filters: StringTrim & StripTags.
validators An optional array of Zend-Validators. A validator will test the filtered value for the input and fail if the data is not valid. If any validator fails, then the entire InputFilter is invalid.

This particular input filter requires that author_id and name are present, but that biography and date_of_birth are optional. The author_id must be a UUID, the name & biography must not have leading or trailing whitespace or no HTML tags and the date_of_birth, if present, must be a valid date in the past.

Using the InputFilter

To use the InputFilter, we set the data and then call isValid(). This can be done in a validate() method that looks like this:

Use Crell\ApiProblem\ApiProblem;
use Error\Exception\ProblemException;

Class Author
{
    // ...

    /**
     * Create an author
     *
     * @param  array $data
     * @return Author
     * @throws ProblemException
     */
    public static function createAuthor($data)
    {
        $inputFilter = $this->createInputFilter();
        $inputFilter->setData($data);

        if ($inputFilter->isValid()) {
            return new Author($inputFilter->getValues());
        }

        $problem = new ApiProblem('Validation failed');
        $problem->setStatus(400);
        $problem['errors'] = $inputFilter->getMessages();

        throw new ProblemException($problem);
    }
}

In this case, it's an API, so the data has come from a PUT or POST request. We call setData() to pass the array of data into the InputFilter and then call isValid(). If the data is valid, we can return a newly instantiated Author object that is constructed with the filter data. If the validation fails, then we throw a ProblemException which needs an ApiProblem instance, so we create one for it.

To find out which validators failed, getMessages() provides a nested array which is very useful for passing back to the API client.

As an example, this is what failure looks like:

$ curl -i -X "POST" "http://localhost:8888/authors" \
     -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d $'{ "name": "", "author_id": "1234" }'

HTTP/1.1 400 Bad Request
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+json

{
    "errors": {
        "author_id": {
            "valueNotUuid": "Invalid UUID format"
        },
        "name": {
            "isEmpty": "Value is required and can't be empty"
        }
    },
    "title": "Validation failed",
    "type": "about:blank",
    "status": 400
}

Fin

That's all there is to it. Zend-InputFilter is a very flexible data filter and validator and works really well for APIs, such as those written in Slim.

Rendering problem details in Slim

As I've already noted, in the project I'm currently building, I'm rendering errors in my Slim Framework API using RFC 7807: Problem Details for HTTP APIs via Larry Garfield's ApiProblem component and rka-content-type-renderer.

One place where we need to integrate this approach into Slim is in the error handlers. Let's look at NotFound. To ensure that we return a response in the right format, we need to implement our own NotFound handler:

src/App/Handler/NotFound.php:

<?php
namespace App\Handler;

use Crell\ApiProblem\ApiProblem;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class NotFound
{
    /**
     * Invoke not found handler
     *
     * @param  ServerRequestInterface $request  The most recent Request object
     * @param  ResponseInterface      $response The most recent Response object
     *
     * @return ResponseInterface
     */
    public function __invoke(ServerRequestInterface $request, ResponseInterface $response)
    {
        $problem = new ApiProblem(
            'Not Found',
            'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html'
        );
        $problem->setStatus(404);

        $renderer = new RKA\ContentTypeRenderer\ApiProblemRenderer();
        return $renderer->render($request, $response, $problem);
    }
}

The NotFound handler must return a Response. To do this, we create a new ApiProblem object with the title "Not Found" and set the type to HTML page that defines the 404 status code. We then instantiate the ApiProblemRenderer and call its render method. The renderer will then return a Response object in either XML or JSON based on the Accept header, with the correct Content-Type header. We then set the status code and return it.

To register our new handler, we use the container. If you're using the skeleton application as your base, then this goes in dependencies.php:

src/dependencies.php:

// Error handlers
$container['notFoundHandler'] = function () {
    return new App\Handler\NotFound();
};

Slim will now use our handler whenever a NotFoundException is raised.

This is it in action:

$ curl -i -H "Accept: application/xml" http://localhost:8888/foo
HTTP/1.1 404 Not Found
Host: localhost:8888
Connection: close
X-Powered-By: PHP/7.0.14
Content-type: application/problem+xml

<?xml version="1.0"?>
<problem>
  <title>Not Found</title>
  <type>http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html</type>
  <status>404</status>
</problem>

We can of course apply this to the other error handers: NotAllowed, Error & PhpError which all follow the same pattern as we have done for NotFound.