By | 19 March 2017

REST Web Services - Generating and Working with Error Responses

In this article, we cover returning different HTTP Status codes and error responses in PeopleTools REST Web Services. We look at the following areas:

  • Why do you need error responses?
  • What are HTTP Status codes and how do you map different types of errors to HTTP status codes?
  • How do you create error responses in PeopleSoft REST services and return different HTTP status codes.
  • We look at PeopleTools bugs with error responses and the Synchronous Integration Broker Monitor and some limitions with the framework.

I created an article called A Complete PeopleSoft REST Web Service Example which guides the reader through creating a basic PeopleSoft REST web service. If you are not familiar with PeopleTools REST Service Operations, I would recommend reading that article before proceeding with this article.

Why do we need error responses?

Web services need to be able to return errors to the client. There are an infinite number of reasons a web service might error out. Errors happen and we need to be able to return "signals" to the client that the call was not successful.

  • The client could have submitted some bad data to the service.
  • The client may be asking for something that does not exists like an invalid EMPLID.
  • The service may be down or experiencing issues.

Almost always any errors that occur are returned in the HTTP Body using some message encoding like JSON or XML. That is true for SOAP and REST. However, in REST there is a preference to make use of HTTP Status codes as a central place to flag failures and success.

Mapping HTTP Status Codes to Error Conditions

REST aims to piggy back on the HTTP protocol. REST makes use of HTTP verbs and other features in the protocol. How do we easily communicate status using the HTTP Protocal? HTTP Status Codes. Every time I create some new API for a client and I study the list of HTTP status codes. I am always impressed by how well thought out those codes are. There seems to be a code for about every condition you can encounter.

As a general rule in REST APIs, the status should be communicated through the HTTP status code.

  • HTTP 200 OK means everything was fine. Really anything in the 2xx range is success and there are subtle differences between each value.

The 4xx client errors and 5xx server errors is really what we are interested in here.

  • In a "GET" REST web service, if you request a resource and receive HTTP 404 Not Found that means that the value does not exists. It could have been deleted or may have never existed. This is a signal to the client that they may have an error or a reference to an invalid resource.
  • In a "POST" REST web service, if you try to post some data that is malformed, then the service would likely return HTTP 400 Bad Request.

When errors happen, there may be additional information in the response body but the status code should be the first place the client looks for success or errors. The client may deal with errors differently depending on the smarts of the client or the context of the request. Some clients may be able to recover from the error and continue. In other contexts, the error may stop the whole process and the error may be logged or displayed to the user or both.

Since there is no defacto or industry specification on how to return detailed error messages in web services each service generally defines a different data format for the response body. The detailed error message on the response body can have any shape defined by the author. It is important to first reflect the service call status using HTTP status codes. This insures that many different clients can invoke the service and detect success or failure with the minimual amount of information about the web service design. Not all clients may be able to parse the error responses contained in the response body but all HTTP compatible clients will be able to read HTTP status codes easily.

In short, HTTP Status codes should reflect the status of the service call. Do NOT return 200 OK then bury some error code and message in the response body.

Creating Our Example Web Service

First we need an example web service to work with for this post. In this screenshot, we create a USER_GET service operation which will retrieve data from PSPOPRDEFN and return a few fields back. A client can request information on a specific OPRID using standard REST parameters in the URL. At this point we do not have this setup to return error responses. We will "Add a Fault Type" (red box) shortly. Also note that this web service will return HTTP Status Code 200 (yellow box) when it runs to success. That can be changed depending on your use case.

If you click the "Add Fault Type" button, a new row will be inserted in the message instance grid (red box).

  • Here we set the message to the same as our success since we will be generating XML in the PeopleCode as we did in our REST example.
  • We also choose a status code of 404 Not Found to be returned when an error happens.
    • This is a "hard coded" status that will always be returned for any error. We will go into more on this later but this is a pretty big limitation. Basically the only status code we can return for this web service is that 404.

What we have now is a skeleton structure to return 200 OK on success and a 404 Not Found on any error. We have to pair this configuration with some special peoplecode to actually get the response to return 404.

Creating REST Error Response in PeopleCode

First I will dump the full PeopleCode handler for this web service here. Then we will go over each part.

import PS_PT:Integration:IRequestHandler;

class C_USER_handler implements PS_PT:Integration:IRequestHandler
   method C_USER_handler();
   method OnRequest(&_MSG As Message) Returns Message;
   method OnError(&pRequestMsg As Message) Returns string;

end-class;

method C_USER_handler
   /* Nothing here for now */
end-method;

