Never mind what I wrote above. That only solves it part of the time. It seems Google Sheets API simply doesn't support CORS consistently. I wrote a server-side proxy which only passes requests through to google.com, that being the only way forward.
I thought I'd share my js code, because I wrote a nice little thing that can be used just like $.ajax. Also glad to share the server-side code too, but you could use something like this to interface with your own server-side proxy. It's not pretty but it is working. Oh, um, LGPL. Here's the js:
// #### #### ##### #### ## ## ## ## ## ##
// ## ## ## ## ## ## ## #### ## ## #### ## ##
// ## ## ## ##### #### ## ## ## # ## ## ## ####
// ## ## ## ## ## ## ## ###### ####### ###### ##
// #### #### ## ## #### ## ## ## ## ## ## ##
function CorsAway(serverSideUrl) {
// Server-side proxy handling of cross-domain AJAX requests.
this.serverSideUrl = serverSideUrl;
// This hash contains information as to whether each $.ajax parameter should be submitted to $.ajax directly, or passed to the CorsAway server.
// true means that the parameter should be passed to the CorsAway server
this.parameterIsForRemoteServer = {
// accepts: // not supported
// async: // not supported
beforeSend: false, // submit to $.ajax
// cache: // not supported, see $.ajax documentation for how to implement
complete: false, // submit to $.ajax
contents: false, // submit to $.ajax
contentType: true, // submit to remote server
context: false, // submit to $.ajax
converters: false, // submit to $.ajax
// crossDomain: // not supported
data: true, // submit to remote server
dataFilter: false, // submit to $.ajax
dataType: false, // submit to $.ajax
error: false, // submit to $.ajax
// global: // not supported
headers: true, // submit to remote server
// ifModified: // not supported
// isLocal: // not supported
// jsonp: // not supported
// jsonpCallback: // not supported
method: true, // submit to remote server
/// mimeType: true, // submit to remote server
/// password: true, // submit to remote server
// processData: // REQUIRES SPECIAL HANDLING: SEE COMMENTS IN CODE BELOW
// scriptCharset: // not supported
statusCode: false, // submit to $.ajax
success: false, // submit to $.ajax
timeout: false, // submit to $.ajax
// traditional: // not supported
// type: // not supported
/// url: true, // submit to remote server
/// username: true // submit to remote server
// xhr: // not supported
// xhrFields: // not supported
}
// Use it just like $.ajax
this.ajax = function (url, jqAjaxInfo) {
//Redirect all requests to a call to the server
// Sort jqAjaxInfo into parameters for $.ajax and for the remote server
var localAjaxParams = {};
var remoteHttpRequestParams = {};
for(var k in jqAjaxInfo) {
if(this.parameterIsForRemoteServer[k]) {
// Submit it to the remote server
remoteHttpRequestParams[k] = jqAjaxInfo[k];
} else { // some parameters are not supported; their behavior is undefined and doesn't matter
// Submit it to $.ajax
localAjaxParams[k] = jqAjaxInfo[k];
}
}
// Prepare specially encapsulated data parameter for local $.ajax to submit to server-side CorsAway
localAjaxParams.data = {
dataToSubmit: localAjaxParams.data,
remoteHttpRequestParams: remoteHttpRequestParams,
remoteUrl: url
};
localAjaxParams.method = 'PUT'; // Always make request to CorsAway by PUT
// Make call to $.ajax and pass info to server-side CorsAway service
$.ajax(this.serverSideUrl, localAjaxParams);
}
}
// Instantiate global object with URL of server-side CorsAway service
window.corsAway = new CorsAway('/local/url/of/corsaway.php');
So now instead of $.ajax
I use window.corsAway.ajax
with exactly the same results. The server-side proxy is designed to return the data from the remote server, or to pass through any HTML error it receives back to ajax.
Something seems very wrong about writing a utility called CorsAway, but hey. The server-side proxy checks the domain and only passes things through to approved domains (right now only Google) so what could go wrong, right? Somebody tell me if something could go wrong. :-)