function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
Kenji775Kenji775 

Internal Salesforce Error on sObject list insert

Hey all,

I posted a bit about this on twitter but I figured I'd make a thread to get some more details. What's happening is that I have a method (it's an Apex Rest method if that matters) whose job is to insert a list of sObjects. For some reason, attempting to do so causes an uncatchable Internal Salesforce Error. Weird part is, if I remove the offending code, and run it by itself, it works just fine. The object in question here is a custom object called survey_answer__c. These are created by deserializing some JSON from a remote webservice. For example

[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]

 

 Contains data for two answer objects to get inserted. So I deserailize it, which goes just fine. Now I have a list of sObjects. I attempt to insert it using the plain old insert command and I get my error. Now the weird part is if I mimic this setup by itself in an execute anonymous block using the following code, it will run fine. You'll notice all the data for the objects is exactly the same, except for the second set has an Id for the survey_entry__c relationship field (which is appended to each sObject in the first example later on by iterating the list of sObjects and setting the field manually).

list<Survey_Answer__c> answers = new list<Survey_Answer__c>();

Survey_Answer__c ans1 = new Survey_Answer__c();
ans1.Question__c= 'what is blah 1?';
ans1.Id__c='51345X1607X53053';
ans1.Survey_Entry__c='a0zS0000001HSavIAG';
ans1.Answer__c='2.0';

Survey_Answer__c ans2 = new Survey_Answer__c();
ans2.Question__c= 'what is blah 2?';
ans2.Id__c='51345X1607X53100';
ans2.Survey_Entry__c='a0zS0000001HSavIAG';
ans2.Answer__c='3.0';

answers.add(ans1);
answers.add(ans2);

insert answers;

 

 So there you have it. Random error for no good reason. I'll post the entire class below for you to review. If you want the object definiton files to play with it yourself, let me know and I can host them somewhere.

 

@RestResource(urlMapping='/importSurvey/*') 
global class importSurveyData 
{
	public static boolean isApexTest = false;
	
	//this method takes a post request with a survey id, a user token and other paramters about the survey. It reaches out to another 
	//webservice which gets the survey answer data and returns it. The data is used to generate survey_answer__c objects and insert them.
	//It will also create a survey__c and survey_entry__c object if required (if they don't exist). Each campaign should have only one survey__c object
	//which can contain multiple survey entries (one per person who took the survey). Each survey entry can contain multiple answers. Each contact will
	//have only one survey entry for a survey
	
	/* Schema
	
	Campaign
	|
	+-> Survey__c 
	    |
	    +-> Survey_Entry__c <- Contact 
	        |
	        +->Survey_Answer__c
	*/
	@HttpPost
	global static list<Survey_Answer__c> doPost(RestRequest req, RestResponse res) 
	{
		list<Survey_Answer__c> answers;
		String jsonString;	
		Survey_Entry__c surveyEntry = new Survey_Entry__c();	
		
		//Get the ID of the survey we are working with from the URL
		string survey = req.params.get('survey');
		
		//Loop over all the parameters passed in and try to assign them to the survey object. Just discard any unusable data
		for(string param : req.params.keySet())
		{
			try
			{
				surveyEntry.put(param,req.params.get(param));
			}
			catch(Exception e)
			{
				
			}
		}
		
		//We have to call out to a web service to get the lime survey answer data. It will get returned as a JSON encoded array
		//of answer objects. These can be directly de-serialized to survey_answer__c objects. They contain all the actual answers
		//to the questions as well as data about the questions itself
        try 
        {           
            
            HttpRequest getDataReq = new HttpRequest();
            getDataReq.setMethod('GET'); 
            getDataReq.setEndpoint( 'http://xxxxxxxxxxxxxxxxxxxxxxxx.com/?method=getLimeSurveyAnswers&surveyId='+survey+'&Token='+surveyEntry.token__c);
              
            Http http = new Http();

			if(!isApexTest)
			{
            	//Execute web service call here     
            	HTTPResponse getDataRes = http.send(getDataReq);   
            
	            if(getDataRes.getStatusCode() == 200)
	            {
					jsonString = getDataRes.getBody();  
	            }
	            else
	            {
	                system.debug('Could not contact web service');
	            }
			}
			else
			{
				system.debug('Apex Test Mode. Webservice not Invoked');
				jsonString = '[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]';
			}
			
			//This is just put here for testing. It's faster and more reliable for testing to just have the same JSON content every time
			//in production this line would be removed
			jsonString = '[{"Answer__c":2.0,"Id__c":"51345X1607X53053","Question__c":"what is blah 1?"},{"Answer__c":3.0,"Id__c":"51345X1607X53100","Question__c":"what is blah 2?"}]';		
			
			//deserialize the list of answers retreived from the webservice call
			answers = (List<Survey_Answer__c>) JSON.deserialize(jsonString, List<Survey_Answer__c>.class);	
			
			//Now we need to find the ID of the survey to attach these answers to. This method will either find the existing
			//survey_entry__c id (based on token, contact, and lime survey id) or create a new one and return it. Either way
			//it's going to return a survey_entry__c object (it will also create a survey object to attach itself to if there isn't one)					
			Id surveyEntryId = importSurveyData.getSurveyEntry(surveyEntry, survey).id;									
			
			//We don't want duplicate data, and this may get run more than once, so delete any existing answer data for this survey entry	
			List<Survey_Answer__c> toDelete = [select id from Survey_Answer__c where Survey_Entry__c = :surveyEntryId];
			delete toDelete;
			
			//We now also need to update these survey answer objects with the survey_entry__c id so the relationship gets set
			for(Survey_Answer__c ans : answers)
			{
				ans.Survey_Entry__c = surveyEntryId;
			}	
				
			insert answers;	//This line errors for no discernable reason.		        
        } 
        catch(Exception e) 
        {
            system.debug('======================== ERROR IMPORTING ANSWERS: ' +e.getMessage() + ' ' +e.getCause() + ' ' +e.getTypeName());
        } 
        		
		return answers;
	}
	
	//this will either find an existing survey_entry__c or create a new one if one does not exist.
	//it will also request creation of a survey__c object to attach itself to if one does not exist.
	global static Survey_Entry__c getSurveyEntry(Survey_Entry__c surveyEntry, string surveyId)
	{	
		//try to find existing survey entry for this person 	
		list<Survey_Entry__c> entries = [select id, 
												Survey__c, 
												token__c, 
												contact__c 
										 from Survey_Entry__c 
										 where survey__r.study__r.lime_survey_id__c = :surveyId 
										 and contact__c = :surveyEntry.contact__c 
										 limit 1];
										 	
		//if a survey entry is not found, create one otherwise return existing
		if(entries.isEmpty())
		{				
			
			surveyEntry.Survey__c = getSurvey(surveyId).id;
			insert surveyEntry;		
			return surveyEntry;
		}	
		else
		{
			return entries[0];	
		}
		
	}
	
	//create a survey for the given campaign if does not exist, otherwise return the existing one.
	global static Survey__c getSurvey(string surveyId)
	{
		//find the existing survey if there is one for this id
		list<Survey__c> surveyObj = [select id 
									 from Survey__c 
									 where Study__r.Lime_Survey_ID__c = :surveyId 
									 limit 1];		
		if(surveyObj.isEmpty())
		{
			Survey__c survey = new Survey__c();
			RecordType ParentRecordType = [select id from RecordType where name = 'FPI Parent Campaign' ];
			list<Campaign> parentCampaign = [select id from Campaign where Lime_Survey_ID__c = :surveyId and recordTypeId = :ParentRecordType.id order by createdDate asc limit 1];
			
			survey.Study__c = parentCampaign[0].id;		
			insert survey;
			return survey;
		}	
		else
		{
			return surveyObj[0];
		}						 
	}
    @isTest 
    public static void importSurveyDataTest()
    {
    	isApexTest = true;
    	string surveyID = '51345';
    	
        //Insert the check record, with testContact as the contact
        Account thisTestAccount = testDataGenerator.createTestAccount();
        Contact thisTestContact = testDataGenerator.createTestContact(thisTestAccount.id);
        Campaign thisTestUmbrellaCampaign = testDataGenerator.createTestUmbrellaCampaign(thisTestContact.id);
        Campaign thisTestParentCampaign = testDataGenerator.createTestParentCampaign(thisTestUmbrellaCampaign.id, thisTestContact.id); 
        
        thisTestParentCampaign.Lime_Survey_ID__c = surveyID;
        
        update thisTestParentCampaign;

        RestRequest req = new RestRequest(); 
        RestResponse res = new RestResponse();
 
	
 		//Do a sample request that should invoke most of the methods 
        req.requestURI = 'https://cs1.salesforce.com/services/apexrest/importSurvey';
        req.httpMethod = 'POST';	 
		req.addParameter('survey',surveyID);
		req.addParameter('token__c','mkwcgvixvxskchn'); 
		req.addParameter('contact__c',thisTestContact.id);
		req.addParameter('type__c','survey');
		req.addParameter('label__c','test survey');
		req.addParameter('result__c','qualified');
		
        list<Survey_Answer__c> importedAnswers = doPost(req,res); 
        
        system.debug(importedAnswers);
        //The first thing it should have done is create the survey object itself, attached to the master campaign.
        list<Survey__c> testSurvey = [select id from Survey__c where study__r.Lime_Survey_ID__c = :surveyID];
        system.assertEquals(1,testSurvey.size());
        
        //now it also should have created a survey entry for this person, with the attributes passed in the request
        list<Survey_Entry__c> testEntry = [select id,type__c,label__c,result__c,token__c from Survey_Entry__c where contact__c = :thisTestContact.id and survey__c = :testSurvey[0].id];
        system.assertEquals('survey',testEntry[0].type__c);
        system.assertEquals('test survey',testEntry[0].label__c);
        system.assertEquals('qualified',testEntry[0].result__c);
        system.assertEquals('mkwcgvixvxskchn',testEntry[0].token__c);
        
        //Also all the survey answers should have been imported. There should be two of them.        
   		list<Survey_Answer__c> answers = [select id, Answer__c, Question__c from Survey_Answer__c where Survey_Entry__c = :testEntry[0].id];
   		system.assertEquals(2,answers.size());
    }
    
}

 

Kenji775Kenji775

Also, this is the code block I've been using to test the method while I've been attempting to debug this.

string surveyID = '51345';

RestRequest req = new RestRequest(); 
RestResponse res = new RestResponse();
 
	
//Do a sample request that should invoke most of the methods 
req.requestURI = 'https://cs1.salesforce.com/services/apexrest/importSurvey';
req.httpMethod = 'POST';	 
req.addParameter('survey',surveyID);
req.addParameter('token__c','mkwcgvixvxskchn'); 
req.addParameter('contact__c','003S000000OJ82B');
req.addParameter('type__c','survey');
req.addParameter('label__c','test survey');
req.addParameter('result__c','qualified');

list<Survey_Answer__c> importedAnswers =  importSurveyData.doPost(req,res);

 

Also, the getSurvey and getSurvey entry method have been tested been themselves and operate as expected. They are able to create the survey and survey entry, but errors on the answer insertion. Not sure if it's expected behavior or not, but it does seem to roll back the database changes when the error happens. Meaning it does create the survey and survey entry objects during execution but as soon as it errors, they are removed/deleted.

 

Kenji775Kenji775

One more bit of info. Doing a system.debug on the answers list right before insert prints off the object data as follows.

13:36:00:100 USER_DEBUG [99]|DEBUG|(

Survey_Answer__c:{Question__c=what is blah 1?, Id__c=51345X1607X53053, Survey_Entry__c=a0zS0000001HSavIAG, Answer__c=2.0},

Survey_Answer__c:{Question__c=what is blah 2?, Id__c=51345X1607X53100, Survey_Entry__c=a0zS0000001HSavIAG, Answer__c=3.0})

 And here are a few screenshots 
(http://i.imgur.com/5mRkK.png)



(http://i.imgur.com/qNNnA.png

Kenji775Kenji775

One more post. Here is the full stack trace as well.

http://pastebin.com/raw.php?i=nsALrCZx 

@altius_rup@altius_rup
Have you tried the same test with answers to 2 different Survey Entries ? Rup
SuperfellSuperfell

I logged a bug with the apex team for this one, its not happy with the state of your SObjects, but no idea why.

rungerrunger

Thanks for the bug, Simon.  It's a bug in the new runtime, when doing DML on a list constructed by json.deserialize().

 

While we get this fixed, you can use the following workaround:

 

list<survey_answer__c> answers = new list<survey_answer__c>();
answers.addAll((List<Survey_Answer__c>) JSON.deserialize(jsonString, List<Survey_Answer__c>.class)); 

Kenji775Kenji775

This workaround seemed to work for me. Thank you very much for that. I can finally get this code out in production! Always a good feeling :)
 

What would be the equivilent for deserializing to a single custom class, called limeSurveyWebServiceObject. I think I am having what equats to basically the same bug in another class while deserializing some JSON into a custom class type. 

rungerrunger

This particular bug is only applicable to lists.  I'd have to see a failing piece of sample code to diagnose your other bug.

Kenji775Kenji775

Ah, I was probably mistaken in my guess then. It's that classic 'Don't know type of object to deserialize' error. No workaround I've tried has worked for that one yet. 

rungerrunger

Is it happening consistently or sporadically?  Which instance are you on?