method OnRequest
   /+ &_MSG as Message +/
   /+ Returns Message +/
   /+ Extends/implements PS_PT:Integration:IRequestHandler.OnRequest +/

   Local Message &request, &response;
   &request = &_MSG;
   Local Document &reqDOC;
   &reqDOC = &request.GetURIDocument();
   Local string &parm1;
   &parm1 = &reqDOC.GetElement("user").value;

   &response = CreateMessage(Operation.USER_GET, %IntBroker_Response);

   Local XmlDoc &xmlout;
   Local XmlNode &childNode;
   &xmlout = CreateXmlDoc("<?xml version='1.0'?><RESPONSE/>");

   Local Record &recPSOPR = CreateRecord(Record.PSOPRDEFN);
   &recPSOPR.OPRID.Value = &parm1;
   If &recPSOPR.SelectByKey() Then
      &childNode = &xmlout.DocumentElement.AddElement("OPRID").AddText(&recPSOPR.OPRID.Value);
      &childNode = &xmlout.DocumentElement.AddElement("EMPLID").AddText(&recPSOPR.EMPLID.Value);
   Else
      throw CreateException( - 1, - 1, "User not found %1", &parm1);
   End-If;

   &response.SetXmlDoc(&xmlout);

   Return &response;
end-method;


method OnError
   /+ &pRequestMsg as Message +/
   /+ Returns String +/
   /+ Extends/implements PS_PT:Integration:IRequestHandler.OnError +/


   Local XmlDoc &xmlout;
   Local XmlNode &rootNode;
   Local XmlNode &tempnode, &childnode;

   &xmlout = CreateXmlDoc("<?xml version='1.0'?><RESPONSE/>");
   Local Exception &exception = &pRequestMsg.IBException;
   &childnode = &xmlout.DocumentElement.AddElement("ERROR");
   &tempnode = &childnode.AddText(&exception.ToString());

   Return &xmlout.GenXmlString();
end-method;

There are two methods that we need to concern ourselves with:

  • OnRequest - Responsible for handling the request and returning a response for a successful path.
  • OnError - Responsible for returning some error string which will be returned to the client. This method gets called because of a throw in the OnRequest method or some other error happening in the OnRequest. If the OnError returns, then the 404status code we defined in the service operation will be sent on the response. There does not seem to be a way to set the status code programmatically which is lame.

Let's go over our code path step by step.

First we have some code that is just grabbing the parameters from the URL. I did not show this setup in this blog post. If you are interested in how this is done please see A Complete PeopleSoft REST Web Service Example. At the end of this code we have a value in &parm1 that will be the OPRID the client wants information on.

  Local Message &request, &response;
   &request = &_MSG;
   Local Document &reqDOC;
   &reqDOC = &request.GetURIDocument();
   Local string &parm1;
   &parm1 = &reqDOC.GetElement("user").value;

Next we have some standard code to create a response message object and some XML objects to be used to encode response data.

   &response = CreateMessage(Operation.USER_GET, %IntBroker_Response);

   Local XmlDoc &xmlout;
   Local XmlNode &childNode;
   &xmlout = CreateXmlDoc("<?xml version='1.0'?><RESPONSE/>");

Now we have code to try to lookup the OPRID requested. If the OPRID was found we encode the OPRID and the EMPLID in some XML nodes. If the OPRID search did not return any rows we end up with a throw statement which we will look at shortly.

   Local Record &recPSOPR = CreateRecord(Record.PSOPRDEFN);
   &recPSOPR.OPRID.Value = &parm1;
   If &recPSOPR.SelectByKey() Then
      &childNode = &xmlout.DocumentElement.AddElement("OPRID").AddText(&recPSOPR.OPRID.Value);
      &childNode = &xmlout.DocumentElement.AddElement("EMPLID").AddText(&recPSOPR.EMPLID.Value);
   Else
      throw CreateException( - 1, - 1, "User not found %1", &parm1);
   End-If;

If we continue down the successful path these two last lines from OnRequest will execute which take our XML and pass it to the response message. We then return that to the client. In our case a HTTP 200 OK will be returned to the client because that is how we configured the service operation on the first screenshot above.

  &response.SetXmlDoc(&xmlout);

   Return &response;

Now let's look at what happens when the OPRID was not found. When an error is thrown the code execution jumps to the OnError method assuming there is not a surrounding try/catch block.

Lets look at the full OnError code again here.

method OnError
   /+ &pRequestMsg as Message +/
   /+ Returns String +/
   /+ Extends/implements PS_PT:Integration:IRequestHandler.OnError +/


   Local XmlDoc &xmlout;
   Local XmlNode &rootNode;
   Local XmlNode &tempnode, &childnode;

   &xmlout = CreateXmlDoc("<?xml version='1.0'?><RESPONSE/>");
   Local Exception &exception = &pRequestMsg.IBException;
   &childnode = &xmlout.DocumentElement.AddElement("ERROR");
   &tempnode = &childnode.AddText(&exception.ToString());

   Return &xmlout.GenXmlString();
