Pragmatism in the real world

CORS and OpenWhisk web actions

By default, OpenWhisk will handle the relevant responses for CORS.

Specifically, it will respond to an OPTIONS request with these headers:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH
Access-Control-Allow-Headers: Authorization, Content-Type

If you need to change what is sent or you don’t want to send these headers at all, then you need to do set the annotation web-custom-options to true and handle the OPTIONS header yourself.

Note that if you don’t set this annotation, you must not set any of these headers yourself as you’ll break things!

Implementing the CORS headers yourself

To implement the CORS headers yourself you do something like this in your action (example in Swift, PHP version at the end):

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

	let corsAllowedOrigin = "http://example.com" // URI that may access this resource ("*" is wildcard)

    // OPTIONS handling for CORS
    guard
        let method = args["__ow_method"] as? String,
        let headers = args["__ow_headers"] as? [String:Any]
    else {
        return ["statusCode": 500]
    }
    if method == "options" {
        let allowedHeaders = headers["access-control-request-headers"] ?? "Content-Type, Authorization"
        return [
            "statusCode": 200,
            "headers": [
                "Access-Control-Allow-Methods": "OPTIONS, GET, PUT, DELETE",
                "Access-Control-Allow-Origin": corsAllowedOrigin,
                "Access-Control-Allow-Headers": allowedHeaders
            ]
        ]
    }

    let data = ["hello": "world"]

    return [
        "statusCode": 200,
        "headers": [
             "Content-Type": "application/json",
             "Access-Control-Allow-Origin": corsAllowedOrigin, // don't forget this header!
         ],
        "body": Data(WhiskJsonUtils.dictionaryToJsonString(jsonDict: data)!.utf8).base64EncodedString()
    ]
}

Create it it OpenWhisk with:

wsk action create api api.swift --web true -a web-custom-options true

For a “non-simple” request, the browser will send an OPTIONS request to the option that you need to respond to. This is call the “preflight” request.

The __ow_method in args will tell you the method (lowercased) and __ow_headers will contain all the headers that were sent.

If the method is options, then we need to return the CORS headers. You can expect that the client will send a Access-Control-Request-Headers header containing the list of headers that its going to send to you when it does the actual request. If you don’t have a white list of headers that you’re expecting, then you should send back the list you are sent as otherwise the request will fail.

You also need to set the Access-Control-Allow-Methods header to the list the methods you accept for this end point; it is the CORS version of the Allow header. The client should send you a Access-Control-Request-Method header to tell you the method it will use when it sends the actual request.

If the method is not OPTIONS, then you send back your response as usual. In addition, you need to send the Access-Control-Allow-Origin header with the same value as you used for the preflight request.

That’s it.

Aside: PHP version

As an aside, here’s how it looks in PHP, where the type handling is looser:

<?php

function main(array $args) : array
{
    $corsAllowedOrigin = "http://example.com"; // URI that may access this resource ("*" is wildcard)

    // OPTIONS handling for CORS
    $method = $args["__ow_method"] ?? "";
    $headers = $args["__ow_headers"] ?? [];
    if ($method == "options") {
        $allowedHeaders = $headers["access-control-request-headers"] ?? "Content-Type, Authorization";
        return [
            "statusCode" => 200,
            "headers" => [
                "Access-Control-Allow-Methods" => "OPTIONS, GET, PUT, DELETE",
                "Access-Control-Allow-Origin" => $corsAllowedOrigin,
                "Access-Control-Allow-Headers" => $allowedHeaders
            ]
        ];
    }

    $data = ["hello" => "world"];

    return [
        "statusCode" => 200,
        "headers" => [
             "Content-Type" => "application/json",
             "Access-Control-Allow-Origin" => $corsAllowedOrigin, // don't forget this header!
         ],
        "body" => base64_encode(json_encode($data))
    ];
}

The PHP version operates in the same manner as the Swift one, so the discussion above applies to this code too. The most obvious differences are the lack of a guard section do handle the type & optional casting from the args dictionary and the ease of converting an array to a base64 string in PHP.