Creating CloudEDGE Handler Files

Developer instructions for creating CloudEDGE handler files

CloudEDGE workers provide a way to augment your application by modifying the requests that it handles and the responses that it generates without the need to change your existing, deployed application code.

Handlers run your custom JavaScript code in a serverless execution environment that is agnostic to your cloud provider. They provide a uniform development environment and deployment flexibility. Additionally, handlers can run at the CDN edge, closer to your users, which allows for improved response times.

For more information about CloudEDGE Workers, see CloudEDGE Workers Overview.

Topics

Handler files

A CloudEDGE handler is implemented in a handler file. The handler file is structured as a NPM package. It includes your JavaScript source code and all of the third party-dependencies (also known as node modules) needed to run your code. You do not need to publish this package to the official NPM repository.

In addition to the source code and node modules, the handler file must include two files: package.json and manifest.json. These files must exist in the same directory, which is the package’s root directory.

Handler file directory structure

The package.json file describes the dependencies of the source code, the most important of which is the @webscale-networks/cloudedge-handlers node module. This Webscale node module runs your handler in a serverless execution environment once it is deployed. The environment is independent of your cloud provider. The Webscale API will reject handler files that fail to include this dependency in both the dependencies and bundledDependencies sections of their package.json file. Additionally, the @webscale-networks/cloudedge-handlers node module must exist at <root-directory>/node_modules.

The manifest.json file contains metadata about the source code of your handler file. Specifically, it enumerates the handler functions contained within, where their implementations exist, and whether they process requests or responses.

manifest.json file specification

[{
    'moduleName':     a unique module name as a string,
    'relativePath':   
      relative path from package root directory to the handler function source file,
    'functions': [{
        name:         function name as a string,
        type:         one of ‘handleRequest’ or 'handleResponse',
      }]
}]

Example of a manifest.json file

The following sample code is for a single manifest.json file with two handler functions.

[{
    'moduleName':     handlerSource,
    'relativePath':   “src/handler.js”,
    'functions': [
      {
        name:         “addsRequestHeader”,
        type:         “handleRequest”,
      },
      {
        name:         “hijacksResponseBody”,
        type:         “handleResponse”,
      },
    ]
}]

Writing handler functions

Handlers that process requests are called request handlers. Likewise, handlers that process responses are called response handlers. When creating the implementation of a handler function, it is important to decide whether it should process requests or responses as the interfaces are similar but not identical.

To reiterate, a single source file can contain multiple handler functions, mixing request and response handlers. However, a single handler function cannot process both requests and responses. The function that runs depends on the handler function that an end user chooses when they create a handler through the Webscale Control Panel. For general information on working with handlers through the Webscale Control Panel, see CloudEDGE Workers Overview.

CloudEDGE handler interfaces

Depending on what type of handler function you write, the function must accept different arguments.

  • Request handlers must accept a single argument, representing the client’s request.
  • Response handlers must accept two arguments. The first one is the client’s request and the second is the generated response.

Handler functions are not forced to return a result. One example is if the function decides not to modify the wRequest or wResponse object. If the handler function does return a result, it must be an object containing a subset of the following properties: error, request, and response. The error property, if set, should be a JavaScript error.

Request handlers

Request handlers can set the following properties on their returned object.

  • Return a JavaScript error by setting the error property on the returned object. The error will be logged in the cloud provider. The cloud provider’s default error response will be returned to the client.
  • Return a JavaScript error and custom response by setting the error and response properties. The error will be logged in the cloud provider. The response will be returned to the client.
  • Return a custom response by setting the response property
  • Update the request by setting the request property
  • Return nothing

Response handlers

Response handlers can set the following properties on their returned object.

  • Return a JavaScript error by setting the error property on the returned object. The error will be logged in the cloud provider. The cloud provider’s default error response will be returned to the client.
  • Return a JavaScript error and custom response by setting the error and response properties
  • Return a custom or modified response by setting the response property
  • Return nothing

Response handlers cannot return an object with the request property set.

wRequest and wResponse objects

If your handler function processes requests, it will be called with a wRequest object.

If your handler function processes responses, it will be called with a wRequest and wResponse object.

wRequest object


{
  body: {
    data: ...,
    encoding: ...,
    modified: ...,
    truncated: ...,
  },
  headers: ...,
  method: ...,
  path: ...,
  peerAddress: ...,
  query: ...,
  originRequest: {                                
    headers: ...,
    host: ...,
    keepaliveTimeout: ...,
    path: ...,
    port: ...,
    protocol: ...,
    readTimeout: ...,
    sslProtocols: ...,
  },
  providerSpecific: {
    ...,
  }
}

wRequest object properties

Property Description
body.data Content of the request body
body.encoding Encoding of the request body. Valid values are text or base64.
body.modified If your handler modifies the original request body, this property must be set to true
body.truncated Some cloud providers place limits on the size of request bodies. If a request body is larger than this limit, they might truncate the content that is available to your handler. Valid values are true or false.
headers HTTP request headers that the client sent. A header value can be retrieved by calling its get method with the header name. A header value can be set by calling its set method with the header name and value. Header names are case-insensitive.
method HTTP method of the client’s request
originRequest.headers HTTP request headers that will be sent to origin. A header value can be retrieved by calling its get method with the header name. A header value can be set by calling its set method with the header name and value. Header names are case-insensitive.
originRequest.host Hostname used on the request sent to origin
originRequest.keepaliveTimeout Maximum amount of time that the connection to origin will be kept alive (in seconds)
originRequest.path Path to use when retrieving a resource from origin
originRequest.port Port number on which to connect to origin
originRequest.protocol Connection protocol to use when connecting to origin
originRequest.readTimeouts Maximum amount of time to wait for a response from origin (in seconds)
originRequest.sslProtocols Minimum SSL/TLS protocol to use when creating an HTTPS connection with origin, as an array. If multiple protocols are specified, they are tried in order.
path Path from the requested URI. This property can be changed so the HTTP response returns a different resource.
peerAddress IP address of the client from which the request originates
providerSpecific Object containing any additional information included by your cloud provider
query Query string from the requested URI

