An Improved Pattern for Fault Handling

This article seeks to build on work by Ozan Seyman's pattern described in "An error handling pattern for Apigee proxies". While there are elements of this approach I like, and I fully agree with Ozan's summary "main point", I recommend some modifications to the approach to fit better with Apigee's native fault handling, which will reduce code duplication and improve maintenance.

As noted in Ozan's article, using DefaultFaultRule provides many of the benefits described:

  • central error handling/response message
  • simpler FaultRule definitions

However, using a DefaultFaultRule as presented with replacement custom error variables doesn't prevent code duplication/repeat, it just moves the problem from RaiseFault to AssignMessage. Granted, you don't need the error response payload copied/duplicated in RaiseFault, but either an AssignMessage or extension group policy is needed in the fault flow to set the custom flow.myapi... variables. Unless these variables are set with literals, or the native Apigee error variables are copied to them in every case, the common message will have empty holes. This leads to either generic messages or code duplication in AssignMessage. In Ozan's example, custom flow variables are suggested to replace error.content, error.status.code, and error.reason.phrase. Why write code to replace these, if often these have the values we want? This alone would cause concern; however, the larger issue fully custom error variables are it's side effects when departing from the Apigee error flow and variables. Specifically:

  • RaiseFault is crippled. It's purpose is to initiate the error flow and set error variables, specifically error.content <Payload>, error.status.code <StatusCode>. and error.reason.phrase <ReasonPhrase>, as well as affect headers. The general RaiseFault.Json does demonstrate this, but some other policy (AssignMessage or extension group like JavaScript) had to be aware of an actual or pending fault and set the custom variables, even if the error flow already had what it needed.
  • I like the JavaScript throw. It's a great way provide more complex validation logic and enter the error flow when the request or response isn't as expected. However, if custom error variable are used, one must set the flow.myapi... variables prior to the throw or absent companion policy return a generic message. This creates a difference between a planned throw and an unplanned one when the JavaScript fails for a something not handled or a code error.
  • Lastly, this approach less compatible with the security policy group, whose purpose is to fail when something isn't right. Since these policies can't touch custom variables, they'll need flow step conditionals and AssignMessage policies to copy information even when the native error variables already tell the story of what went wrong. This is extra code.

Below I outline an improved approach that builds on Ozan's work. We use the DefaultFaultRule for all of its benefits, but avoid the downside of replacement error variables. Then we add one policy that works with, rather than replaces, Apigee's native fault handling.

Before describing the solution below, let's be grounded on this key concept: first the FAULT event, which we sometimes control occurs, and second we manage the proxy's RESPONSE which we always control, Our goal is to have the fault event leave us in as consistent a state as possible, so we need the least code possible to coordinate a consistent response. The reason we want to work with Apigee's native design, rather than replace it, is because we don't always control why a fault is generated. Departing from the native Apigee error state leaves us with two response approaches (native and custom) or makes us copy the native state to the custom one. Another consideration is forward compatibility. The less we depart from or customize Apigee's solution, the less maintenance or breakage we'll have to adopt future capabilities.

Fault events occur because of the following scenarios:

1.A policy fails and doesn't continueOnError. The security group policies exist for this purpose, however, other policy groups also encounter fatal errors.

2.A error condition in the flow is noted and RaiseFault's purpose is to depart the normal flow and enter the error flow with a defined error state.

3.The extension group (an example is JavaScript code) traps an error condition and throws or returns an error

4.The target returns an error, i.e. a response error can push us into the error flow.

5.Something bad happens in the Apigee platform or infrastructure, in the network, or in our code, etc. and an error condition is raised.

Note: scenarios 1 - 3 are usually planned, we've coded for them and raised the error purposefully. Scenario 4 could be planned or unplanned, and Scenario 5 is a non planned error condition. All need to be handled consistently which starts by getting our error state consistent and going from there in managing the response. In other words, use existing design and conventions and only departing when necessary, leads to fewer lines of code.

