19

Consider this very simple class for querying accounts:

@RestResource(urlMapping='/accounts/*')
global with sharing class AccountRestService {
    @HttpGet
    global static List<Account> getAccounts() {
        List<Account> accounts = [SELECT Id, Name FROM Account];

        return accounts;
    }
}

Easy - I do a GET and get a list of all my accounts.

Now - for some reason this query might fail and we want to correctly signal this to the one using the interface. So in this case we want to return a 500 with the error message as our body.

@RestResource(urlMapping='/accounts/*')
global with sharing class AccountRestService {
    @HttpGet
    global static List<Account> getAccounts() {
        List<Account> accounts;

        try {
            accounts = [SELECT Id, Name FROM Account];
        } catch (Exception e) {
            RestResponse res = RestContext.response;
            res.responseBody = Blob.valueOf(e.getMessage());
            res.statusCode = 500;
        }

        return accounts;
    }
}

This looks nice - however the return statement will overwrite our response in case of an error which is clearly not what we want. Of course you can make it void and do the returning of the data (and the serialization) all by yourself - but that seems a lot of work for something that's done and should be done by default. Also - all the examples I found so far do have return values but do not handle these cases in any way.

Am I missing something obvious here? Since I never know how my methods will eventually evolve that just leaves me with all void methods since I cannot change the signature of global methods in a managed package.

28

Having written a Salesforce managed package app making use of the automatic serialization and deserialization based on the method signature, in future I will avoid that pattern. The primary reason is that it is then impossible to change the format of the data (because of managed package version compatibility constraints) as the OP mentions, but it also limits the error handling choices. (And the return of JSON_PARSER_ERROR errors rather than an opaque error for invalid JSON requests also raised some security review flags.)

I recommend this approach (providing you are happy to stick with JSON):

@RestResource(urlMapping='/accounts/*')
global with sharing class AccountRestService {
    @HttpGet
    global static void getAccounts() {
        RestResponse res = RestContext.response;
        try {
            List<Account> accounts = [SELECT Id, Name FROM Account];
            res.responseBody = Blob.valueOf(JSON.serialize(accounts));
            res.statusCode = 200;
        } catch (Exception e) {
            res.responseBody = Blob.valueOf(e.getMessage());
            res.statusCode = 500;
        }
    }
}

The pain of a few extra lines of code is more than outweighed by the flexibility to change the requests and responses as needed in the future. And it allows a more RESTful style of error handling.

  • To be pedantic, you should be returning a 400 error code if the JSON fails to parse, because it's a client error, not a server error. – sfdcfox Aug 31 '16 at 21:05
  • 1
    @sfdcfox Good point about respecting the status codes; in this case with no request JSON perhaps 500 is the better choice... – Keith C Aug 31 '16 at 21:24
  • 3
    That is exactly the solution I've come up with as well. It's amazing how many pitfalls you find in everyday Apex - just like in this little example - that are really going to give you a hard time especially when you're building a managed package. Plus: There are no caveats for problems like this mentioned anywhere in the Salesforce documentation! Back to topic: The solution is elegant and flexible and I think that this is the way to go as it provides you the most control even later in the life of the interface. In theory you could also serialize to XML if needed. Thanks for your feedback! – Semmel Aug 31 '16 at 21:25
  • @sfdcfox I agree - good point. But why would you return a 400 and not a 500? And there's no JSON being parsed. – Semmel Aug 31 '16 at 21:28
  • @Semmel Oh, actually, I misread the code. Silly me. Still, there's no point in a try-catch here, as the only exceptions you can possibly get would be governor limits you couldn't catch anyways. – sfdcfox Aug 31 '16 at 21:42
6

You can write a Wrapper object instead of the actual object .

Create a wrapper class like below

public class ResponseWrapper{
    public list<Account> lstaccounts;
    public boolean isError ;
    public string errorCode;
    public integer statusCode;
       public ResponseWrapper(){
         lstaccounts = new list<Account>();
         isError= false;
    }

}

Return the wrapper object instead

@RestResource(urlMapping='/accounts/*')
global with sharing class AccountRestService {
@HttpGet
global static ResponseWrapper getAccounts() {
    ResponseWrapper resWrap = new ResponseWrapper();
    List<Account> accounts;

    try {
        accounts = [SELECT Id, Name FROM Account];
        res.lstaccounts = accounts;
    } catch (Exception e) {
        RestResponse res = RestContext.response;
        resWrap.isError = true;
        res.responseBody = Blob.valueOf(e.getMessage()) = resWrap.errorCode;
        res.statusCode = 500 = resWrap.statusCode;
    }

    return resWrap;
  }
}
  • I think this is a feasible solution if you want some kind of standardized response across all your REST interfaces. However to ease the pain with and help with the development of managed packages I personally think that explicit is better than implicit - particularly in this case - so I'm going with the manual route. – Semmel Aug 31 '16 at 21:35
  • This solution isn't working for me - I keep getting the following error: HttpGet methods do not support return type of ResponseWrapper – Brian Miller Apr 14 at 19:27
0

It’s cleaner to throw a specific exception that contains your httpstauscode and return it and the message from the exception from an exception filter. This eliminates the need for a try/catch anywhere in your controllers or lower layers of code.

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.