Apex Callout Delegator
Performing Callouts after DML in a Synchronous Context.
Background
When working with callouts there usually comes a time when we need to perform a DML operation and then do a callout with the returned data. As an example we might have a system that processes Rebate Applications. When submitting an Application a user has to upload some additional documents such as a Proof of Purchase to an online file storage site that is integrated with our system.
In this example we need to insert the Application record and then create a folder with the auto generated Application Name. Performing this set of operations in a single context means that the system would do a DML to insert the record and then do a Callout to create the folder which would throw the following error.
System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.
After some quick searching you will find that you cannot make callouts once changes to the database have been made. Also there is no way to explicitly commit the changes to the database. At this point you will come to the conclusion that it’s not possible to perform this set of operations in a single context. The likely solution will be to split the operations into multiple contexts. As an example we would have a Trigger on Application that calls a @future or Queueable to perform the callout.
Now we will take a look at how the Apex Callout Delegator can make the impossible; possible! That is how to perform a Callout after a DML.
Technical Details
The Apex Callout Delegator is structured very much like the standard Http* classes. It consists of a single Callout class which has Request and Response subclasses. Like the Http class Callout has a send method that takes Callout.Request as a parameter and returns a standard HttpResponse object.
The ‘Callout.Request’ and Callout.Response subclasses have identical methods to their standard counterparts. The only reason these were created is that the standard HttpRequest and HttpResponse classes do not have getters for all their properties and are not fully serializable.
The Callout Delegator performs callouts in a separate context, much like @future and Queueable, but with the key difference of being synchronous. This is achieved by utilizing PageReference.getContent method. When calling Callout.send we get a reference to Callout.page and then serialize and encode the Callout.Request finally adding it as a URL parameters to the Callout.page reference and calling the PageReference.getContent method.
Next the Callout.page is fetched at which point the Callout controller is initialized and the Callout.run method executed. In the Callout.run method we grab the serialized and encoded Callout.Request and we proceed to decode and deserialize it back into the Callout.Request object. The Callout.Request class has a generate helper function which is used to generate a standard HttpRequest which is then sent with the Http.send method and a HttpResponse is received. Then we grab all the data from then HttpResponse and create a Callout.Response which is then serialized and merged onto the Callout.page
At this point we are back in the Callout.send method, the PageReference.getContent has returned a Blob which represents our Callout.Response. Finally we convert the Blob into a String, deserialize it back into a Callout.Response and use it helper generate method to generate a standard HttpResponse which is returned in a synchronous context.
Usage
The Apex Callout Delegator is very easy to use. It has the same methods exposed as the standard Http* classes. Please visit the callout-delegator-apex GitHub page for usage examples.
Hi, your example uses ApexPage. Is the approach usable if there is no hands-on user and no page? For example, the process started from an inbound REST POST. Then the Apex code needs to INSERT data, GET more data, then INSERT again.
thanks.
Hey Sean,
That would have been possible when this was initially released.
Unfortunately Salesforce patched it by changing PageReference.getContent calls to act as callouts themselves.
You will likely have to split your functionality into multiple REST calls.
This code is not working for me ,….throwing an exception ..System.JSONException: No content to map to Object due to end of input.
Kindly help asap
As mentioned before, Salesforce changed how PageReference.getContent is counted agains its limits patching this.
So basically, this means that this beautifully coded and elegant solution is no longer a solution at all anymore?
It doesn’t work anymore, I’ve tried without success, still getting: “You have uncommitted work pending. Please commit or rollback before calling out”