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?

AndrewRalon
  • 121
  • 4

1 Answers1

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....
    }
    
AndrewRalon
  • 121
  • 4