Catching a few more (theoretical/conjectural) types of Marketo form load errors

Some months back, I told you how to accurately wait for the MktoForms2 global object — a dependency for all custom Forms 2.0 JS behaviors.

A recent Marketo Nation thread piqued my interest in the opposite direction. The question, consolidated for readability:

Question about embedded forms.

I can detect when the MktoForms2 object is present and respond to that. My concern is the possible case where MktoForms2 is available but the Form Editor config [Sandy’s note: this is called the “form descriptor”] fails to load for any reason.

Is there any indication that a form was not successfully loaded, even though the Forms 2.0 library was?

Now, the moment you say “for any reason” you have to cover a wide range of cases. Not to say these cases have ever happened, let alone with any frequency worth worrying about, but they could potentially happen with any forms library. Even excluding problems wholly on the end user’s machine, you still have to cover:

  1. the server or network connects, but is nonresponsive: connection hangs
  2. the server or network is completely down: connection drops
  3. the server or proxy is up, but broken: immediately returns HTTP 5xx
  4. the server or proxy is up, but the URL isn’t found or is blocked: immediately returns HTTP 4xx
  5. the server is up, returns HTTP 2xx/3xx, but the response data is incomplete: good JavaScript, but contains unexpected/incomplete data
  6. the server is up, returns HTTP 2xx/3xx, but the response data is broken: in the case of the form descriptor, this would mean JavaScript with a syntax error

Cases ➋ ➌ and ➍ can be detected by an elegant event listener.

Case ➎ has a built-in error handler in the Forms 2.0 library, though almost no one knows about it!

Case ➏ can be detected only in Firefox and IE, though that’ll still give you a cue that something might be globally wrong. In Chrome and Safari, you have to reuse the solution for ➊.

Case ➊ is the really annoying one that forces us to fall back to a timeout (not coincidentally).

So let’s start with the fun ones.

Solving ➋ ➌ and ➍

First things first: this question only pertains to embedded forms, because with named forms on a Marketo LP, the form descriptor is part of the page. There’s no way for it to fail to load, since it doesn’t “load” at all.

With an embedded form, in contrast, the form descriptor is fetched via JSONP. (That is, a GET of a remote JavaScript file — not a JSON file.)

GET-ing a script might explicitly fail at the HTTP level (➌ ➍) or be disconnected immediately at the socket level (➋). These can all be detected together using a capturing error event listener on document. Just check if the source script matches the known path of the JSONP endpoint:

document.addEventListener("error",function(e){
    if( e.target.tagName == "SCRIPT" ) {
        let srcLoc = document.createElement("a");
        srcLoc.href = e.target.src;

        if( /^(\/)?index\.php\/form\/getForm/.test(srcLoc.pathname) ) {
            console.log("Marketo getForm failed at the network or HTTP level");
            // take further action
        }
    }
}, true);

Solving ➎

This one’s kind of cool because you’d never even know it’s built into the Forms 2.0 library unless you forced it to fail.

The optional 4th argument to MktoForms2.loadForm is the onReady callback for that form:

MktoForms2.loadForm("//landingpages.example.com", "MUNCH-KIN-ID", 1234, function(mktoForm){ 
        // form is (probably) ready
});

Normally you wouldn’t use this feature unless you had something special to do on ready (and it’s not as efficient as using a shared whenReady across all forms), but the onReady also fires if the form ID cannot be found, with its argument mktoForm set to null. Very handy!

So you can check for this case like so:

MktoForms2.loadForm("//landingpages.example.com", "MUNCH-KIN-ID", 1234, function(mktoForm){ 
     if( !mktoForm ) {
       console.log("Marketo getForm failed due to a bad form ID or other internal error");
       // take further action
     } else {
       // form is definitely ready   
     }
 });

Solving ➏ where you can

Case ➏ is again, a contrived type of fatal error and not something I’ve ever seen in reality unless a Marketo user, not Marketo itself, made a mistake. Nevertheless, if you’re gonna say “for any reason,” you have to consider it.

This is the case where the JSONP response is bad JavaScript. For an easily understood example, instead of the usual:

jQuery1124026595395461795657_1633206864967({
    "Id":787,
    "Vid":787,
    "Status":"approved",
    "Name":"Lab #0787 - Basix",
    /* ... lots of other properties here... */
    "invalidInputMsg":"Invalid Input",
    "formSubmitFailedMsg":"Submission failed, please try again later."
});

it’s like this, with missing closing } and ):

jQuery1124026595395461795657_1633206864967({
    "Id":787,
    "Vid":787,
    "Status":"approved"

Now, all browsers will throw an ErrorEvent when that happens, which you can pick up by listening for error on the window. So you’d think you could just do this:

window.addEventListener("error",function(e){
    let srcLoc = document.createElement("a");
    srcLoc.href = e.filename;

    if( /^(\/)?index\.php\/form\/getForm/.test(srcLoc.pathname) ) {
        console.log("Marketo getForm response could not be parsed");
        // take further action
    }   
})

Alas, no. Here’s where browser differences come into play.

Firefox (and IE 11 fwiw) will throw the ErrorEvent and also populate the filename property, so you can check if the error came from Marketo’s JSONP URL.

Webkit (Safari, Chrome, modern Edge) will throw the ErrorEvent but will leave all its interesting properties blank, including the all-important filename under the somewhat unconvincing banner of “cross-origin security.”* So that match against pathname will never work. And you can’t simply remove the match condition, because then you’d mistake a JS error from any script for our specific, interesting error.

Nevertheless, if your goal is to detect some crazy-wacky-out-there kind of server condition (one Marketo’s never had before to my knowledge), it’s still worth it to have a fraction of your visitors be able to report the detailed error.

Solving ➊

The above solutions are satisfyingly simple. While covering case ➊ (the indefinitely hanging connection) is also simple, the solution is sort of discomfiting.

You might not remember this, but any asset that isn’t loaded via traditional Ajax (XMLHttpRequest) does not have a built-in timeout option.**

In other words, this <script> tag can block the rest of the page forever, if the server never drops the connection but also never finishes returning data:

<script src="https://some/server/that/wont/let/go.js"></script>

(Yes, it’s alarmingly easy for a misbehaving site to completely paralyze sites that use its assets!)

With such a request, there’s no event fired on “absence of evidence” — that is, the fact that a request is still in progress after a certain amount of time is not an event in itself. So you have no choice but to set a separate timer and wait for it to expire. When the form becomes ready, stop the timer:

let getFormTimeout = setTimeout(function(){
  console.log("Marketo getForm failed due to timeout")
  // take further action
}, 5000);

MktoForms2.loadForm("//app-sj01.marketo.com", "410-XOR-673", 787, function(mktoForm){
   if( mktoForm ){
       clearTimeout(getFormTimeout);
   }
});

This does work for case ➊, but it’s hackier than I would’ve liked.

What could “further action” be?

Up to you!

I recommend starting with a GTM event — hopefully, you’ll see it in the console quickly. Even better: send to a product analytics package like Heap or an error tracking service like TrackJS. These are purpose-built to send alert emails, Slack messages, and so on.

Some people imagine injecting or revealing a non-Marketo form, in real-time, if an error is detected with the Marketo form. You’re unlikely to get this right without a ton of testing. Redirecting to a fallback page seems more sensible (and of course also sending that back-end event).

Notes

* More on this in another post, perhaps. I think Webkit is not being sensible here (given how easily their “security” is circumvented) and if anyone cares about security it’s Firefox.

** XMLHttpRequest offers a built-in timeout option, but neither remote scripts/images/stylesheets nor the new Fetch API (!) have such an option. In practice, browsers do apply their own hardcoded timeout — 90 seconds, 2 minutes and 5 minutes are all seen in the wild — but they are under no obligation to do so. And 5 minutes might as well be forever from an end user’s standpoint!