Code the Web Service Call

The general approach to coding web services in Salesforce B2C Commerce involves writing a script for a RESTful web service and storing credentials or certificates in the Business Manager.

The web service script:
  • gets Service objects from the registry
  • provides input parameters for the createRequest callback used to construct the URL to call the web service
  • provides error handling based on the Result of the web service call
  • writes output variables to the pipeline dictionary to be used by the rendering template

If you are implementing a SOAP web service or require WS-Security features, you can also write a script that uses the dw.ws package to implement web services. In this case, certificates must be stored in your cartridge.

Creating a Controller for Web Services

When creating a controller for web services, you usually need to include error handling and render the results of the web service call.

Understanding Pipelines for Web Services in Legacy Implementations

If you are maintaining an older cartridge that uses pipelines, web services are implemented in a pipeline with a script node for the web service script. The script node must have an error transition for error handling and a transition to a rendering template if you need to render the results of the web service call.

Creating a Web Service Script

Your web service script needs to import the dw.svc package.

Use LocalServiceRegistry.createService to create a Service object. Your script can modify or extend the Service object. You can also include business logic to control the parameters used to invoke the web service.

Use Service.call to invoke the web service.

When the service is invoked, the service framework checks the circuit breaker and rate limiter. If either the circuit breaker or rate limiter are triggered, then no call is made and the appropriate errors are logged. If neither are triggered, then the callback methods in the service instance are executed, and the dw.svc.Result object is stored in the Service.

Example 1: Simple FTP Service Call

In this example, the operation invoked for the web service is determined by an input variable passed from the pipeline. In the dictionary bindings for the script node, the numCalls variable is bound to CurrentHttpParameterMap.numCalls.stringValue and the testType variable is bound to CurrentHttpParameterMap.testType.stringValue.

/**
 *  testFTPClient.ds
 *
 
 *   @input testType : String
 *   @input numCalls : String
 *   @output svcConfig : dw.svc.Service
 *   @output ftpResult : dw.svc.Result
 *
 */
importPackage(dw.util);
importPackage(dw.svc);
importPackage(dw.net);
importPackage(dw.io);

function execute(args: PipelineDictionary): Number {

    var service: Service;
    var result: Result;
    var counter = args.numCalls;
    var mockCall = false;
    var pipelineError = false;

    var callTestFTP = LocalServiceRegistry.createService("test.ftp", {
        mockCall: function(svc: FTPService, params) {
            return [{
                    "name": "testfile1",
                    "timestamp": new Date(2011, 02, 21)
                },
                {
                    "name": "testfile2",
                    "timestamp": new Date(2012, 02, 21)
                },
                {
                    "name": "testfile3",
                    "timestamp": new Date(2013, 02, 21)
                }
            ];
        },
        createRequest: function(svc: FTPService, args) {
            return svc;
        },
        parseResponse: function(svc: FTPService, result: Array) {
            var ret: Array = [];
            for (var i = 0; i < result.length; i++) {
                ret.push(result[i].name);
            }
            ret.sort();
            return ret;
        }
    });

    // Execute the request on the service configuration
    function makeCall(svcConfig: Service, params: Object) {

        if (counter == null) {
            counter = 1;
        }

        while (counter != 0) {
            if (mockCall) {
                result = service.setMock().call(params);
            } else if (pipelineError) {
                result = service.setThrowOnError().call(params);
            } else {
                result = service.call(params);
            }
            counter--;
        }

        // Set pdict out values
        args.svcConfig = service;
        args.ftpResult = result;
    }

    switch (args.testType) {
        case "LIST":
            service = callTestFTP;
            service.setOperation("list");
            break;
        case "CD":
            service = callTestFTP;
            service.setOperation("cd", "/");
            break;
        case "MKDIR":
            service = callTestFTP;
            service.setOperation("mkdir", "test");
            break;
        case "DELETE":
            service = callTestFTP;
            service.setOperation("del", "test");
            break;
    }

    makeCall(service);

    if (result == null || service == null) {
        return PIPELET_ERROR;
    }

    return PIPELET_NEXT;
}

Example 2: Modifying the Service Object for an HTTP Service

This example demonstrates how you can use the methods on the Service object to modify the Service object before invoking the service. The ENCODING case in this example adds a header, URL parameters, and changes the encoding before invoking the service.

/**
 * testHTTPClient.ds
 *
 *   @input testType : String the type of test to run.
 *	 @input numCalls : String the number of calls to make.
 *   @input returnCode : String the simulated http return code.
 *	 @output svcConfig : dw.svc.Service the service configuration object.
 *   @output httpResult : dw.svc.Result the http result object.
 *
 */
importPackage(dw.system);
importPackage(dw.svc);
importPackage(dw.net);
importPackage(dw.io);