Apigee's native fault state sets error.content to a JSON object:

{ "fault": { "faultstring": "Apigee [error.message]",
	     "detail": { "errorcode": "[error prefix].[fault.name]" }
	   }
}

We can reuse this structure, particularly the faultstring value to our advantage. My team's standard error message is:

{ "userMessage": "friendly end user message",
  "developerMessage: "detailed/technical reason for the fault for the app developer",
  "messageID": "custom header to identify the transaction",
  "severity" : "error",
  "code": http-status-code
}

this is constructed using the DefaultFaultRule pattern in an AssignMessage policy. Prior to the AssignMessage, we have one JavaScript policy which acts on error.content to integrate our Apigee extension with native Apigee. The DefaultFaultRule section of the proxy endpoint is as follows:

<DefaultFaultRule name="default-rule">
    <AlwaysEnforce>true</AlwaysEnforce>
    <Step>
         <Name>JavaScriptCatchJSErrors</Name>
    </Step>
    <Step>
        <Name>AssignMessageGenerateFaultResponse</Name>
    </Step>
    <Step>
        <Name>ServiceCalloutLogError</Name>
    </Step>
</DefaultFaultRule>

The AssignMessageGenerateFaultResponse <Payload> assignment is similar in structure to Ozan's RaiseFault.Json <Payload>. Both of these set the error.content variable. It's not necessary to set the Apigee error variables error.status.code and error.reason.phrase using a RaiseFault, because they're already set. The principle difference is, Ozan's approach operates on custom variables in the error flow and then sets Apigee variables towards the end, while my recommendation is to use the native variables (with augmentation) throughout.

The AssignMessageGenerateFaultResponse code is:

<AssignMessage async="false" continueOnError="false" enabled="true" name="AssignMessageGenerateFaultResponse">
    <DisplayName>AssignMessage.GenerateFaultResponse</DisplayName>
    <Set>
        <Headers>
            <Header name="Content-Type">application/json</Header>
        </Headers>
        <Payload contentType="application/json">
            {   "userMessage": "{user.message}",
                "developerMessage": {error.content},
                "messageID": "{request.header.X-NW-MessageID}",
                "severity": "error",
                "code": {error.status.code}
            }
        </Payload>
    </Set>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
    <AssignTo createNew="false" transport="http" type="response"/>
</AssignMessage>

Note: userMessage uses a custom variable, often because userMessage is a custom string. Though not always, particularly when an fault of type 4 or 5 occurs. Why, because when our proxy didn't initiate the fault, the best it can do is pass on all or part of error.content or return one or more generic messages. For those scenarios where we want the default, there's no coding to use it. For those we need to groom the message, there is no additional coding with this approach. So let's look at our planned faults and see how we can use error.content even when we want custom information included.

  1. Policy failure: the Policy Error Reference documentation provides details as to how the error.content is built. Apigee uses the convention that faultstring is a human readable explanation of why the fault occurred and errorcode contains the technical reference detail, which is packaged with elements like [prefix], [policy_name], [error.name], etc. Sometimes what's packaged here is good enough. Not always, but as noted above, code when you need an exception, not all the time. If faultstring is an adequate userMessage, no additional code is needed, as by default we'll use the faultstring as the userMessage and the whole error.content object as the developerMessage.
  2. RaiseFault policy: By minimizing our custom variables we can set the majority of the error variables using standard RaiseFault elements and maximize compatibility with Apigee. Additionally, by leveraging the error.content object, which is set implicitly in scenarios 1, 4, and 5 when Apigee enters the error flow, and is explicitly accessible from RaiseFault (2), AssignMessage, and extension group policies (3), we can inject custom variables with very little code. Examples are presented below.
  3. Extension group policies: I'll use JavaScript for this discussion, however, any of the language extension policies that support the concept of throw should work similarly. When a JavaScript runtime error occurs, Apigee enters the error flow and error.content contains:
{ "fault":
	{ "faultstring":"Execution of <file> failed with error: Javascript runtime
			 error: \"<code error>. (<file>.js:<line #>)\"",
	  "detail":{ "errorcode":"steps.javascript.ScriptExecutionFailed"}
	}
}

