32

Background

I'm using Apex Wrapper Salesforce Metadata API to update some picklist values on the fly. (i.e. Every time a record is inserted/updated/deleted in Table A, I'm adding/subtracting picklist values into a MultiselectePicklist in table B).

Since,

An Apex trigger can execute a callout when the callout is invoked within a method defined as asynchronous: that is, defined with the @future keyword.

I implemented a method with the future annotation, and

For Apex code that is executed asynchronously, such as @future methods, Batch Apex jobs, or scheduled Apex jobs, getSessionId returns null.

so, I needed to pass the session Id as a parameter in the method, I implemented something like:

@future (callout=true)
public static void addContactPicklistPreference(List<String> preferences, String sessionId){
    Schema.DescribeSObjectResult sObjectDescribe = Contact.sObjectType.getDescribe();
    String picklistFieldName = 'Preference__c';              

    Picklist_MDT.addPicklistValues(sObjectDescribe, picklistFieldName, artists, sessionId);
}

public class Picklist_MDT { 
    public static void addPicklistValues(Schema.DescribeSObjectResult sObjectDescribe, String picklistFieldName, List<String> picklistValueNames, String sessionId) {
        MetadataService.MetadataPort service = createService(sessionId);
        ...    
    }

     public static MetadataService.MetadataPort createService(String sessionId) {
        MetadataService.MetadataPort service = new MetadataService.MetadataPort();
        service.SessionHeader = new MetadataService.SessionHeader_element();
        service.SessionHeader.sessionId = sessionId;
        return service;
    }
}

And called the addContactPicklistPreference method from trigger context passing it the UserInfo.getSessionId() value.

Problem

This worked just fine in salesforce classic but not in lightning, when it triggers in lightning I'm getting System.CalloutException: Web service callout failed: WebService returned a SOAP Fault: INVALID_SESSION_ID.... Funny thing is that if I put a System.debug(UserInfo.getSessionId()); before calling the addContactPicklistPreference method it prints a valid session Id in both classic and lightning.

I would really appreciate any input about this.

2

6 Answers 6

37

So, as it turns outs there is no way of getting a valid API-capable Session Id from lightning component, here is a quote from the partner discussion forums:

Three things:

  1. There's no $Api global variable in Lightning Components today. So, {! $Api.whatever } will always fail.

  2. This is intentional at this time. There's no official/supported way to get an API-capable session ID in Lightning Components. Again, this is by design.

  3. With only a little creativity and cleverness you can create a bare-bones Visualforce page with {! $Api.SessionID }, and then call getContent(thatPage) from Apex to get a page that contains an API-capable session ID, which you should be able to parse out easily enough.

From there, it's not hard to pass that session ID along to Lightning Components via an @AuraEnabled method.

But let's be clear: this is a hack, the performance will be poor, and there's good reasons to keep an API-capable session ID out of your Lightning Components code.

So, I ended-up building the not recommended hack as a workaround for this to work in Lightning.

Just create a simple Visualforce Page as:

<apex:page>
    Start_Of_Session_Id{!$Api.Session_ID}End_Of_Session_Id
</apex:page>

And then get the Session Id with the visualforce_Page.getContent() method, something like:

public static MetadataService.MetadataPort createService()
{
    MetadataService.MetadataPort service = new MetadataService.MetadataPort();
    service.SessionHeader = new MetadataService.SessionHeader_element();
    //service.SessionHeader.sessionId = UserInfo.getSessionId();
    service.SessionHeader.sessionId = Utils.getSessionIdFromVFPage(Page.SessionId);
    return service;
}

Where

global class Utils {
    global static String getSessionIdFromVFPage(PageReference visualforcePage){
        String content = visualforcePage.getContent().toString();
        Integer s = content.indexOf('Start_Of_Session_Id') + 'Start_Of_Session_Id'.length(),
                e = content.indexOf('End_Of_Session_Id');
        return content.substring(s, e);
    }
}

And then you got a valid session id to work with.