wResponse object

{
  body: {},
  headers: ...,
  statusCode: ...,
  providerSpecific: {
    ...,
  }
}

wResponse object properties

Property Description
body Not all cloud providers grant access to the response body. If your cloud provider does, this property stores an object with the data and encoding properties. These properties can be updated if they exist and added if they do not. Using the body property, you can inspect or replace the response. The encoding of data must match the value of the encoding property.
headers HTTP response headers that will be sent to the client. A header value can be retrieved by calling its get method with the header name. A header value can be set by calling its set method with the header name and value. Header names are case-insensitive.
providerSpecific An object containing any additional information included by your cloud provider. The properties that are mutable depend on your cloud provider.
statusCode HTTP status code sent to the client. This property can be updated and it must be specified.

AWS property mutability

wRequest

Immutable:

  • peerAddress
  • method
  • Properties in providerSpecific

Mutable:

  • All other fields

wResponse

Immutable:

  • Properties in providerSpecific

Mutable:

  • All other fields

Examples of handler functions

Following are some examples of handler functions.

Request handlers

Generating an error when a specific request header is set

exports.generatesErrorHandler = (wRequest) => {
  if (wRequest.headers.get('webscale-generate-error')) {
    return {
      error: Error('Operator error'),
      response: {
        body: {
          modified: true,
          data: '<html>Generated error</html>',
          encoding: 'text',
        },
        headers.set('content-type', 'text/html'),
        statusCode: 400,
      },
    };
  }
};

Modifying the request query parameters

exports.addQueryHandler = (wRequest) => {
  wRequest.query = "delay=10s";
  return {
    request: wRequest,
  };
};

Adding a request header sent to origin using an asynchronous JavaScript function

exports.addCustomOriginHeader = async (wRequest) => {
  wRequest.originRequest.headers.set(
    'your-header-name',
    'your-header-value',
  );
  return new Promise((resolve, reject) => {
    resolve({
      request: wRequest,
    });
  });
};

Modifying the request body sent to origin using an asynchronous JavaScript function

exports.changeRequestBodyHandlerAsync = async (wRequest) => {
  if (wRequest.method == 'POST') {
    wRequest.headers.set('host', 'ptsv2.com');
    wRequest.originRequest.host = 'ptsv2.com';
    wRequest.originRequest.protocol = 'http';
    wRequest.originRequest.port = 80;
    wRequest.body.modified = true;
    wRequest.body.encoding = 'text';
    wRequest.body.data = 'This is a brand new POST body.';
  }
  return new Promise((resolve, reject) => {
    resolve({
      request: wRequest,
    });
  });
};

Response handlers

Redirecting a response to another page

exports.redirectToWebscale = (wRequest, wResponse) => {
  wResponse.statusCode = 301;
  wResponse.headers.set(
    'location',
    'https://www.webscale.com',
  );
  return {
    response: wResponse,
  };
};

Replacing a response completely

exports.helloWorldResponse = (wRequest, wResponse) => {
  wResponse.body.modified = true;
  wResponse.body.data = '<html>hello world</html>';
  wResponse.body.encoding = 'text';
  wResponse.headers.set('content-type', 'text/html');
  wResponse.headers.set('content-encoding', '');
  return {
    response: wResponse,
  };
};

Hijacking the response to return an error using an asynchronous JavaScript function

exports.responseError = async (wRequest, wResponse) => {
  wResponse.body.modified = true;
  wResponse.body.encoding = 'text';
  wResponse.body.data = '<html>Resource not found</html>';
  wResponse.headers.set('content-type', 'text/html');
  wResponse.headers.set('content-encoding', '');
  wResponse.statusCode = 404;
  return new Promise((resolve, reject) => {
    resolve({
      error: Error('Not found'),
      response: wResponse,
    });
  });
};

Testing handlers

You can write unit tests for your handler using any JavaScript testing framework. Additionally, the @webscale-networks/cloudedge-handlers node module includes a testing framework that will automatically run each handler function defined in your manifest.json file against a collection of tests that enforce the CloudEDGE handler interfaces and specific constraints of your cloud provider.

To run the CloudEDGE handler test suite, simply run the run-execution-testing.js node script. To integrate the test suite into your NPM build process, you can run the script on the posttest NPM event by updating the scripts section of your package.json file.

Integrating the CloudEDGE handler testing framework

“scripts”: {
  ...
  “posttest“:
    "node node_modules/@webscale-networks/cloudedge-handlers/run-execution-testing.js <cloud-provider>",
  ...
}

<cloud-provider> should be replaced with the name of the cloud provider for the test. For AWS providers, use aws.

To execute NPM’s testing pipeline, run npm test in the root directory of your package.

To create mock wRequest and wResponse objects in your own unit tests, import the following mock data available to you from the CloudEDGE handler test suite.

  • Complete wRequest and wResponse objects are available in: .../node_modules/@webscale-networks/cloudedge-handlers/execution_testing/webscale/src/object.js
  • The case-insensitive HTTP header class is available in: .../node_modules/@webscale-networks/cloudedge-handlers/execution_testing/utils/src/headers.js

Further reading

Have questions not answered here? Please Contact Support to get more help.


Last modified November 10, 2021