Skip to content

Commit

Permalink
Callout 1.0.1 - Fixed framework ignoring CalloutException not related…
Browse files Browse the repository at this point in the history
… to endpoint response
  • Loading branch information
pkozuchowski committed Jun 30, 2024
1 parent c2e0212 commit e11cf95
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 138 deletions.
271 changes: 143 additions & 128 deletions docs/Callout.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

[Source](https://github.com/pkozuchowski/Apex-Opensource-Library/tree/master/force-app/commons/callout)
[Dependency](/apex/runtime)
[Install In Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t08000000ga94AAA)
[Install In Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t08000000ga94AAA)
[Install In Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t08000000UK6VAAW)
[Install In Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t08000000UK6VAAW)

```bash
sf project deploy start \
Expand All @@ -29,68 +29,75 @@ All calls to the API should have streamlined workflow - callouts should be autho
To implement this configuration, we could derive a new class from Callout class and configure it as follows:
```apex
public class AcmeApiCallout extends Callout {
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onBeforeCallout()
.add(match.once(), authorizationHandler);
onAfterCallout()
.add(match.onUnauthorized(), authorizationHandler)
.add(match.onUnauthorized(), action.retry(1))
.add(match.onTimeout(), action.retry(1))
.slot('beforeValidation')
.add(match.onAnyErrorCode(), action.logCallout(LoggingLevel.ERROR))
.add(match.onAnyErrorCode(), action.throwEx())
.add(match.onSuccess(), action.logCallout(LoggingLevel.INFO))
.add(match.onSuccess(), action.returnJSON(responseType));
}
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onBeforeCallout()
.add(match.once(), authorizationHandler);
onAfterCallout()
.add(match.onUnauthorized(), authorizationHandler)
.add(match.onUnauthorized(), action.retry(1))
.add(match.onTimeout(), action.retry(1))
.slot('beforeValidation')
.add(match.onException(), action.logCallout(LoggingLevel.ERROR))
.add(match.onException(), action.throwEx())
.add(match.onAnyErrorCode(), action.logCallout(LoggingLevel.ERROR))
.add(match.onAnyErrorCode(), action.throwEx())
.add(match.onSuccess(), action.logCallout(LoggingLevel.INFO))
.add(match.onSuccess(), action.returnJSON(responseType));
}
}
```

Let's break this down:
- Before Callout:
1. Runs custom authorization handler once. This is example class that would generate Oauth token for us if Named Credential can't be used.
It's just an example of custom handler, it's not necessary to write any in most cases.
1. Runs custom authorization handler once. This is example class that would generate Oauth token for us if Named Credential can't be used.
It's just an example of custom handler, it's not necessary to write any in most cases.


- After Callout:
1. If response returned 401 Unauthorized, run authorization handler again
1. Retry callout once again with new authorization token
1. On timeout, retry once again
1. Slot named "beforeValidation" - this does nothing, but can be used for injecting handlers later in this place
1. If webservice responded with error codes (400-599), creates log record with ERROR severity
1. If webservice responded error code, throw CalloutResponseException
1. If webservice responded with success code, log callout with INFO severity
1. If webservice responded with success code, deserialize response body to given apex type.
1. If response returned 401 Unauthorized, run authorization handler again
1. Retry callout once again with new authorization token
1. On timeout, retry once again
1. Slot named "beforeValidation" - this does nothing, but can be used for injecting handlers later in this place
1. If any CalloutException was thrown, creates log record with ERROR severity
1. If any CalloutException was thrown, throws CalloutResponseException
1. If webservice responded with error codes (400-599), creates log record with ERROR severity
1. If webservice responded error code, throws CalloutResponseException
1. If webservice responded with success code, log callout with INFO severity
1. If webservice responded with success code, deserialize response body to given apex type.

## Usage in API class
The callout class can then be used in methods which expose particular endpoints.
```apex
public class AcmeCustomerAPI {
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.setMethod('GET');
c.setEndpoint('callout:MyCredential/api/Customer');
c.setParam('id', accountIds, true);
c.setResponseType(List<Customer>.class);
c.onAfterCallout()
.addToSlot('beforeValidation',
c.match.onNotFound(), c.action.returns(new List<Customer>())
);
return (List<Customer>) c.execute();
}
public Customer updateCustomer(Customer customer) {
Callout c = new AcmeApiCallout();
c.setMethod('POST');
c.setEndpoint('callout:MyCredential/api/Customer');
c.setBodyJSON(account);
c.setResponseType(Account.class);
return (Account) c.execute();
}
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.setMethod('GET');
c.setEndpoint('callout:MyCredential/api/Customer');
c.setParam('id', accountIds, true);
c.setResponseType(List<Customer>.class);
c.onAfterCallout()
.addToSlot('beforeValidation',
c.match.onNotFound(), c.action.returns(new List<Customer>())
);
return (List<Customer>) c.execute();
}
public Customer updateCustomer(Customer customer) {
Callout c = new AcmeApiCallout();
c.setMethod('POST');
c.setEndpoint('callout:MyCredential/api/Customer');
c.setBodyJSON(account);
c.setResponseType(Account.class);
return (Account) c.execute();
}
}
```
In the above example, **slot** functionality was utilized to return an empty list when webservice responds with 404 Not Found.
Expand All @@ -106,32 +113,32 @@ Consider the following setup:

```apex | API-specific Callout configuration
public class AcmeApiCallout extends Callout {
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onAfterCallout()
.add('authorize', match.onUnauthorized(), authorizationHandler)
.add('authorizeRetry', match.onUnauthorized(), action.retry(1))
.add('timeoutRetry', match.onTimeout(), action.retry(1))
.add(match.onSuccess(), action.logCallout(LoggingLevel.INFO))
.add(match.onSuccess(), action.returnJSON(responseType));
}
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onAfterCallout()
.add('authorize', match.onUnauthorized(), authorizationHandler)
.add('authorizeRetry', match.onUnauthorized(), action.retry(1))
.add('timeoutRetry', match.onTimeout(), action.retry(1))
.add(match.onSuccess(), action.logCallout(LoggingLevel.INFO))
.add(match.onSuccess(), action.returnJSON(responseType));
}
}
```

In one of the calls, we will remove an `authorizeRetry` step and replace retry with logging action. We will also retry on timeout 5 times instead of once.
```apex | Client Code
public class AcmeCustomerAPI {
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.onAfterCallout()
.remove('authorize')
.replace('authorizeRetry', c.action.logCallout(LoggingLevel.ERROR))
.replace('timeoutRetry', c.action.retry(5));
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.onAfterCallout()
.remove('authorize')
.replace('authorizeRetry', c.action.logCallout(LoggingLevel.ERROR))
.replace('timeoutRetry', c.action.retry(5));
return (List<Customer>) c.execute();
}
return (List<Customer>) c.execute();
}
}
```

Expand All @@ -140,15 +147,15 @@ List of handlers can be defined with a slot - placeholder in which we can later

```apex | API-specific Callout configuration
public class AcmeApiCallout extends Callout {
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onAfterCallout()
.slot('beforeValidation')
.add(match.onAnyErrorCode(), action.logCallout(LoggingLevel.ERROR))
.add(match.onAnyErrorCode(), action.throwEx())
.add(match.onSuccess(), action.returnJSON(responseType));
}
private AcmeAPIAuthHandler authorizationHandler = new AcmeAPIAuthHandler();
protected override void setupHandlers() {
onAfterCallout()
.slot('beforeValidation')
.add(match.onAnyErrorCode(), action.logCallout(LoggingLevel.ERROR))
.add(match.onAnyErrorCode(), action.throwEx())
.add(match.onSuccess(), action.returnJSON(responseType));
}
}
```

Expand All @@ -158,15 +165,15 @@ Add handler to the slot which does following:
```apex
public class AcmeCustomerAPI {
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.onAfterCallout()
.addToSlot('beforeValidation',
c.match.onNotFound(), c.action.returns(new List<Customer>())
);
public List<Customer> getCustomers(List<String> accountIds) {
Callout c = new AcmeApiCallout();
c.onAfterCallout()
.addToSlot('beforeValidation',
c.match.onNotFound(), c.action.returns(new List<Customer>())
);
return (List<Customer>) c.execute();
}
return (List<Customer>) c.execute();
}
}
```

Expand All @@ -178,23 +185,23 @@ Callout Framework can be easily extended by implementing two interfaces for matc

```apex | Condition | Generic Condition interface is used to check if Callout satisfies the condition for associated action.
public interface Condition {
Boolean isTrue(Object item);
Boolean isTrue(Object item);
}
```

```apex | Callout.Handler | Represents action to perform.
public interface Handler {
Object handle(Callout c);
Object handle(Callout c);
}
```

The Framework works as follows - when callout is executed():
1. Iterate through pairs of Condition-Handler
1. If the Condition returns true:
1. Execute Handler and check return value:
1. If `null` - continue iteration over actions.
1. If not null - return this immediately as response from callout `execute` method.
1. If throws exception, breaks the code execution - this exception has to be handled in client code.
1. Execute Handler and check return value:
1. If `null` - continue iteration over actions.
1. If not null - return this immediately as response from callout `execute` method.
1. If throws exception, breaks the code execution - this exception has to be handled in client code.

Callout has two lists of handlers - one executed before and one after the callout.

Expand All @@ -205,36 +212,36 @@ Callout has two lists of handlers - one executed before and one after the callou
* Matches Response body that contains substring
*/
private class SubstringMatcher implements Condition {
private String substring;
private String substring;
private SubstringMatcher(String substring) {
this.substring = substring;
}
private SubstringMatcher(String substring) {
this.substring = substring;
}
public Boolean isTrue(Object item) {
Callout c = (Callout) item;
public Boolean isTrue(Object item) {
Callout c = (Callout) item;
return c.getResponse()?.getBody()?.containsIgnoreCase(substring) == true;
}
return c.getResponse()?.getBody()?.containsIgnoreCase(substring) == true;
}
}
```

```apex | Example of Handler class
private class RetryHandler implements Callout.Handler {
private Integer attempt = 0, maxAttempts;
private Integer attempt = 0, maxAttempts;
public RetryHandler(Integer howManyTimes) {
maxAttempts = howManyTimes;
}
public RetryHandler(Integer howManyTimes) {
maxAttempts = howManyTimes;
}
public Object handle(Callout c) {
if (attempt < maxAttempts) {
attempt++;
return c.execute();
}
public Object handle(Callout c) {
if (attempt < maxAttempts) {
attempt++;
return c.execute();
}
return null;
}
return null;
}
}
```

Expand Down Expand Up @@ -284,14 +291,14 @@ private class RetryHandler implements Callout.Handler {
## Condition
```apex | Condition
public interface Condition {
Boolean isTrue(Object item);
Boolean isTrue(Object item);
}
```

## Callout.Handler
```apex | Callout.Handler
public interface Handler {
Object handle(Callout c);
Object handle(Callout c);
}
```

Expand All @@ -300,21 +307,21 @@ public interface Handler {
- It's not required to extend Callout class. It can be used as is or configured without inheritance:
```apex
public Callout getAcmeCallout() {
Callout c = new Callout();
c.onBeforeCallout()
.add(c.match.once(), authorizationHandler);
c.onAfterCallout()
.add(c.match.onUnauthorized(), authorizationHandler)
.add(c.match.onUnauthorized(), c.action.retry(1))
.add(c.match.onTimeout(), c.action.retry(1))
.slot('beforeValidation')
.add(c.match.onAnyErrorCode(), c.action.logCallout(LoggingLevel.ERROR))
.add(c.match.onAnyErrorCode(), c.action.throwEx())
.add(c.match.onSuccess(), c.action.logCallout(LoggingLevel.INFO))
.add(c.match.onSuccess(), c.action.returnJSON(responseType));
return c;
Callout c = new Callout();
c.onBeforeCallout()
.add(c.match.once(), authorizationHandler);
c.onAfterCallout()
.add(c.match.onUnauthorized(), authorizationHandler)
.add(c.match.onUnauthorized(), c.action.retry(1))
.add(c.match.onTimeout(), c.action.retry(1))
.slot('beforeValidation')
.add(c.match.onAnyErrorCode(), c.action.logCallout(LoggingLevel.ERROR))
.add(c.match.onAnyErrorCode(), c.action.throwEx())
.add(c.match.onSuccess(), c.action.logCallout(LoggingLevel.INFO))
.add(c.match.onSuccess(), c.action.returnJSON(responseType));
return c;
}
```

Expand All @@ -324,9 +331,17 @@ onAfterCallout()
.add(match.onUnauthorized(), action.retry(1))
.add(match.onTimeout(), action.retry(1))
.slot('beforeValidation')
.add(match.onException(), action.logCallout(LoggingLevel.ERROR))
.add(match.onException(), action.throwEx())
.add(match.onAnyErrorCode(), action.logCallout(LoggingLevel.ERROR))
.add(match.onAnyErrorCode(), action.throwEx())
.add(match.onSuccess(), action.logCallout(LoggingLevel.INFO))
.add(match.onSuccess(), action.returnJSON(responseType));
```
- Client code can remove or replace particular handlers. Name can be added to the handler, which then can be used to remove/replace.
- Client code can remove or replace particular handlers. Name can be added to the handler, which then can be used to remove/replace.

---
# Change Log

### 1.0.1
* Fixed bug where CalloutException was not handled properly and resulted in framework returning null.
Loading

0 comments on commit e11cf95

Please sign in to comment.