IO proxy: POST for long payloads, network error resilience

- Switch to POST with JSON body when query string exceeds 1500 chars
  (highlight calls with large component sources hit URL length limits)
- Include CSRF token header on POST requests
- Add .catch() on fetch to gracefully handle network errors (return NIL)
- Upgrade async eval miss logs from logInfo to logWarn for visibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 09:23:20 +00:00
parent cb0990feb3
commit aa67b036c7
2 changed files with 59 additions and 7 deletions

View File

@@ -14,7 +14,7 @@
// ========================================================================= // =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-07T09:03:03Z"; var SX_VERSION = "2026-03-07T09:23:03Z";
function isNil(x) { return x === NIL || x === null || x === undefined; } function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); } function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -3336,7 +3336,7 @@ callExpr.push(dictGet(kwargs, k)); } }
result.then(function(rendered) { result.then(function(rendered) {
callback(rendered); callback(rendered);
}).catch(function(e) { }).catch(function(e) {
logInfo("sx:async eval miss: " + (e && e.message ? e.message : e)); logWarn("sx:async eval miss: " + (e && e.message ? e.message : e));
callback(null); callback(null);
}); });
} else { } else {
@@ -4329,6 +4329,7 @@ callExpr.push(dictGet(kwargs, k)); } }
} }
// Register a server-proxied IO primitive: fetches from /sx/io/<name> // Register a server-proxied IO primitive: fetches from /sx/io/<name>
// Uses GET for short args, POST for long payloads (URL length safety).
function registerProxiedIo(name) { function registerProxiedIo(name) {
registerIoPrimitive(name, function(args, kwargs) { registerIoPrimitive(name, function(args, kwargs) {
var url = "/sx/io/" + encodeURIComponent(name); var url = "/sx/io/" + encodeURIComponent(name);
@@ -4341,8 +4342,29 @@ callExpr.push(dictGet(kwargs, k)); } }
qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k]))); qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k])));
} }
} }
if (qs.length) url += "?" + qs.join("&"); var queryStr = qs.join("&");
return fetch(url, { headers: { "SX-Request": "true" } }) var fetchOpts;
if (queryStr.length > 1500) {
// POST with JSON body for long payloads
var sArgs = [];
for (var j = 0; j < args.length; j++) sArgs.push(String(args[j]));
var sKwargs = {};
for (var kk in kwargs) {
if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]);
}
var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" };
var csrf = csrfToken();
if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf;
fetchOpts = {
method: "POST",
headers: postHeaders,
body: JSON.stringify({ args: sArgs, kwargs: sKwargs })
};
} else {
if (queryStr) url += "?" + queryStr;
fetchOpts = { headers: { "SX-Request": "true" } };
}
return fetch(url, fetchOpts)
.then(function(resp) { .then(function(resp) {
if (!resp.ok) { if (!resp.ok) {
logWarn("sx:io " + name + " failed " + resp.status); logWarn("sx:io " + name + " failed " + resp.status);
@@ -4359,6 +4381,10 @@ callExpr.push(dictGet(kwargs, k)); } }
logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e)); logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e));
return NIL; return NIL;
} }
})
.catch(function(e) {
logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e));
return NIL;
}); });
}); });
} }

View File

@@ -1699,6 +1699,7 @@ ASYNC_IO_JS = '''
} }
// Register a server-proxied IO primitive: fetches from /sx/io/<name> // Register a server-proxied IO primitive: fetches from /sx/io/<name>
// Uses GET for short args, POST for long payloads (URL length safety).
function registerProxiedIo(name) { function registerProxiedIo(name) {
registerIoPrimitive(name, function(args, kwargs) { registerIoPrimitive(name, function(args, kwargs) {
var url = "/sx/io/" + encodeURIComponent(name); var url = "/sx/io/" + encodeURIComponent(name);
@@ -1711,8 +1712,29 @@ ASYNC_IO_JS = '''
qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k]))); qs.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(kwargs[k])));
} }
} }
if (qs.length) url += "?" + qs.join("&"); var queryStr = qs.join("&");
return fetch(url, { headers: { "SX-Request": "true" } }) var fetchOpts;
if (queryStr.length > 1500) {
// POST with JSON body for long payloads
var sArgs = [];
for (var j = 0; j < args.length; j++) sArgs.push(String(args[j]));
var sKwargs = {};
for (var kk in kwargs) {
if (kwargs.hasOwnProperty(kk)) sKwargs[kk] = String(kwargs[kk]);
}
var postHeaders = { "SX-Request": "true", "Content-Type": "application/json" };
var csrf = csrfToken();
if (csrf && csrf !== NIL) postHeaders["X-CSRFToken"] = csrf;
fetchOpts = {
method: "POST",
headers: postHeaders,
body: JSON.stringify({ args: sArgs, kwargs: sKwargs })
};
} else {
if (queryStr) url += "?" + queryStr;
fetchOpts = { headers: { "SX-Request": "true" } };
}
return fetch(url, fetchOpts)
.then(function(resp) { .then(function(resp) {
if (!resp.ok) { if (!resp.ok) {
logWarn("sx:io " + name + " failed " + resp.status); logWarn("sx:io " + name + " failed " + resp.status);
@@ -1729,6 +1751,10 @@ ASYNC_IO_JS = '''
logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e)); logWarn("sx:io " + name + " parse error: " + (e && e.message ? e.message : e));
return NIL; return NIL;
} }
})
.catch(function(e) {
logWarn("sx:io " + name + " network error: " + (e && e.message ? e.message : e));
return NIL;
}); });
}); });
} }
@@ -3406,7 +3432,7 @@ PLATFORM_ORCHESTRATION_JS = """
result.then(function(rendered) { result.then(function(rendered) {
callback(rendered); callback(rendered);
}).catch(function(e) { }).catch(function(e) {
logInfo("sx:async eval miss: " + (e && e.message ? e.message : e)); logWarn("sx:async eval miss: " + (e && e.message ? e.message : e));
callback(null); callback(null);
}); });
} else { } else {