6
  • 7
    interesting...amounts to the same, but I created a page like this, makes parsing a bit easier <apex:page contentType="application/json" > { "sessionId": "{!$Api.Session_ID}" } </apex:page> Jan 9, 2017 at 20:08
  • I have same requirement and I am using your trick but I am getting some error Page session id Doesn't exist . cloud you please help me . Jan 19, 2017 at 14:48
  • @NagarajaSV I think your org is having Lightning locker service enabled. This approach will not work with locker service enabled. Mar 6, 2017 at 1:09
  • One way I think might work, but haven't tried. Try firing up the event with session Id as param from vf page and then catch that in lightning component and assign it to an attribute. Mar 6, 2017 at 1:10
  • Hi @NagarajaSV, do you have 'My Domain' (under Setup - Domain Management - My Domain) enable? This will prevent you from making a callout to the same domain so the MetadataService won't work.
    – Jose
    Mar 16, 2017 at 21:45
11

There is a Salesforce-accepted way to do this. Use a Named Credential along with a Connected App and Auth Provider (which will be Salesforce). The Named Credential will be used as your endpoint instead of the hardcoded URL. You'll still manually set the Session ID, but it will be to a merge field from the Named Credential. This blog walks through every step of the process, and I can confirm I was able to get it to work this way: https://gscloudsolutions.com/tips-tricks/using-named-credentials-with-the-apex-wrapper-salesforce-metadata-api/

Your createService() method will look like this:

public static MetadataService.MetadataPort createService() {
    MetadataService.MetadataPort service = new MetadataService.MetadataPort();
    service.SessionHeader = new MetadataService.SessionHeader_element();
    service.SessionHeader.sessionId ='{!$Credential.OAuthToken}';
    return service;
}
1
7

According to the known issue Summer'17- Generating a Session Id from Lightning Domain Provides Invalid Session Id (W-3981698) it should be possible to pass UserInfo.getSessionId() out to lightning component via an @AuraEnabled an use it for API calls.

This should be possible from Winter '18 onward.

7
  • I say should here as I've had someone tell me this still isn't working for them. Mar 20, 2018 at 1:44
  • 7
    UserInfo.getSessionId() doesn't work in Summer 18 Jun 8, 2018 at 16:06
  • Can anyone please confirm if this is actually fixed. I am on ApiVersion 45. From lightning component, I am calling controller, which in turn calls the MetadataService class to deploy trigger metadata. In metadata service I am using "UserInfo.getSessionId()" but still getting the same error as mentioned here. Am I missing something?
    – MandyKool
    Aug 30, 2019 at 21:18
  • 1
    Its still not working as of 47.0
    – Mohan P
    Jan 28, 2020 at 21:02
  • Well, in Summer'20 I am getting a value from Apex in Javascript. However, good luck trying to use that session id, as all I get are errors: This session is not valid for use with the REST API. Aug 13, 2020 at 14:15
0

Note that if your Lightning Component is in a Flow Screen, you can retrieve the SessionId using a Flow formula with $Api.Session_ID and then pass it into the lightning component.

2
0

Well, there are a few ways to accomplish this requirement.

  1. use Named Credentials, and leverage the merge field '{!$Credential.OAuthToken}';
  2. Create Visualforce page and access the sessionId from the content.
  3. Create an auto launched flow that accesses the {!$Api.Session_ID} as a Formula field, then use Flow.Interview to invoke the flow, then use get variable value to retrieve the variable value.
-2

I know this question is pretty old and has been answered. However, I found a better fix for Apex that doesn't include the Visualforce hack. So I thought I would post it here for others if they come across this.

Simply place all your callouts into an internal wrapper class, making sure to set the sessionID = UserInfo.getUserId() inside the subclass, not from the controller. This apparently removes the user's session ID far enough away from the controller that it gets the correct one for the call. At least this works for me when trying to access the Tooling API.

public class MyController{

  public void getData()
  {
       ToolingClient clt = new ToolingClient();
       ToolingClient.ReturnResult = clt.getmyData();
       //do what you want with the data
  }
  public class ToolingClient{
    public ToolingClient(String strSessionId)
    {
        // set timeout
        this.timeout_x = 60 * 1000;
        //this.SessionHeader.sessionId = strSessionId;

        this.SessionHeader.sessionId = UserInfo.getSessionId();
        this.endpoint_x = RemoteSiteController.getApiUrl() + '/services/Soap/T/48.0';
    }
  //put all the callouts 
   }

}

1
  • This did not work for me in v52 making a callout to the metadata api. Aug 4, 2021 at 19:46

You must log in to answer this question.

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