15

Given two sub domains:

web.mysite.com and api.mysite.com

Currently making any request from web. to api. results in the preflight OPTIONS request being made. This wouldn't be so much of an issue if it didn't add an extra 600ms to requests in China.

I was told that setting document.domain = 'mysite.com'; in JS would resolve the issue but this hasn't helped at all.

Is it possible / how can I disable the OPTIONS request when sending to just a different sub domain.

sideshowbarker
  • 62,215
  • 21
  • 143
  • 153
Phill
  • 17,067
  • 6
  • 53
  • 99
  • Would doing a JSONP request solve that? I'm not in a capacity to try it out right now, but may be worth looking into? – javaauthority Jul 17 '15 at 04:06
  • jsonp would remove the preflight, but, that may cause issues with the api itself, if it doesn't support JSONP. kinda hard to do PUT and POST requests with jsonp. – Kevin B Jul 17 '15 at 04:08
  • We mainly want to solve it for GET only. I'm wondering if sending a cookie with the credentials instead of in the header would help then there wouldn't be custom headers. – Phill Jul 17 '15 at 04:10
  • I think cookie would still trigger it (but worth a shot anyway). if GET only, jsonp is likely the best option, if your api can support it. – Kevin B Jul 17 '15 at 04:11
  • @KevinB I got it working by creating an iframe and loading the same domain for both, then pulling the window from the iframe back to the parent and using it to create the XMLHttpRequest object. Then I could invoke it on the parent and all requests no longer include an OPTIONS request. I'll write this up in more detail when I get a chance. – Phill Jul 17 '15 at 08:07
  • @KevinB added my answer below – Phill Jul 22 '15 at 06:30
  • @javaauthority added my answer below – Phill Jul 22 '15 at 06:31

3 Answers3

14

Solved this using the iframe technique which seems to be what Facebook / Twitter do.

Steps below:

1) Set the document.domain to be the root domain. So given the url http://site.mysite.com/ I set the domain in JavaScript like document.domain = 'mysite.com';

2) Setup an iframe which pulls a HTML file from the API Domain.

<iframe id="receiver" src="http://api.mysite.com/receiver" style="position:absolute;left:-9999px"></iframe>

This is set to be positioned so it can't be seen.

3) Set the HTML of the receiver page to set the domain:

<!DOCTYPE html><body><script>document.domain='mysite.com'</script></body></html>

4) Added an onload event to the iframe to capture the window once its loaded.

onload="window.tempIframeCallback()"

5) Assign the child window to a variable.

window.tempIframeCallback = function() {
  window.childWindow = window.receiver.contentWindow;
}

6) Make the XMLHttpRequest() from the childWindow instead of the main window.

var xhr = new window.childWindow.XMLHttpRequest();

Now all requests will be sent without a preflight OPTIONS request.


7) When using jQuery, you can also set the source of xhr in the settings:

$.ajax({
  ...
  xhr: function() {
    return new window.childWindow.XMLHttpRequest();
  }
});
nrofis
  • 7,124
  • 9
  • 44
  • 90
Phill
  • 17,067
  • 6
  • 53
  • 99
  • 3
    Thank you so much for this idea ! For future readers, this also works with the `fetch` API, using `window.childWindow.fetch()` instead of `window.fetch()`. Crazy that the web standards cannot accommodate situations like that without hackish workarounds like this one ... – ouk Aug 11 '16 at 01:33
  • 1
    Anyone attempting this solution at present (Dec 2016) should be aware that this will not work if the API domain is not a child of the domain where the website itself is hosted. E.g. something like api.example.org won't work if your site is hosted on www.example.org, and the browser will refuse to set `document.domain`. – Adam Reis Dec 24 '16 at 21:24
  • @AdamReis It works fine. You need to set both the iframe and site to use example.org – Phill Dec 25 '16 at 18:26
  • 1
    When you *don't* use the apex domain, but work with subdomains (e.g. app.example.org) and then try to connect to another subdomain (e.g. api.example.org), then this will not be allowed. Chrome will refuse to let you change `document.domain`. Instead, you could use api.app.example.org, which would be allowed. In our app, we use subdomains to identify tenants, so we cannot simply use the apex domain. – Adam Reis Dec 26 '16 at 19:55
  • @adamreis I don't believe it has ever worked with setting across sub domains. It has to be the domain below. In any case I now use AWS api gateway and just route /api to the api and everything else to s3 and no longer deal with this issue at all. – Phill Dec 28 '16 at 00:46
  • @Phill yes I am aware of that options, but our proxy was causing a delay in requests of about 2 seconds, so at present even with the CORS preflight request the whole request took less than with the proxy. That's why I looked into alternative options. – Adam Reis Dec 29 '16 at 02:26
  • Ahh ok. Using AWS gateway my requests to NancyFx api are ~10ms and about the same for requests to s3 so I haven't noticed any perf issues myself. – Phill Dec 30 '16 at 06:51
  • @AdamReis for me it works flawlessly using the code in my answer further down this page. I can fetch api.domain.com from my.domain.com or whatever.domain.com on every current browser. I have no issue setting document.domain on Chrome 55 – ouk Jan 13 '17 at 00:09
  • I think this doesn't work anymore (or never) because of this error (Chrome): "Uncaught DOMException: Blocked a frame with origin 'www.example.org' from accessing a cross-origin frame." I think this is a CSP (https://stackoverflow.com/a/25098153/10088259) problem, isn't it? – Fred Hors Feb 02 '19 at 02:15
6

As a complement to @Phill's answer that deserves all the credits, here is the final html code, that also exposes the iframe's fetch function:

<!DOCTYPE html>
<html><body>
<script>
    document.domain = 'mysite.com';
    window.setupAPI = function() {
        var receiverWindow = window.receiver.contentWindow;
        // you may also want to replace window.fetch here
        window.APIfetch = receiverWindow.fetch;
        // same thing, you may as well replace window.XMLHttpRequest
        window.APIXMLHttpRequest = receiverWindow.XMLHttpRequest;
    }
</script>
<iframe id="receiver" 
        src="http://api.mysite.com/receiver" 
        style="position:absolute;left:-9999px"
        onload="window.setupAPI()"></iframe>
</body></html>

And of course the HTML "http://api.mysite.com/receiver" should retrieve:

<!DOCTYPE html>
<html><body><script>
    document.domain='mysite.com';
</script></body></html>

And then, within your JS code, you can now use APIfetch and APIXMLHttpRequest like you'd use fetch and XMLHttpRequest ... et voilà, no more preflight request whatever the method and content type used!

ouk
  • 365
  • 4
  • 7
  • I think this doesn't work anymore (or never) because of this error (Chrome): "Uncaught DOMException: Blocked a frame with origin 'www.example.org' from accessing a cross-origin frame." I think this is a CSP (https://stackoverflow.com/a/25098153/10088259) problem, isn't it? – Fred Hors Feb 02 '19 at 02:14
2

Here's an all javascript approach:

document.domain = 'mysite.net';
var apiIframe = document.createElement('iframe');
apiIframe.onload = function(){
    window.XMLHttpRequest = this.contentWindow.XMLHttpRequest;
};
apiIframe.setAttribute('src', API_URL + '/iframe');
apiIframe.style.display = 'none';
document.body.appendChild(apiIframe);

where API_URL + '/iframe' returns this:

<!DOCTYPE html><body><script>document.domain = 'mysite.net'</script></body></html>
ricka
  • 1,000
  • 1
  • 9
  • 13
  • I think this doesn't work anymore (or never) because of this error (Chrome): "Uncaught DOMException: Blocked a frame with origin 'www.example.org' from accessing a cross-origin frame." I think this is a CSP (https://stackoverflow.com/a/25098153/10088259) problem, isn't it? – Fred Hors Feb 02 '19 at 02:14