where <file> is the name of the JavaScript file, <code error> is the JavaScript exception, and <line #> is the source line of code where the error was thrown. Similarly, if we force an exception using 'throw new Error()' we get the following:

{ "fault":
	{ "faultstring":"Execution of <file> failed with error: Exception thrown
			 from JavaScript : Error (<line #>)",
	  "detail":{ "errorcode":"steps.javascript.ScriptExecutionFailed"}
	}
}

Here is where we extend both the RaiseFault and extension group policies. When a string or object is included as a parameter to 'throw new Error()', the Apigee fault object includes it in the faultstring. Reference the code snippet below:

throw new Error(JSON.stringify({
    "userMessage": userMsg,
    "developerMessage": developerMsg,
    "status": httpStatus,
    "reasonPhrase": reasonPhrase
}));

... results in ... (formatted for readability)

{ "fault":
	{"faultstring":"Execution of <file> failed with error: Exception thrown from
			JavaScript : Error: {\"userMessage\":\"Bad Request\",
					     \"developerMessage\":\"Content-Type Header
								    is missing\",
					     \"status\":\"400\",
					     \"reasonPhrase\":\"Bad Request\"}
			(<file.js><line #>)",
	 "detail":{"errorcode":"steps.javascript.ScriptExecutionFailed"}
	}
}

Similarly we can use a RaiseFault <Payload> element tag to set up error.content. In this example, a conditional flow that matched when an invalid request.verb for a given proxy.pathsuffix was seen, a fault is raised and a custom error message is set in RaiseFault, which can include Apigee variables and wills be used in the DefaultFaultRule steps.

<RaiseFault continueOnError="false" enabled="true" name="RaiseFaultMethodNotAllowed">
    <DisplayName>RaiseFault.MethodNotAllowed</DisplayName>
    <FaultResponse>
        <!--  EXAMPLE: using RaiseFault (RF) in cooperation with catchJSError to allow
	      RF to set variables. If no special fault processing is needed, i.e. no
	      fault rule, the RaiseFault (RF) policy can use the catchJSErrors policy
	      by setting the Payload (error.content) the same way .js code can.  This
	      allows a RaiseFault to set user.message indirectly, which would normally
              be inaccessible to it as RF doesn't support AssignVariable. Note: the
	      escaped faultstring, must be on one line. -->
        <Set>
            <Payload>{"fault": { "faultstring": "{\"userMessage\": \"The HTTP method used is invalid\", \"developerMessage\": \"Method not allowed - Resource Name: {proxy.pathsuffix}, method : {request.verb}\", \"status\": 405, \"reasonPhrase\": \"Method Not Allowed\" } ",
                                 "detail": { "errorcode": "unused" } } }
            </Payload>
            <!-- if not using the method above, than HTTP status and ReasonPhrase
	         could be set using XML configuration
            <StatusCode>405</StatusCode>
            <ReasonPhrase>Method Not Allowed</ReasonPhrase>                   -->
        </Set>
    </FaultResponse>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</RaiseFault>

So any number of custom variables you might need can be indirectly passed to the catchJSErrors.js code that we'll look at next. It is best to keep the number of custom variables few or optional, so faults arising from scenarios 1, 4, or 5 can map to your standard fault message. Alternatively you can alter catchJSErrors.js to fill in defaults for custom variables or use a precedence hierarchy, similar to what it's doing with userMessage.

Let's refresh on the DefaultFaultRule steps. If we've always accepted the default values and/or put our error information into Apigee's error.content variable, then we can use one JavaScript policy to parse it all out into whatever variables we need prior to finalizing the format in the common AssignMessage policy and exercising whatever common logging our operation requires.

<DefaultFaultRule name="default-rule">
    <AlwaysEnforce>true</AlwaysEnforce>
    <Step>
         <Name>JavaScriptCatchJSErrors</Name>
    </Step>
    <Step>
        <Name>AssignMessageGenerateFaultResponse</Name>
    </Step>
    <Step>
        <Name>ServiceCalloutLogError</Name>
    </Step>
</DefaultFaultRule>

If scenario 1, 4, or 5 occurred and we don't like the existing faultstring, we can always write a specific fault rule and AssignMessage to groom the custom variables before entering the DefaultFaultRule. We've extended Apigee's capabilities, but we haven't replaced any. So traditional patterns work. An example of scenario 1 with a groomed user message upon API key failure is:

-<FaultRule name="API Key Fault">
    <Condition>(verifyapikey.VerifyAPIKey.failed = true) or (response.status.code equals 403) or (response.status.code equals 401)</Condition>
    <Step>
        <Name>AssignMessageAPIKeyFault</Name>
    </Step>
</FaultRule>

... and AssignMessageAPIKeyFault ...

<AssignMessage continueOnError="false" enabled="true" name="AssignMessageAPIKeyFault">
    <DisplayName>AssignMessage.APIKeyFault</DisplayName>
    <AssignVariable>
        <Name>user.message</Name>
        <Value>The application is not authorized to make this request</Value>
    </AssignVariable>
    <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
</AssignMessage>

... and the output is ...

{   "userMessage": "The application is not authorized to make this request",
    "developerMessage": "{\"fault\":{\"faultstring\":\"Failed to resolve API Key variable request.header.client_id\",\"detail\":{\"errorcode\":\"steps.oauth.v2.FailedToResolveAPIKey\"}}}",
    "messageID": "some-alphanumeric-messageID",
    "severity": "error",
    "code": 401
}

... our OAS specifies the developer message as a string.  So objects are stringified to
    match the spec.  With little modification, the catchJSErrors.js code can
    support putting a JSON object as the value instead of a string.

So this leads us to catchJSErrors.js. It's commented heavily, so I'll just give a summary here:

function catchJSErrors accesses error.content and attempts to parse and access faultstring.

If

  • it's not a JSON object (rare),
  • or it doesn't contain faultstring,
  • or faultstring doesn't have an embedded object with userMessage in it

then

  • the catch block will be executed and user.message will be examined to see if it's defined, if not, it will be set to error.message
  • and error.content will have any double quotes escape quoted, so it can be wrapped in quotes for output

else

  • user.message will be set to passed in value of userMessage; this is defined to be a string, so it's assumed to be properly escape quoted
  • error.content will be set to the passed in value of developerMessage; this can be a string or an object, so it will be escaped quoted, so any type of object passed in still renders as valid JSON in the response message which applies {error.content}, but the OAS defines as a string.
  • error.status.code will be set to the passed in value of status and
  • error.reason.phrase will be set to the passed in value of reasonPhrase

If your OAS permits or you would want to return error.content as an object, the code comments should get you 90% of the way there.

    function catchJSErrors() {

    // in the Apigee error flow, error.content is always JSON object with the structure
    // { "fault": { "faultstring": "Apigee [error.message]",
    //              "detail": { "errorcode": "[error code prefix].[fault.name]" } } }
    // if javascript throws an error the error parameter is inserted in the faultstring
    // value.  We can use this to extract a escape quoted JSON object out of the string,
    // thereby passing variables to construct the error and user message state
    var errorContent = context.getVariable('error.content');
    
    // if error.content is empty (""), that means the provider had an error response,
    // but Apigee didn't set the error.content.  If the OAS requires error.content to be
    // an object, uncomment the code below to set it as such; otherwise we'll treat it
    // as a string.
    // if (errorContent === undefined || errorContent === null || errorContent === "") {
    //     errorContent = '{}';
    //     context.setVariable('error.content',errorContent);
    // }


    try {
        var faultObj = JSON.parse(errorContent);
        var faultstring = faultObj.fault.faultstring;
        
        // if .js code identified and threw a known error, the faultstring contains an
        // embedded JSON object { "userMessage": "value", "developerMessage": "value",
        //                        "status": 999, "reasonPhrase": "value" }
        // It's also possible for a RaiseFault to construct a similar JSON payload
        
        // if an uncaught .js error occurs, then faultstring will contain the .js file
        // location of the error, which doesn't have the JSON object.  Likewise, system
        // or Apigee policy errors won't have a JSON object in faultstring.  That's OK,
        // in these scenarios, an error is thrown accessing errObj.userMessage and the
        // catch block is executed.  Catch leaves the existing variables unchanged
        // (except for stringifying error.content) and sets user.message (if undefined)
        // to error.message; so the user message isn't left blank.
        var errObj = JSON.parse(/\{.+\}/.exec(faultstring));
        context.setVariable('user.message',errObj.userMessage);
        
        // our OAS specifies the developer message as a string, so any object placed
        // in errror.content needs to be stringified to match the OAS.  If your OAS
	// allows developerMessage to be an object, this block needs to be reworked
	// to support different object types while removing the enclosing quotes.
        var devMsg = errObj.developerMessage;
        switch (typeof devMsg) {
            case "object":
            case "array":
                var newErrContent = JSON.stringify(devMsg); // string; not escape quoted
                context.setVariable('error.content','"' + newErrContent.replace(/([^\\])\"/g,"$1\\\"") + '"');
                break;
            // strings, numbers, and booleans are quoted also to match the OAS
            default:
                context.setVariable('error.content','"' + devMsg.replace(/([^\\])\"/g,"$1\\\"") + '"');
        }
        context.setVariable('error.status.code',errObj.status);
        context.setVariable('error.reason.phrase',errObj.reasonPhrase);
        
    }
    catch (err) {
        print("caught error:" + err.message + "; this is normal");


        var userMessage = context.getVariable('user.message');
        print("userMessage:" + userMessage);
    
        if (userMessage === undefined || userMessage === null) {
            print("using Apigee error.message as a backup user.message");
            userMessage = context.getVariable('error.message');
            context.setVariable('user.message',userMessage.replace(/([^\\])\"/g,"$1\\\""));
        }
        
        // if Apigee error content is passed as the developer message, it needs to
        // be strinfified first, so it matches the OAS.  If the OAS specifies an object
        // for the devMsg, remove this portion of code and uncomment setting
        // error.content to an empty object '{}' above.
        var devMsg = context.getVariable('error.content');
        if (devMsg != "") {
            context.setVariable('error.content','"' + devMsg.replace(/([^\\])\"/g,"$1\\\"") + '"');
        }
   }
}


// this block provides support for Jasmine unit testing.
if (typeof module !== 'undefined') {
    module.exports = catchJSErrors;
} else {
    catchJSErrors();
}

Lastly, here is an example .js file that's doing header validation checking. It's using a few approaches in the throw to demonstrate the flexibility of the throw and how easy it is to pass context.

var validate = {
    requestHdr: function (messageID) {
        if (messageID === null || messageID === undefined) {
            // example where the developer message is an object
            this.throwError('Bad Request', {
                "error": "Message ID request header missing",
                "action": "add X-NW-MessageID as a request header",
                "detail": "X-NW-MessageID must be alphanumeric (no special characters)"
            },
                400, 'Bad Request');
        } else {
            if (! /^[a-zA-Z0-9-]+$/.test(messageID)) {
                // since the response type is application/json, the error context
		// shouldn't use double quotes in the value (unless escaped).
		// Single quotes are OK.
                this.throwError('Bad Request', "X-NW-MessageID header is invalid; must be alphanumeric optionally with dashes ('-')", 400, 'Bad Request');
            }
        }
        context.setVariable('globalTransactionId', messageID);
    },


    contentTypeHdr: function (contentType) {
        if (contentType === null || contentType === undefined) {
            // example were the status code is a string
            this.throwError('Bad Request', 'Content-Type Header is missing', '400', 'Bad Request');
        } else {
            aPrint('_contentTypeHdr: ' + contentType);
            if (contentType !== 'application/json') {
                //example where the status code is a number
                this.throwError('Bad Request', "Content-Type Header is invalid; 'application/json' expected", 400, 'Bad Request');
            }
        }
    },


    authHdr: function (auth) {
        if (auth === null || auth === undefined) {
            this.throwError('The user is not authorized to make this request', 'Access Token in request.header.Authorization is missing', '401', 'Unauthorized');
        }
    },


    throwError: function (userMsg, developerMsg, httpStatus, reasonPhrase) {
        if (typeof module == 'undefined') {
            throw new Error(JSON.stringify({
                "userMessage": userMsg,
                "developerMessage": developerMsg,
                "status": httpStatus,
                "reasonPhrase": reasonPhrase
            }));
        }
    }
}


if (typeof module !== 'undefined') {
    module.exports = validate;
} else {
    var messageID = context.getVariable("request.header.X-NW-MessageID");
    var contentType = context.getVariable("request.header.Content-Type");
    var auth = context.getVariable("request.header.Authorization");

    validate.requestHdr(messageID);
    validate.contentTypeHdr(contentType);
    validate.authHdr(auth);
}

and a similar .js file that looks for code injection threats of various types in the header and, if found, throws and error, but includes the header and RegEx pattern (not in the developer message) for logging. Note: I've sanitized the RegEx patterns.

var check = {
    getHeaders: function () {
        // Get all Request Headers from Context (Apigee has array brackets in string)
        var rqstHdrs = context.getVariable("request.headers.names");

        // Note: the toString() method converts to a String object, not a typeof string,
	// so the addition of an empty string is needed to force type conversion and
        // prevent a type error when the replace() method is invoked.
        var rqstHdrsStr = (rqstHdrs.toString() + "").replace(/(\[|\])/g, ""); // convert to a string and remove the leading and trailing []'s

        return (rqstHdrsStr.split(', ')); //Convert to array and return
    },


    getPatterns: function () {
        // Define Regular Expression Patterns
        return ({
            "Patterns": [
                /(((alter|create|drop|truncate) ... more stuff ,
                / ... another pattern ... /ig,
                / ... and another ... /ig,
                / ... /ig
            ]
        });
    },


    hdrs: function () {
        var requestHeaders = check.getHeaders();
        var regExps = check.getPatterns();


        // Test each header against each RegEx throwing an error if a pattern matches
        requestHeaders.forEach(function (hdr) {
            regExps.Patterns.forEach(function (item) {
                if (check.regExpTest(context.getVariable("request.header." + hdr), item)) {
                    check.throwError(hdr, item);
                }
            });
        });
    },


    regExpTest: function (hdr, regExpPattern) {
        if (regExpPattern.test(hdr)) {
            return true;
        }
    },


    // note the extra values in the throw; they won't be in the standard message,
    // but could be picked out using checkJSErrors() for logging if needed
    throwError: function (hdr, regExp) {
        if (typeof module == 'undefined') {
            throw new Error(JSON.stringify({
                "userMessage": "Bad Request",
                "developerMessage": "Regular Expression Protection Violation",
                "status": 400,
                "reasonPhrase": "Bad Request",
                "header": '"' + hdr + '"',
                "regExp": '"' + regExp + '"'
            }));
        }
    }
};


if (typeof module !== 'undefined') {
    module.exports = check;
} else {
    check.hdrs();
}

I hope this has been helpful.

Comments
anilsr
Staff

+1 , Love it & Definitely helps !! Thank you for the detailed article @Kevin Shomper !

ozanseymen
Staff

Hi Kevin

As I understand it, you are diverting from the original article in two main ways:

First is the use of Apigee error messages in the error response as opposed to setting them using AssignMessage policies. I understand that the reason you are promoting this is to "reduce code duplication and improve maintenance". However I personally don't like this approach as even though Apigee's error messages are quite useful for the API team during troubleshooting, they are not always helpful or precise for the app developers - and we are sending error responses to app developers. Also in some situations Apigee errors are exposing the internal implementation detail (like the JS file and line number) which should not leave the API boundaries. That is the reason why I prefer custom variables to override the default error responses and force a stock 500 response if the error case is not handled properly. I support the idea of parsing/extracting an Apigee error description variable value if the policy is returning one that can be used for this purpose but don’t think this is too much of a deal. They are not dynamic so we are relying on Apigee to set the error ourselves. You also mention that the response will have "empty holes if those custom variables are not set" - which is exactly the intent. As you will have your integration test cases validate each error response, the cases with holes in the response will never go past dev environment. Reusing Apigee error variables will not create any holes but might result in unwanted messages/data in the error response unintentionally which seems more difficult to catch to me.

The other is setting <statuscode> and <reasonphrase>. You are questioning "why write code to replace these, if often these have the values we want" - which I believe is not true as default errors thrown by Apigee are usually 500 and you don't always want that, e.g. OAuth errors with 400, or message validation (using MessageValidation policy) with 400.

Another feedback from me is around the error message structure:

  • userMessage: the notion of returning end-user error message is an interesting concept and definitely up for debate. Two reasons for not doing this for me personally are internationalization and the fact that you can unintentionally make backwards-incompatible api changes by changing the length or structure of the message. In theory if you are responding with end-user content, every api release should include UI tests to make sure they are being rendered correctly.
  • Severity: can’t see the point as your execution has failed and there is nothing you can do about that. Severity is useful for logging but can’t see the use in an error response.
  • Code: http status code is already in the response, this seems to be a duplication.
shompek2
Explorer

Thank you for the well thought out comments. I suspect we're coming from different perspectives, and that's why we may have differences in our approach. Following the pattern you described, we had developers assigning error codes and responses for values that already existed. I saw this as unnecessary code. Also, while our consulting team strongly espouses APIs that can be widely adopted beyond our own applications, we do believe there will exist some internal APIs/resources. As the examples demonstrate, nothing in the approach prevents replacing the developer message, it's just not enforced by returning an alternative in the general response.

You're correct, empty values would be caught in a good test framework. Assuming this exists (and it should), the choice of approach may come down to whether a team has a bias towards reuse of the Apigee values or replacement of them, and who's receiving them. My company has the U.S. market as a strategic focus, i.e. we've highlighted the aspect that our products are not designed to be sold internationally. Given this, it made sense to create a difference between the user message and developer message, which was targeted to the API user/app developer. In reviewing internal code, prior to writing the article, I was concerned by the overwriting of information that would be helpful to the API consumer or the internal operational team.

While there is technical content in the developer message, the intent is to use that either to help the developer better understand the issue or to provide detail that can be used by the API developer. Again, you are correct, in some of my simple examples internal implementation is displayed. My approach does assume a measure of responsibility on the part of the API developer to not expose any implementation that might compromise security. That's why I included the example on the regular expression check, which provides expanded data in the throw, without automatically including it in the developer message.

Regarding severity, I can't git you a good rational for that other than it was a standard defined in our OAS, so it had to be included. As you can see in the GenerateFaultResponse, it's always "error"; so, it's future possibilities aren't defined (or perhaps not present 😉 ).

Again, I strongly appreciate your comments. No approach is perfect and even our own team has had debate regarding the degree of technical content the API should return. My bias was to demonstrate a pattern that minimized code in an enterprise, federated development pattern where not all of our API developers would have the benefit of full test suite. This approach favored the default for that reason.

AwatefR
Observer

Thank you for the details @shompek2 

Version history
Last update:
‎09-21-2017 03:38 PM
Updated by: