1

Even after researching it a fair bit, I don't understand why I keep getting the CalloutException. No matter what I do to prevent it, the exception happens when triggering my API class via Process Builder:

An Apex error occurred: System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

The goal is to do the following when an Opportunity's Stage is changed:

  1. Query for info from Opportunity ID
  2. Send HttpRequest to API
  3. Interpret HttpResponse

Here is the code:

private static Api_Settings__c Settings {get;set;}

@InvocableMethod(label = 'Request API Access' 
    description = 'Requests access to API a SF Opportunity meets criteria.')
public static List<Boolean> CreateAndSendApiAccessRequests(List<ID> opportunityIds)
{
    Settings = Api_Settings__c.getOrgDefaults();

    List<AccessRequest> requests = new List<AccessRequest>();

    for (ID id: opportunityIds)
    {
        System.debug(id);

        AccessRequest request = CreateApiAccessRequest(id);
        requests.add(request);
    }

    List<Boolean> results = new List<Boolean>();

    for (AccessRequest request: requests)
    {
        Boolean result = SendApiAccessRequest(request);
        results.add(result);

        System.debug(result);
    }

    return results;
}

public static AccessRequest CreateApiAccessRequest(ID opportunityId)
{
    // Get info from the Opportunity ID
    Opportunity opp = [
        SELECT Name
            ,Primary_Contact__r.Name
            ,Primary_Contact__r.Email
            ,Account.Name
            ,Account.BillingAddress
        FROM Opportunity
        WHERE ID=:opportunityId];

    // Create the address info from the Opportunity's Primary Contact
    Address address = new Address();
    address.line1 = opp.Account.BillingAddress.getStreet();
    address.line2 = '';
    address.city = opp.Account.BillingAddress.getCity();
    address.postalCode = opp.Account.BillingAddress.getPostalCode();
    address.stateOrProvinceCode = opp.Account.BillingAddress.getStateCode();
    address.countryCode = opp.Account.BillingAddress.getCountryCode();

    // Create the access request
    AccessRequest request = new AccessRequest();
    request.contactName = opp.Primary_Contact__r.Name;
    request.contactEmail = opp.Primary_Contact__r.Email;
    request.organizationName = opp.Account.Name;
    request.address = address;
    request.reason = 'Salesforce API Access Request';

    return request;
}

public static Boolean SendApiAccessRequest(AccessRequest request)
{
    Boolean result = false;

    // Construct the request url
    String requestUrl = Settings.ApiBaseurl__c + Settings.ApiEndpoint__c;

    // Construct the Basic Auth string
    String basicAuthString = BasicAuthService.GetBasicAuthString(Settings.ClientId__c, Settings.ClientSecret__c);

    // Create the API access request
    HttpRequest httpRequest = new HttpRequest();
    httpRequest.setEndpoint(requestUrl);
    httpRequest.setMethod('POST');
    httpRequest.setHeader('Username', Settings.ApiHeaderUsername__c);
    httpRequest.setHeader('Password', Settings.ApiHeaderPassword__c);
    httpRequest.setHeader('Content-Type', 'application/json');
    httpRequest.setHeader('Authorization', BasicAuthString);
    httpRequest.setBody(JSON.serialize(request));

    // Send the request
    HttpResponse httpResponse = new Http().send(httpRequest);

    // Ensure we received the correct status code
    if (httpResponse.getStatusCode() == 201)
    {
        result = true;
    }
    else
    {
        result = false;
    }

    return result;
}

I've tried to separate CRUD operations from callouts, but obviously it hasn't worked.

What am I missing here?

5
  • 3
    Callouts are not allowed synchronously from a trigger. You have to move the logic to a @future method.
    – Adrian Larson
    Commented Sep 18, 2017 at 20:48
  • 3
    I've marked this as a duplicate of the previous question as it seems like the same scenario to me. Let us know if that is not the case and how your question differs. Commented Sep 18, 2017 at 22:47
  • @DanielBallinger I researched many of those questions before posting. My problem was figuring out how to organize my code to properly implement @future (callout=true). I was able to fix it, and would gladly preserve the solution here, but I can't post my answer yet (not enough reputation on this SE site). Commented Sep 19, 2017 at 12:38
  • @AndrewRalon Glad you got it sorted. Please add your answer. Commented Sep 20, 2017 at 0:47

1 Answer 1

1

I had three big problems:

  1. Using two methods won't work. One must be marked @future (callout = true).
  2. The first method should call the callout method in this case.
  3. Api_Settings__c cannot be static.

Here's how I fixed the System.CalloutException:

  1. Make Api_Settings__c settings local in the invocable method (see #2)

  2. Only call Create from the invocable method:

    @InvocableMethod(label = 'Request API Access' 
        description = 'Requests access to API a SF Opportunity meets criteria.')
    public static void CreateAndSendApiAccessRequests(List<ID> opportunityIds)
    {
        Api_Settings__c settings = Api_Settings__c.getOrgDefaults();
    
        for (ID id: opportunityIds)
        {
            CreateApiAccessRequest(settings, id);
        }
    }
    
  3. Query for info in Create and pass everything to Send callout:

    private static void CreateApiAccessRequest(Api_Settings__c settings, ID opportunityId)
    {
        // Get info from the Opportunity ID....
        // Create the request URL....
        // Create the Basic Auth string....
    
        // Callout in separate method
        SendApiAccessRequest(
            opportunityID, 
            requestUrl, 
            basicAuthString, 
            settings.ApiHeaderUsername__c, 
            settings.ApiHeaderPassword__c, 
            opp.Primary_contact__r.Name, 
            opp.Primary_Contact__r.Email,  
            opp.Account.Name, 
            opp.Account.BillingAddress.getStreet(), 
            '', 
            opp.Account.BillingAddress.getCity(), 
            opp.Account.BillingAddress.getPostalCode(), 
            opp.Account.BillingAddress.getState(), 
            opp.Account.BillingAddress.getCountry());
    }
    
  4. Mark Send as @future (callout = true) with return type void:

    @future (callout = true)
    private static void SendApiAccessRequest(....)
    
  5. Take new parameters and handle all send / receive logic in Send:

    @future (callout = true)
    private static void SendApiAccessRequest(
        ID opportunityId, 
        String requestUrl, 
        String basicAuthString, 
        String apiHeaderUsername, 
        String apiHeaderPassword, 
        String contactName, 
        String contactEmail, 
        String accountName, 
        String accountBillingLine1, 
        String accountBillingLine2, 
        String accountBillingCity, 
        String accountBillingPostalCode,
        String accountBillingState, 
        String accountBillingCountry) 
    {
        // Create the address....
        // Create the access request....
        // Create the API access request....
        // Send the request....
    
        // Ensure we received the correct status code....
    
        // Do something with result here....
    }
    

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .