3

So I implemented a callout to an external service where first we should validate that our token wasn't expired. So I implemented a refresh token method, that updated our custom setting where we stored that info.

So here's the thing, since I wanted to avoid the uncommited work pending I moved the dml to update the Custom setting to the end of my code.

Months after setting this up and explaining to another peer how I did it I realized my code shouldn't be working at all. The modification of my custom setting wasn't being neither passed as a parameter or stored in database, but when I accessed it, it was updated.

Here's the code :

Class 1:

@future(callout=true)
public static void integrate(Id accId){
    Account a = [IRRELEVANT QUERY];
    Quiter_Token__c tokenT = QuiterTokenBearer.getToken();
    if(String.isBlank(a.OM_VCAR_ExternalId__c)){
        QuiterWS.sendAccountsCreate(a,tokenT.token__c);   
    }
    else{
        QuiterWS.sendAccountsUpdate(a,tokenT.token__c);   
    }
    update tokenT;
}

Class QuiterTokenBearer:

public static Quiter_Token__c getToken(){
    /*
     * Returns the token for authentification to Quiter, either using the last token retrieved if it's still valid or requesting a new one
     */ 
    if(isTokenExpired()){
        refreshToken();
    }
    Quiter_Token__c token = Quiter_Token__c.getValues('Quiter Token');
    return token;
}

public static void refreshToken() {
    /*
     * Retrieves a refreshed token and store it, alongside its expiration window and datime of reception on Quiter_Token__c
     */ 
    Quiter_Token__c token = Quiter_Token__c.getValues('Quiter Token');
    System.debug('Token : ' + token);
    Http http = new Http();
    HttpRequest request = new HttpRequest();
    request.setEndpoint(token.Url__c);
    request.setHeader('Accept','application/json'); 
    String payload = 'client_id='+EncodingUtil.urlEncode(token.Client_ID__c,'UTF-8')+'&client_secret='+EncodingUtil.urlEncode(token.Secret__c,'UTF-8')+'&code='+EncodingUtil.urlEncode(token.Code__c,'UTF-8')+'&grant_type=authorization_code';
    request.setBody(payload);
    request.setMethod('POST');
    HttpResponse response = new HttpResponse();
    if(!Test.isRunningTest()){
        response = http.send(request);
    }else{
        response = QuiterWSMock.respond(request);
    }
    
    if (response.getStatusCode() == 200) {
        System.debug(response.getBody());
        Map<String, Object> json = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
        token.Token__c = (String)json.get('access_token');
        token.Expires_In__c = (Decimal) json.get('expires_in');
        token.Last_Token_Time__c = system.now();
    }
}

public static Boolean isTokenExpired(){
    /*
     * This function returns false if System.Now - Last time token was retrieved is greater than the token expiration window and true otherwise
     */ 
    Quiter_Token__c token = Quiter_Token__c.getValues('Quiter Token');
    if(token.Token__c!=null && token.Last_Token_Time__c != null && token.Expires_in__c !=null){
        System.debug((System.now().getTime()-token.Last_Token_Time__c.getTime())/1000  > token.Expires_in__c);
        return (System.now().getTime()-token.Last_Token_Time__c.getTime())/1000  > token.Expires_in__c;
    }
    return true;
}

If you check the function refreshToken you see the custom setting gets modified with the received values on the response of the callout, but never gets passed to getToken, but still when we retrieve the values again from the custom setting the values ARE THERE.

I also tried this setting a system.debug just after

Quiter_Token__c tokenT = QuiterTokenBearer.getToken();

on integrate function, and , yes, the value is stored across the whole Transaction, but only stored in memory if the dml is performed.

I searched information about this but couldn't find anything that adresses custom settings behaving different than Standard/Custom Objects in this matter. If more information needs to be clarified please, ask on the comments and I'll try to provide more context as required.

Why is this code working?

6
  • 1
    You updated the Quiter_Token__c settings returned from Quiter_Token__c.getValues('Quiter Token'); in refreshToken. I'm guessing that the in-memory cache of this is directly updated, so all later calls get this same cached in-memory object, that includes your changes.
    – Phil W
    Mar 31, 2021 at 13:58
  • Yeah that's my guess aswell. But why there's no info of this anywhere? Mar 31, 2021 at 14:14
  • 1
    Yet another undocumented behaviour (which therefore could change at any time).
    – Phil W
    Mar 31, 2021 at 14:36
  • 1
    The read behavior (cache vs "database") is thinly documented. update on CS as an SObject is not documented at all. Updating through UI and its effect on in-memory cache would be an interesting experiment. Bug or not, if you can switch to Custom Metadata, you'd be better off.
    – identigral
    Mar 31, 2021 at 15:48
  • 1
    @identigral I wouldn't say this behaviour is documented at all (from my reading of the page you cite); just because there's caching (so the DB isn't hit) doesn't mean you necessarily get the same in-memory record each time. Indeed, I resort to cloning data to be exposed outside APIs where I don't want the caller to be able to mess up my behind-the-API-data... so, that all said, I see The Fox's excellent bit of experimentation and suggested approach documented in a fine answer.
    – Phil W
    Mar 31, 2021 at 16:54

1 Answer 1

4

I would expect this behavior in the general sense. The settings are loaded in memory, then calling get or getAll returns that cached data. However, being undocumented, I'd avoid specifically depending on this behavior and being more explicit:

public class QuiterTokenBearer {
  public static Quiter_Token__c token = Quiter_Token__c.getValues('Quiter Token');
  // Modify rest of code to use this static variable instead of local copy

Now, you can guarantee, logically, that the token is up-to-date automatically without depending on undocumented behavior.


I actually wrote a test, and confirmed that this does work if the custom setting exists, but not if it doesn't.

Setting__c s1 = Setting__c.getInstance('ExistingValue');
Setting__c s2 = Setting__c.getInstance('ExistingValue');
Setting__c s3 = Setting__c.getInstance('NonExistingValue');
Setting__c s4 = Setting__c.getInstance('NonExistingValue');

System.assert(s1 === s2); // Same memory object
System.assert(s3 !== s4); // Not same memory object

s1.SomeField__c = 'Hello World';
s3.SomeField__c = 'Hello World';
System.assert(s1.SomeField__c == s2.SomeField__c);
System.assert(s3.SomeField__c != s4.SomeField__c);

So, a cautionary tale: make sure your setting exists, or you'll get the behavior you expect (e.g. in a unit test where you forget to create the record). Using a specific static variable in memory avoids this potential problem.

1
  • Nice! I have to say, we do have our own Settings manager which does itself locally cache the settings records on first access in a session. It also makes sure to merge in default values as needed, but that's a different story.
    – Phil W
    Mar 31, 2021 at 16:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.