Apex callout PATCH to Heroku Controller workaround

Setup a method to send Aync call to Heroku, add ‘?_HttpMethod=PATCH’ to the path to notify Heroku it’s a patch request

@future(callout=true)
	public static void callApiEndpointAsync(String apiEndpoint, String method, String aPayload){
		HttpRequest req = new HttpRequest();
		List<String> theArgs = new List<String>();
		HttpResponse res = new HttpResponse();
    try {
      if (apiEndpoint != null) {
        req.setTimeout(120000);
				if ('PATCH'.equals(method)){
					apiEndpoint += '?_HttpMethod=PATCH';
					req.setMethod('POST');
				} else {
					req.setMethod(method);
				}

        setAuthHeaderAsync(req);
        req.setEndpoint(herokuUrl + apiEndpoint);
				if (aPayload!=null)
					req.setBody(String.valueOf(aPayload));

				if (Test.isRunningTest() && (mock!=null)) {
					 mock.respond(req);
			 	} else {
					Integer retry = 0;

					while (retry < 2){
						Http http = new Http();
		        res =  http.send(req);
						if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
							retry +=2;
						} else if (res.getStatusCode()==503 || res.getStatusCode()==400){
							retry +=1;
							res = http.send(req);
						} else {
							retry +=1;
							theArgs.add('Api');
							theArgs.add(req.getEndpoint());
							theArgs.add(res.getBody());
						}
					}
					throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', null, theArgs));
				}

      } else {
        System.debug('Service apiEndpoint and payload must not be null');
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED'));
      }
    } catch (Exception ex) {
			theArgs.add('Api');
			theArgs.add(req.getEndpoint());
			theArgs.add(res.getBody());
			throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', ex, theArgs));
			
    }
}

Spring controller checks the RequestParam _HttpMethod to see if it’s a POST or PATCH request

