The dreaded “Submission failed, please try again” error and how you caused it 😊
Widespread form submission failures are a MOPS nightmare — especially if you only find out when your boss sends you a screenshot. Oops.
With Marketo forms, there’s one error that has the exact same cause, every single time I’ve looked into it for a community poster or client. This one:
The cause: you have at least 2 form embeds in the page, with loadForm
loading from different domains.
The 2 domains are typically:
- an LP domain (like
pages.example.com
) — which is now the default in the embed code for all subscriptions - the instance URL (like
app-xx-01.marketo.com
) — the old default
The embeds might both be in the original document HTML, like so:
<!-- embed #1 (Primary LP domain) -->
<script src="//pages.example.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_1449"></form>
<script>MktoForms2.loadForm("//pages.example.com", "410-XOR-673", 1449);</script>
<!-- /embed #1 -->
<div>Some other stuff</div>
<!-- embed #2 (Marketo instance domain) -->
<script src="//app-sj01.marketo.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_787"></form>
<script>MktoForms2.loadForm("//app-sj01.marketo.com", "410-XOR-673", 787);</script>
<!-- /embed #2 -->
Or one might be loaded via a tag manager or other external script, while the other is in the page... or both loaded via GTM... or (I’ve seen this too) there may be 3 or 4 different embeds!
The solution: only use the LP domain, everywhere.
This may require a little hunting — people sometimes swear they have only one form, until I find the other one that’s loaded by their site’s common-scripts-xyz123.js or what-have-you — but do it and you’re done.
Why you might have this setup
The reason both embed types are still found in the wild is that SSL didn’t always come with Marketo subscriptions. (Now, it comes with all packages, but the horse is out of the barn.)
So if you hadn’t added SSL for pages.example.com
to avoid the extra expense, but you were embedding on an https://
page, you had to load from https://app-xx-01.marketo.com
. Remember, embedded scripts must be secure if the main document is secure.
Old embeds using https://app-xx-01.marketo.com
of course still work fine. (It would be terrible if they spontaneously stopped working!) And of course new embeds using your secure LP domain work fine, too.
But they don’t work fine if you mix them on the same page.
I could stop there, but it’s nice to know the technical reasons why mixing domains breaks stuff. So please read on if you have time.
Going in deeper
Your browser’s F12 Console gives you all the clues. If you’re not familiar with how the forms library is supposed to work, though, this might as well be the Zodiac code:
Note it’s a CORS (Cross-Origin Resource Sharing) error: the browser is rejecting an attempt by https://somehost.somedomain.example
to communicate with https://anotherhost.anotherdomain.example
.
But a Forms 2.0 form post is never cross-origin. So that tells you right away something has malfunctioned, and that something relates to origins & domains.
record_scratch.mp3
“Hold up!” you say.
“How could the embed not use a cross-origin post? If my website is https://www.example.com and my Marketo instance is https://app-sj01.marketo.com, that’s cross-origin! And same with https://pages.example.com, because the origin includes the hostname.”
True: if the form post originated from https://www.example.com
, then it would be cross-origin. But it doesn’t do that. An embedded Marketo form posts from the domain value you pass to loadForm()
to that exact same domain. It’s same-origin.
How does this work?
- The Forms 2.0 library loads a hidden IFRAME from the domain in
loadForm()
. That IFRAME hosts a tiny HTML page that loads another copy of the library. - When the form is ready to submit, the library (the one loaded by the main document) uses JS postMessage to pass form field values over to the IFRAME. It also includes the final Marketo endpoint URL where the IFRAME is expected to post the data.
- The IFRAME receives the data, then its own copy of the library does the submission and passes a status message back to the main doc.
This works beautifully if your forms were all loaded from https://app-*.marketo.com
:
And works just as well if your forms were all loaded from your LP domain:
The problem case is when you load forms from different domains.
See, the forms library is smart. Even if you include multiple <script>
tags pointing to forms2.min.js, on the same or different domains, the library will only effectively load once. At the start, the lib checks if a global MktoForms2
object already exists. If it does, it exits early (neither overwriting the existing MktoForms2
nor creating another one).
The library also is smart about how many times it loads additional assets. It’ll only load CSS stylesheets and JS polyfills once, for example. And it’ll only create one IFRAME, as opposed to one per form.
So this is smart programming, but it has a side effect. Only one same-domain IFRAME will ever be created. And that IFRAME will originate from the first domain passed to loadForm
. Subsequent forms will merrily pass messages to that same IFRAME, expecting it to be able to post to the back-end Marketo instance.
But that won’t work if the first form came from Origin A (meaning the IFRAME is on Origin A) and then another form says, “Hey IFRAME, pass this data to Origin B, okay?” That’s a cross-origin attempt and will fail:
(Of course, it’s not that one can’t in general prepare CORS headers and expectations for this kind of setup, but that’s not the way Marketo’s form infrastructure works. It expects same-origin posts.)
So there you have it. The high-level cause, the easy fix, and hopefully an interesting look under the hood.
Supplementary ’shots
Now that you know a bit more about it, you can also track the problem in your F12 Network tab.
There’s no immediate error upon loading 2 forms from different domains, but note the /XDFrame is only loaded from the first domain, the LP domain in this case:
But when when the form from https://app-*.marketo.com
tries to post data using the IFRAME from the LP domain, there it is: