Apex Callout – GET request received as POST

Matt/ March 1, 2022/ Development, Integration, Salesforce

Have you ever tried to address an issue with your code and gone down a rabbit hole? Of course you have. I recently encountered a pretty great one.

It all started with this message from the console:

Error on line 15, column 1: System.JSONException: Malformed JSON: Expected '{' at the beginning of object

Let’s set the stage. The endpoint for an existing integration (Remote Process Invocation – Request and Reply) was performing poorly so the client built a new endpoint into the API to their back-office system. It was a significant optimization in the API and everything had changed: The endpoint, the HTTP verb, and the response. Simple enough.

I wrote the callout:

// Perform the HTTP request
HttpResponse resp = RestAPIUtility.GetRestApi(orderEndpoint, mapHeaders, null, false);
String responseBody = resp.getBody();

and tested it by running an Anonymous Apex script, where I encountered the above JSONException.

Spoiler Alert: Here’s where I made a key mistake – I didn’t recognize the error was with the response payload, I understood it to be a server error with the null payload I had sent in my request. Oops.

Normally a GET request should not send a body, but here it was asking me for one. “Must be some weird validation check in the endpoint,” I thought. So I changed my code – rather than sending a null body for this GET request, I sent an empty JSON string. I mean, that’s what the server asked for, right?…

// Perform the HTTP request
HttpResponse resp = RestAPIUtility.GetRestApi(orderEndpoint, mapHeaders, '{}', false);
String responseBody = resp.getBody();

… and I ran the script to launch the batch class from Anonymous Apex:

Error on line 901, column 1: ClientBackOffice_CalloutException: {"StatusCode":405,"ErrorMessage":"{\"Error\":{\"Code\":\"UnsupportedApiVersion\",\"Message\":\"The requested resource with API version '1.0' does not support HTTP method 'POST'.\",\"InnerError\":{\"Message\":\"No route providing a controller name with API version '1.0' was found to match HTTP method 'POST' and request URI 'http://clientendpoint.com/newEndpoint'.\"}}}","apiCall":"Get Client Updates"}

Ok, now I’m getting somewhere…. or so I thought. ?

This stood out to me as really weird… I was calling RestAPIUtility.GetRestApi(), but the error told me POST isn’t supported. I checked the RestAPIUtility class in the codebase and in Github to ensure this method hadn’t changed. It hadn’t, and this method is definitely constructed as GET. I looked at the debug statement from immediately before the callout is made and I am satisfied my callout is correct:

10:49:29.370 (645333971)|USER_DEBUG|[73]|DEBUG|RestAPIUtility REQUEST: System.HttpRequest[Endpoint=http://clientendpoint.com/newEndpoint, Method=GET]

So I asked to the client – “Umm, I’m sending you a GET request but the error I’m getting back is that POST requests aren’t supported in this endpoint. Do you mind checking if some sort of redirect logic is being fired?”

Client: (checks) There’s no redirect logic on our end.
Matt: (back to debugging)
I notice this is the only endpoint you’ve given me that is HTTP. Could it be redirecting on your end to HTTPS?
Client:
Shouldn’t matter, you should update your endpoint value to HTTPS.
Matt: (changed endpoint to HTTPS and tries again)
I’m still getting the same message.

So we tested the callout from Postman, and it worked fine.

WTF is happening? ?

I hopped on a Zoom call with the client. I prove to him that I’m sending a GET request. He proves to me that he is receiving a POST request.

I still had a suspicion the callout was being redirected on the client side; after-all, I can clearly see I am sending GET.

Fearful that this issue could be the type of thing someone could spend a dozen hours trying to figure out, I asked some fellow developers in the practice. “Have you ever encountered this?

I ended up in a huddle with Dan Peter. We agreed this was probably a “something small and stupid” error, so one after another we adjusted a bunch of small things. We dug through the Postman console to recreate the exact headers that had been sent. Nothing worked. We browsed a half-dozen stack overflow articles. Two of them seemed potentially relevant so I set them aside to look at closer.

Right before he hung up, Dan had a great idea: “Why not build a fake endpoint and callout that endpoint instead? That will help you confirm if the issue is truly with the client’s endpoint.”

Dan introduced me to RequestBin and within minutes I had set up a dummy endpoint and Remote Site Setting. I ran the script and saw the request come in real time – as a POST request. ?

Clearly the problem is not with the client’s endpoint. I came back to this StackExchange post and reread it.

If you remove the body from your code, you’ll find SF sends it as a GET. Put it back in, and it gets converted to a POST.

“The request has no body”, I thought to myself. “I’m just sending an empty JSON string ‘{}’. The initial JSONException asked for this.” Having tried everything else I decided to remove the ‘{}’ and callout to RequestBin with a null body:

RequestBin’s Inspector showing incoming requests

Much to my surprise the request came in with the desired GET action! This solved the redirect mystery – the empty JSON string was the culprit. I’d come full circle and was back to where I had started. I changed the callout back to the client’s endpoint and removed the body from the request, then ran the script again:

Error on line 15, column 1: System.JSONException: Malformed JSON: Expected '{' at the beginning of object

Back to the initial error message. I inspected it closer and it finally hit me: This is an error with the response payload, not the request payload. ?‍♂️

I hadn’t noticed that the structure of the response payload had changed from a JSON object with an array node to an array of JSON objects, and I had yet to accommodate for that:

if (resp.getStatusCode()==200) {
    results = (List<Client_CallOutResources.OrderChangesResponseWrapper>) JSON.deserialize(resp.getBody(), List<Client_CallOutResources.OrderChangesResponseWrapper>.class);
    System.debug('*** log OrderChangesResponseWrapper responses = ' + results.size());
}

And there it was – a successful callout!

After two hours spinning my wheels I had it solved. This happens to everyone from time to time, and luckily we usually end up learning enough to avoid making the mistake again (or at least recognize it much earlier the next time). So here’s the lesson:

A payload within a GET request message has no defined semantics;
sending a payload body on a GET request might cause some existing
implementations to reject the request.

From the HTTP 1.1 2014 Spec

The funny thing about this was that I’ve built enough callouts that I am surprised I haven’t encountered this before. Either way, there were a few great takeaways from this that I wanted to share:

  • Always question your assumptions.
  • Confirm the line number that throws the exception, then double check it again. Don’t take this for granted.
  • RequestBin is a very useful free tool to quickly check what your callout looks like on the other side.
  • Server semantics for GET request means the body has absolutely no meaning to the request;
  • Servers tend to consider the body of a GET request as an entirely new request and can throw weird exceptions on the second request.

This issue had the potential to become a real time-waster but likely it was resolved relatively quickly.

I know that most developers can relate to this sort of scenario. Have you recently gone down a rabbit hole? Do you have an epic story you will never forget? I’d love for you to share some details in the comments.

Share this Post

About Matt

Matt is a seasoned Salesforce Developer / Architect, with implementations of Sales Cloud, Service Cloud, CPQ, Experience Cloud, and numerous innovative applications built upon the Force.com platform. He started coding in grade 8 and has won awards ranging from international scholarships to internal corporate leadership awards. He is 37x Certified on the platform, including Platform Developer II, B2B Solution Architect and B2C Solution Architect.

2 Comments

  1. We follow our intuition – and are rewarded for doing so – frequently enough that when our initial assumption ends up leading us astray, it can lead us down a rabbit hole! Kudos to you for figuring out the issue in the end; maybe some documentation in your RestAPIUtility class is in order (or validation; don’t let people add a request body to a GET method!) to prevent future consumers from going down the same path? Thanks for the article – it was a great read!

    1. That’s a very insightful comment James. When your intuition is right 90% of the time, every once in a while you end up chasing an issue like this. Thanks for sharing that!

Comments are closed.