@RequestMapping(value = "/customer", method = RequestMethod.POST, produces = "application/json")
@ResponseBody
@ApiOperation(value = "Creates new payment customer")
@ApiResponses(value = {@ApiResponse(code = 200, message = "Creates new payment customer")})
public String createCustomer(@RequestBody PaymentCustomerWrapper paymentCustomerWrapper, @RequestParam(value="_HttpMethod", defaultValue="POST")  String httpMethod)
        throws IOException, URISyntaxException {
     if (httpMethod!=null && "PATCH".equals(httpMethod)){
         itsLogger.debug("update payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.updatePaymentCustomer(paymentCustomerWrapper).toJson();
    } else {
         itsLogger.debug("create payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.createChargeBeeCustomer(paymentCustomerWrapper).toJson();
    }
}

Catch Apex Callout Loop not Allowed

Getting the following error: Apex Callout Loop not Allowed when you are doing a callout to the same Salesforce org and then calling out to another external service.

In this case I was doing a callout to the same salesforce org and then doing a callout to the my logging service.

RestRequest request = RestContext.request;
Map requestHeaders = request.headers;
if (requestHeaders.containsKey('Sfdc-Stack-Depth') && '1'.equals(requestHeaders.get('Sfdc-Stack-Depth'))){
      System.debug('Do not calllout as it will cause a callout loop error');
} 

Further you can write these values to a custom object and have a scheduler or workflow run to do the callout.

Apex HttpCalloutMock DML workaround

When doing a callout it has to happen before the DML action. If it happens after the DML it will throw an error or will not execute. Workaround I used is to manually keep track of the mock response, and call the respond method to intercept Http.send when running a test. By checking for Test.isRunningTest() && (mock!=null) we can send the mock response.

Checking for test running and returning mock response

	public static HttpCalloutMock mock = null;
	public HttpResponse callApiEndpoint(String apiEndpoint, String method, Object aPayload) {
		HttpRequest req = new HttpRequest();
    try {
      if (apiEndpoint != null) {
        req.setTimeout(120000);
        req.setMethod(method);
        setAuthHeader(req);
        req.setEndpoint(WebCustomSettings.Heroku_API_URL__c + apiEndpoint);
				if (aPayload!=null)
        	req.setBody(String.valueOf(aPayload));
        System.debug('Sending api request to endpoint' + apiEndpoint);
				if (Test.isRunningTest() && (mock!=null)) {
					 return mock.respond(req);
			 	} else {
					Http http = new Http();
	        return http.send(req);
				}

      } else {
        System.debug('Service apiEndpoint and payload must not be null');
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED'));
      }
    } catch (Exception ex) {
      System.debug('ERROR sending sync request to Heroku: ' + ex);
      List<String> theArgs = new List<String>();
      theArgs.add('Api');
      theArgs.add(req.getEndpoint());
      throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', ex, theArgs));
    }
  }

Test class

  Test.startTest();
        System.runAs(new User(Id=userWithAccountContact.get('UserId'))){
          //Setting the mock variable and it's also a test so will return the mock response
          Heroku_Services_Api.mock = new Heroku_Services_Api_Mock(200, 'Complete', '{}',null);
          req.requestBody = Blob.valueOf('{"id":"'+ account.Id +'", "clientId":"' + userWithAccountContact.get('ContactId') + '", "amount":122233,"endDate":"2022-01-01"}');
          req.requestURI = '/v1/account';
          req.httpMethod = 'PATCH';
          RestContext.request = req;
          RestContext.response = res;

          try{
            Rest_Dispatcher.doPatch();
          } catch(Rest_Exception ex){
            System.assertEquals(ex.itsRespCode.formattedSystemMessage, 'blah');
          }

          System.assert(res.responseBody!=null);
          System.assertEquals(res.statusCode, 200);
      }
      Test.stopTest();

Apex callout to RabbitMQ Queue

Create AMQP HTTPRequest calling POST /api/exchanges/{username}/{exchangeName}/publish

WebCustomSettings are custom settings in Salesforce the values are the following:
AMQP_Url__c: https://jellyfish.rmq.cloudamqp.com
AMQP_Credentials__c: xxxxxx:xxxxxxxxxxxxxxxxxxxxxxxx (username:password)
	public void callAmqpEndpoint(String exchangeName, String method, String aPayload) {
  	HttpRequest req = new HttpRequest();
    try {
      if (exchangeName != null) {
        req.setTimeout(120000);
        req.setMethod(method);
				setAmqpAuthHeader(req);
				System.debug(String.valueOf(aPayload));
        req.setEndpoint(WebCustomSettings.AMQP_Url__c + '/api/exchanges/' + WebCustomSettings.AMQP_Credentials__c.split(':')[0] + '/' + exchangeName + '/publish');
				if (aPayload!=null)
        	req.setBody(aPayload);
        System.debug('Sending api request to endpoint' + req.getEndpoint());
        Http http = new Http();
        http.send(req);
      } else {
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('AMQP_REQUEST_FAILED'));
      }
    } catch (Exception ex) {
      System.debug('Error sending amqp request ' + ex);
      List&lt;String&gt; theArgs = new List&lt;String&gt;();
      theArgs.add('AMQP');
      theArgs.add(req.getEndpoint());
      throw new Rest_Exception(ResponseCodes_Mgr.getCode('AMQP_REQUEST_FAILED', ex, theArgs));
    }
  }

Setup AMQP headers

private void setAmqpAuthHeader(HttpRequest aReq) {
    Blob headerValue = Blob.valueOf(WebCustomSettings.AMQP_Credentials__c);
    String authorizationHeader = 'Basic ' + EncodingUtil.base64Encode(headerValue);
    aReq.setHeader('Authorization', authorizationHeader);
		aReq.setHeader('Content-Type', 'application/json');
		aReq.setHeader('X-AMQP-Tracer', requestJson!=null &amp;&amp; requestJson.getTraceId()!=null ? requestJson.getTraceId() : '');
  }

Serialize AMQP Request JSON

	private String serializeAmqpRequests(String payload) {
		JSONGenerator generator = JSON.createGenerator(false);
		generator.writeStartObject();
		generator.writeStringField('routing_key','amqp-events');
		generator.writeFieldName('properties');
		generator.writeStartObject();
		generator.writeEndObject();
		generator.writeStringField('payload', payload);
		generator.writeStringField('payload_encoding', 'string');
		generator.writeEndObject();
		return generator.getAsString();
	}

Callout RabbitMQ

public void sendAmqpRequest(String payload){
		 String amqpPayload = serializeAmqpRequests(payload);
		 callAmqpEndpoint('event-exchange', 'POST', amqpPayload);
	}