end-method;
  • We should notice that the method signature for OnError returns a string where the method signature for OnRequest returns a Message. This is kind of inconsistent in the framework. So if we want to return some nicely XML encoded error messages then we must do the work to generate the XML and return that as a string.
  • How do we know what error happened and how we got to the OnError method? This is not obvious and not documented. This might be the most subtle and important part of this post. The OnError has a message input parameter. We can look at the IBException property of that message to get an Exception object which we can look at the detail of the error that occured.

So all this OnError method is doing is getting the Exception that happened and encoding it in XML and returning and XML string. When the OnError returns the string will be returned in the HTTP body and the HTTP Status code will be 404 Not Found because that is what we hard coded in the service operation. Again there does not seem to be a way to return something other than our hard coded 404 in the OnError method. (If anyone knows a trick please let me know.)

Example Web Service Calls

Now lets look at some example request and responses for this web service. First we will look at a successful request where we ask for the user information on the user "PS".

HTTP Request:

GET /PSIGW/RESTListeningConnector/PSFT_CS/USER.v1/PS HTTP/1.1
Host: ibdev.cedarhillsgroup.com

HTTP Response:

HTTP/1.1 200 OK
date:"Wed, 15 Mar 2017 22:31:59 GMT"
content-length:"86"
content-type:"text/xml; encoding="UTF-8""
content-encoding:"gzip"

<?xml version="1.0"?>
<RESPONSE>
    <OPRID>PS</OPRID>
    <EMPLID>KU0007</EMPLID>
</RESPONSE>

You will see that we got some XML back with the EMPLID attached to the PS OPRID. You will also see the first line in the response is HTTP/1.1 200 OK which is a success.

Now let's look what happens when we request a user "JOHN" that does not exist.

HTTP Request:

GET /PSIGW/RESTListeningConnector/PSFT_CS/USER.v1/JOHN HTTP/1.1
Host: ibdev.cedarhillsgroup.com

HTTP Response:

HTTP/1.1 404 Not Found
date:"Wed, 15 Mar 2017 22:30:29 GMT"
content-length:"167"
content-type:"text/plain; charset=UTF-8; encoding="UTF-8""
content-encoding:"gzip"

<?xml version="1.0"?>
<RESPONSE>
    <ERROR>User not found JOHN (-1,-1) C_USER.HANDLERS.C_USER_handler.OnExecute  Name:OnRequest  PCPC:1080  Statement:20 (-1,-1)</ERROR>
</RESPONSE>

The first line of the response shows our 404 Not Found status code. The the body is the XML encoded exception string from this throw statement.

throw CreateException( - 1, - 1, "User not found %1", &parm1);

PeopleSoft Limitations and Issues

Lets look at some of the issues around errors with PeopleSoft and the integration broker.

  • First as we stated throughout this post, there is no way to set different HTTP status codes programmatically to return different variations of 4xx (Client) or 5xx (Server) error codes programmatically. The same applies to different variations on 2xx codes. This is a pretty big limitation to adhere to a REST paradigm. In any other programming language, this would be trivial to set the HTTP status code. However, in PeopleSoft you don't have direct access to what is going out back over the wire because it has been abstracted away too much. Hopefully, this will change in some future release.

    • Why would we want to change status codes programmatically? Let's say that we had some sort of "row level security" in this web service we describled above and the client was requesting a resource they were not authorized to see. In that case a 404 Not Found may not be appropriate and a403 Forbidden may be better. However, it seems we don't have that option which is really frustrating. But we have to soldier on and work with what we are given warts and all.
  • The second annoying thing about returning errors in the integration broker is that they show up as errors in the Synchronous Service Monitor. Every time your code returns something other than a success an error message will be present. Errors are common and may not need to be actually handled by a PeopleSoft administrator. The client may have recovered. So if you have any monitoring looking at error messages you will get many false reports.

  • And to add insult to injury, the detailed error message in the monitor has useless information for you to research the error. If your web service returned some error string, it will not show up in my experience. The thrown message is not logged correctly. The error response XML or string is not visible nor is the structure of the original request. So basically, these are absolutely useless errors and there is no point of logging them since there is no detail on the signature of the service invokation and the error returned. I had opened a case with Oracle and there does not seem to be a way to disable this.

Additional Reading

Do you want to become a CI Expert?

If you want to learn more about developing using Component Interface then check out our CI Training Video.