REST Web Services - Generating and Working with Error Responses
By Chris Malek | Sun, Mar 19, 2017
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.
200 OKmeans everything was fine. Really anything in the
2xxrange is success and there are subtle differences between each value.
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 Foundthat 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 Foundto be returned when an error happens.
- This is a “hard coded” status that will always be returned for any error. This is a way to override this programmatically and we will look at that later.
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
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; property integer OnErrorHttpResponseCode; 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
OnRequestmethod or some other error happening in the
OnRequest. If the
OnErrorreturns, then the
404status code we defined in the service operation will be sent on the response.
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
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
OnErrorreturns a string where the method signature for
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
OnErrormethod? This is not obvious and not documented. This might be the most subtle and important part of this post. The
messageinput parameter. We can look at the
IBExceptionproperty of that message to get an
Exceptionobject 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..
What happens if we want to return different HTTP status codes based on different conditions? It turns out there is an undocumented way to handle this in REST services in 8.55+. Prior to 8.55, there was no way to do this.
First we need to modify our handler class definition adding the following new property that is in the
property integer OnErrorHttpResponseCode;
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; property integer OnErrorHttpResponseCode; end-class;
Next we need to go back to the service operation definition and define a list of “Optional Status Codes” that can be returned. This can be thought of as a “whitelist”. If the value is not defined here it cannot be returned in the code.
Now, in the
onError method you need to set the status code to one of those whitelisted values like this:
%This.OnErrorHttpResponseCode = 400;
That will cause the REST listening connector to return that status code.
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”.
GET /PSIGW/RESTListeningConnector/PSFT_CS/USER.v1/PS HTTP/1.1 Host: ibdev.cedarhillsgroup.com
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.
GET /PSIGW/RESTListeningConnector/PSFT_CS/USER.v1/JOHN HTTP/1.1 Host: ibdev.cedarhillsgroup.com
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.
- There does not seem to be a way to programmatically return different 2xx codes. There seems to be something similar to the error HTTP status codes documented above in the operation setup. Howver, I can’t find the method to set in the handler. The documenation is silent on this and so is oracle support.
- Another annoying thing when 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.