Handling 'text/plain' and Other Unsupported Content Types in ServiceNow Scripted REST APIs

Update, Jan 15, 2020: Added a section at the bottom to cover how to handle the application/x-www-form-urlencoded Content-type.

If you’ve ever tried to set up an AWS/SNS integration, you know that ServiceNow fails (rather spectacularly) to handle certain standard REST API request “Content-Types”; most notably, text/plain.

In this article, we’re going to discuss what happens when you you try to integrate with something that’s trying to send a plain-text payload, and how you can make it work. At the bottom of the article, we’ll have a simple Script Include that you can import into your instance to get around this issue, and make your plain-text integrations work.

The Problem

To illustrate the issue, I’ll wake up my PDI, and set up a Scripted REST API (SRAPI) that’s meant to accept a plain-text payload. I’ll include the steps to set this up, in case you want to follow along in your own PDI.

First, in the Application Navigator, I’m going to open up System Web Services > Scripted Web Services > Scripted REST APIs. On the corresponding list view, I’ll click New, and create my new REST API. I’m calling mine “Plain text REST”, and setting the API ID to “plain_text_rest”.

After saving this SRAPI record, I need to create a new REST Resource, so in the Resources related list, I’ll click New. I’ll name the new resource text-test, and set the HTTP method field to POST.
In the Content Negotiation form tab, I need to check the Override supported request formats checkbox, and set the Supported request formats field to text/plain (otherwise, it’ll reject any plain-text requests without actually running the code in my SRAPI).

Finally, I’ll put a really simple script in the Script field, that just logs the contents of the request itself:

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    gs.log(
        request.body.data
    );
    
})(request, response);

After saving the Resource record, it should look something like this:

And that’s about all there ought to be to it - but if you’ve tried this yourself, you know that we’re not done yet.
To demonstrate the problem, I’ll fire up Postman, create a new POST request, set my Basic Auth credentials to a test user account that I created in my PDI previously, add a Content-Type header and set it to text/plain, add some plain ol’ text to the body, and hit Send.

{
    "error": {
        "message": "com.glide.rest.domain.ServiceException: Service error: attempt to access request body is not allowed when request method is GET or DELETE",
        "detail": ""
    },
    "status": "failure"
}

Hang on a minute, what’s this? The response body contains an error, but the error doesn’t make any sense!

attempt to access request body is not allowed when request method is GET or DELETE

But my request method isn’t GET or DELETE — it’s POST!
Maybe if I have a look at the error logs in my instance, it’ll say something more sensical.

…Then again, maybe not. Somehow, that’s even less helpful; and it still gives me that weird error that talks to GET and DELETE requests:

com.glide.rest.domain.ServiceException: Service error: attempt to access request body is not allowed when request method is GET or DELETE

So what’s going on here?
Well - I have no idea. My guess is that it assumes that the payload is going to be in one of the formats that it understands (XML, JSON, etc.) and if it doesn’t match any of those patterns, then by some rube-goldberg-esque process, it somehow falls through into some condition that fires off this error and then gives up.

Unfortunately, this is a “hard error”, by which I mean that it actually halts execution. This means that the error halts execution of my script, and none of the code after the line that triggers the error, is actually run. This is a big problem, since the line that triggers the error is the line that simply accesses the body (request.body.data).


The Solution

After a lot of fiddling, I figured out that it isn’t accessing request.body that’s triggering the error - it’s specifically request.body.data. If I remove the line of code that’s attempting to access the .data property, the error goes away (but of course, then I can’t do anything with the request). The same problem occurs if I try to access request.body.dataString as well. I assume that’s because the error is happening within the Java “getter” method being run on Mozilla Rhino (the Java-based implementation of JavaScript that ServiceNow uses to run JS on the server). This means that maybe I can pull the data out of the request body myself, without ever accessing that property directly.

The API documentation for the RESTAPIRequestBody object mentions a property called dataStream, which it says you can pass to “a separate API” - without ever mentioning what that API is, or even telling us what type of object this property returns, or telling us how to actually use it or demonstrating its usage in the “example”…

Thanks, ServiceNow. ಠ_ಠ

After slamming my forehead against the problem for a while, I finally identified that the type of object that request.body.dataStream contains, is a GlideScriptableInputStream (great name, guys 👌). That class of object is, of course, undocumented (as far as I could find), but I was able to find one relevant method which accepted that type of object as an argument - GlideTextReader.

Armed with this knowledge, I put together the following Script Include which has one method that’ll get the contents of any plain-text REST API request body I’ve thrown at it:

var RESTPlainTextUtil = Class.create();
RESTPlainTextUtil.prototype = {
    initialize: function() {
    },
    
    /**
     * @description Get the body of a text/plain REST request as a string.
     * @param  request The RESTAPIRequest object (the whole request object, not just the body).
     * @param  [trim=false] Whether to trim the resulting body string before returning it.
     * @returns  The request body as a string. If the trim argument is specified and set to a truthy value, the request body will be trimmed before being returned.
     */
    getTextBody: function(request, trim) {
        var reqBodyLine;
        var reqBodyString = '';
        var streamBody = request.body.dataStream;
        var gtrRequestBodyReader = new GlideTextReader(streamBody);
        
        //get first line
        reqBodyLine = gtrRequestBodyReader.readLine();
        while(typeof reqBodyLine == 'string') {
            reqBodyString += reqBodyLine + '\n';
            
            //get next line
            reqBodyLine = gtrRequestBodyReader.readLine();
        }
        
        return trim ? reqBodyString.trim() : reqBodyString;
    },
    
    type: 'RESTPlainTextUtil'
};

After saving that Script Include and updating my REST Resource script to the following, everything works perfectly!

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    gs.log(
        new RESTPlainTextUtil().getTextBody(request)
    );
    
})(request, response);

And the plain-text contents of the request are logged as expected:


Special case: application/x-www-form-urlencoded

There is another Content-type which triggers the error mentioned above: application/x-www-form-urlencoded.

com.glide.rest.domain.ServiceException: Service error: attempt to access request body is not allowed when request method is GET or DELETE

Unfortunately, if we use the new RESTPlainTextUtil().getTextBody(request) method to get the contents of requests of this type, we will always get a blank string; even when the request method is POST, and the body is not blank!
In this case, ServiceNow treats the body of the message as though the data were URL/query parameters. Even when that’s not the case. That’s a little bit annoying, but now that we know that, we know how to access the body of REST messages using the application/x-www-form-urlencoded Content-type: Simply use request.queryParams to get an object with key/value pairs corresponding to the data that would otherwise be in the body.

Unfortunately, there is no way (that I’ve yet been able to find) to access the exact, original text of the body of the message, which means that signature-based message verification would not be possible. This is a rare and specific use-case, but if you’re trying to integrate between ServiceNow and something like Chargify’s webhooks for example, you won’t be able to do signature-based verification of those messages without the original message body in its original serialized characters.

If you find this as annoying as I do, open a HI ticket and let them know, or post your suggestion to fix it in the community.


Download

To install the above Script Include in your instance, simply follow the steps below:

  1. Download this XML file

  2. Navigate to your instance’s sys_script_include table

  3. Right-click on any of the table column headers on the Script Include table

  4. click “Import XML” (Not “Import”, but “Import XML”)

  5. Click Choose File, and select the XML file you just downloaded

  6. Click Upload.

Thanks for reading! If you enjoy our articles, don’t forget to subscribe! You can find some of our YouTube videos here, and you can find my books here.
Feel free to connect with me on Twitter and LinkedIn, and if your company needs some ServiceNow help, you can book a free, no-pressure consultation to discuss rates and how we can help you, here.