function execute(args: PipelineDictionary): Number {
    var service: Service;
    var result: Result;
    var counter = args.numCalls;
    var mockCall = false;
    var pipelineError = false;
    var returnCode = args.returnCode;
    var requestBody = {
        'testString': 'foo',
        'testNum': 5,
        'testBool': true
    };


    var callTestGet = LocalServiceRegistry.createService("test.http.get", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("GET");
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        },
        mockCall: function(svc: HTTPService, client: HTTPClient) {
            return {
                statusCode: 200,
                statusMessage: "Success",
                text: "MOCK RESPONSE (" + svc.URL + ")"
            };
        },
        filterLogMessage: function(msg: String) {
            return msg.replace("headers", "OFFWITHTHEHEADERS");
        }
    });

    var callTestPost = LocalServiceRegistry.createService("test.http.post", {
        createRequest: function(svc: HTTPService, args) {
            // Default request method is post
            // No need to setRequestMethod
            if (args) {
                svc.addHeader("Content-Type", "text/json");
                return JSON.stringify(args);
            } else {
                return null;
            }
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });

    var callTestPut = LocalServiceRegistry.createService("test.http.put", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("PUT");
            if (args) {
                svc.addHeader("Content-Type", "text/json");
                return JSON.stringify(args);
            } else {
                return null;
            }
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });

    var callTestDelete = LocalServiceRegistry.createService("test.http.delete", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("DELETE");
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });

    var callTestBasicAuth = LocalServiceRegistry.createService("test.http.basicauth", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("GET");
            svc.setAuthentication("BASIC");
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });

    var callTestTimeout = LocalServiceRegistry.createService("test.http.timeout", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("GET");
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });


    var callTestStatus = LocalServiceRegistry.createService("test.http.status", {
        createRequest: function(svc: HTTPService, args) {
            svc.setRequestMethod("GET");
        },
        parseResponse: function(svc: HTTPService, client: HTTPClient) {
            return client.text;
        }
    });


    // Execute the request on the service configuration
    function makeCall(svcConfig: Service, params: Object) {

        if (counter == null) {
            counter = 1;
        }

        while (counter != 0) {
            if (mockCall) {
                result = service.setMock().call(params);
            } else if (pipelineError) {
                result = service.setThrowOnError().call(params);
            } else {
                result = service.call(params);
            }
            counter--;
        }

        // Set pdict out values
        args.svcConfig = svcConfig;
        args.httpResult = result;
    }

    switch (args.testType) {

        // make a GET request with additional headers, query param, json payload
        case "GET":
            service = callTestGet;
            service.URL += "/get";
            service.addHeader('testHeader', 'testHeaderValue')
                .addParam('filter', true);
            break;

            // make a POST request with additional headers, query param, json payload
        case "POST":
            service = callTestPost;
            service.URL += "/post";
            service.addHeader('testHeader', 'testHeaderValue')
            break;

            // make a POST request with additional headers, query param, json payload, using UTF-16 encoding
        case "ENCODING":
            service = callTestPost;
            service.setEncoding('UTF-16');
            break;

            // make a PUT request with additional headers, query param concatenation, json payload
        case "PUT":
            service = callTestPut;
            service.URL += "/put";
            service.addParam('filter', true)
                .addParam('secondParamAmpersand', 1);
            break;

            // make a DELETE request with additional headers, query param, json payload
        case "DELETE":
            service = callTestDelete;
            service.URL += "/delete";
            service.addHeader('testHeader', 'testHeaderValue')
                .addParam('filter', true);
            break;

            // make an HTTP post with BASIC auth.
        case "BASICAUTH":
            service = callTestBasicAuth;
            service.URL += "/basic-auth/user/passwd";
            break;

            // make a GET request with timeout
        case "TIMEOUT":
            service = callTestTimeout;
            service.URL += "/delay/6";
            break;

            // make a GET request and write result object to file
        case "OUTFILE":
            service = callTestGet;
            service.setOutFile(File("TEMP/" + Math.random()));
            break;

            // make a GET request in explicit mock mode
        case "MOCKED":
            service = ServiceRegistry.get("test.http.get");
            mockCall = true;
            break;

            // make a GET request and test ratelimit
        case "RATELIMIT":
            service = callTestGet;
            service.URL += "/get";
            break;

            // make a GET request with status code return as specified by returnCode pipeline query param
        case "STATUS":
            service = callTestStatus;
            if (returnCode == null) {
                trace("No return code provided");
            }
            service.URL += "/status/" + returnCode;
            break;

            // make a GET request and throwOnError() to have PIPELINE handle response error.  
        case "THROWONERROR":
            service = callTestStatus;
            service.URL += "/status/" + 500;
            counter = 20;
            pipelineError = true;
            break;
    }

    // Make the service call here
    makeCall(service, requestBody);

    if (result == null || service == null) {
        return PIPELET_ERROR;
    }

    return PIPELET_NEXT;
}

Error Handling

Information on the service status is stored in the Result object. Use methods on the object to determine the status of the service and any error it has returned:
  • result.error - service-type specific error code, such as a 404 for an HTTP service.
  • result.errorMessage - error message information, such as a Java exception

You can also call .setThrowOnError when calling the service to throw a JavaScript error if the result status is not OK:

result = service.setThrowOnError().call();