The bootstrap's absolutize branch tested `url.indexOf('http') === 0`, so a
ws:// URL matched neither branch: it does not start with '/' (the relative
branch) and it does not start with 'http' (the absolute branch). It fell
through and was returned untouched, which meant the WebSocket shim below it
was bypassed for every real WebSocket.
baseModelApi.js builds exactly that shape:
new WebSocket(`${proto}${window.location.host}${WS_ENDPOINTS.fetchProgress}`)
so under a reverse-proxy prefix the socket dialled the host root instead of
the prefixed path. Behind SwarmUI (/ComfyBackendDirect/), the root is answered
by the front-end router:
ws://host:7801/ws/fetch-progress -> 302
ws://host:7801/ComfyBackendDirect/ws/fetch-progress -> 101
A 302 fails the upgrade immediately, so showWithProgress() rejected and
auto-organize reported an error after the move had already succeeded.
Accept ws:/wss: in the absolutize branch and compare hosts rather than
origins -- a ws: URL's origin serialises as ws://host, never http://host, so
the origin comparison would have rejected the very case being fixed.
Behaviour change is confined to same-origin ws/wss URLs; external and
http(s) cases are unchanged.
Co-authored-by: Kai <kai@tigersden.net>
204 lines
8.1 KiB
HTML
204 lines
8.1 KiB
HTML
{#
|
|
Base-path bootstrap. Must be the FIRST element in <head>: it detects the
|
|
reverse-proxy subpath (e.g. "/comfyui" when served via llama-swap, or
|
|
"/ComfyBackendDirect" via SwarmUI) from the current page URL and, only when
|
|
a prefix exists, patches fetch/XHR/WebSocket/innerHTML/DOM URL setters so
|
|
that root-absolute URLs ("/api/lm/...", "/loras_static/...") generated
|
|
anywhere in the frontend or in backend JSON payloads get the prefix
|
|
prepended. When no prefix is detected (normal or standalone deployment)
|
|
nothing is patched and behavior is byte-identical to before.
|
|
#}
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Manager page routes as the backend sees them (proxies strip the
|
|
// prefix, so the page path always ends with one of these suffixes).
|
|
// Longest first so "/loras/recipes" wins over "/loras".
|
|
var KNOWN_PAGES = ['/loras/recipes', '/loras', '/checkpoints', '/embeddings', '/other', '/statistics'];
|
|
|
|
var path = window.location.pathname.replace(/\/+$/, '') || '/';
|
|
var prefix = '';
|
|
for (var i = 0; i < KNOWN_PAGES.length; i++) {
|
|
var page = KNOWN_PAGES[i];
|
|
if (path === page || (path.length > page.length && path.endsWith(page))) {
|
|
prefix = path.slice(0, path.length - page.length);
|
|
break;
|
|
}
|
|
}
|
|
|
|
window.LM_BASE_PATH = prefix;
|
|
if (!prefix) {
|
|
return;
|
|
}
|
|
|
|
var prefixBody = prefix.slice(1);
|
|
|
|
function prefixPath(p) {
|
|
if (p.charAt(0) !== '/' || p.charAt(1) === '/') {
|
|
return p;
|
|
}
|
|
// Already prefixed (e.g. markup re-serialized from the DOM).
|
|
if (p === prefix || p.indexOf(prefix + '/') === 0) {
|
|
return p;
|
|
}
|
|
return prefix + p;
|
|
}
|
|
|
|
function prefixUrl(url) {
|
|
if (typeof url !== 'string') {
|
|
return url;
|
|
}
|
|
if (url.charAt(0) === '/') {
|
|
return prefixPath(url);
|
|
}
|
|
// Absolute same-origin URL (e.g. from a Request object).
|
|
// ws:/wss: belong here too: a WebSocket URL is absolute, and its
|
|
// origin is the http(s) origin with the scheme swapped. Leaving them
|
|
// out let every `new WebSocket("ws://host/ws/...")` slip through
|
|
// unprefixed, so the socket dialled the proxy root and was answered
|
|
// by the front-end router instead of the backend.
|
|
if (/^(https?|wss?):\/\//i.test(url)) {
|
|
try {
|
|
var u = new URL(url);
|
|
// A ws: URL's origin serialises as ws://host, never http://host,
|
|
// so compare hosts rather than origins to keep the same-origin
|
|
// test working across the scheme swap.
|
|
if (u.host === window.location.host) {
|
|
var prefixed = prefixPath(u.pathname);
|
|
if (prefixed !== u.pathname) {
|
|
u.pathname = prefixed;
|
|
return u.toString();
|
|
}
|
|
}
|
|
} catch (e) { /* not a parseable URL: leave untouched */ }
|
|
}
|
|
return url;
|
|
}
|
|
|
|
window.lmWithBasePath = prefixUrl;
|
|
|
|
// fetch()
|
|
if (window.fetch) {
|
|
var nativeFetch = window.fetch;
|
|
window.fetch = function (input, init) {
|
|
if (typeof input === 'string') {
|
|
input = prefixUrl(input);
|
|
} else if (typeof URL !== 'undefined' && input instanceof URL) {
|
|
// fetch(new URL('/api/...', location.origin)) bypasses the
|
|
// string check — coerce so same-origin URLs get prefixed.
|
|
input = prefixUrl(input.href);
|
|
} else if (typeof Request !== 'undefined' && input instanceof Request) {
|
|
var requestUrl = prefixUrl(input.url);
|
|
if (requestUrl !== input.url) {
|
|
input = new Request(requestUrl, input);
|
|
}
|
|
}
|
|
return nativeFetch.call(this, input, init);
|
|
};
|
|
}
|
|
|
|
// XMLHttpRequest
|
|
if (window.XMLHttpRequest) {
|
|
var nativeOpen = window.XMLHttpRequest.prototype.open;
|
|
window.XMLHttpRequest.prototype.open = function (method, url) {
|
|
arguments[1] = prefixUrl(typeof url === 'string' ? url : String(url));
|
|
return nativeOpen.apply(this, arguments);
|
|
};
|
|
}
|
|
|
|
// WebSocket
|
|
if (window.WebSocket) {
|
|
var NativeWebSocket = window.WebSocket;
|
|
var PatchedWebSocket = function (url, protocols) {
|
|
if (url && typeof url !== 'string' && url.href) {
|
|
url = url.href;
|
|
}
|
|
return protocols === undefined
|
|
? new NativeWebSocket(prefixUrl(url))
|
|
: new NativeWebSocket(prefixUrl(url), protocols);
|
|
};
|
|
PatchedWebSocket.prototype = NativeWebSocket.prototype;
|
|
PatchedWebSocket.CONNECTING = NativeWebSocket.CONNECTING;
|
|
PatchedWebSocket.OPEN = NativeWebSocket.OPEN;
|
|
PatchedWebSocket.CLOSING = NativeWebSocket.CLOSING;
|
|
PatchedWebSocket.CLOSED = NativeWebSocket.CLOSED;
|
|
window.WebSocket = PatchedWebSocket;
|
|
}
|
|
|
|
// Markup injected via innerHTML/outerHTML/insertAdjacentHTML carries
|
|
// root-absolute URLs from backend JSON (preview URLs, placeholders) and
|
|
// inline handlers (onerror="this.src='/...'"): rewrite the attributes.
|
|
var ATTR_URL_RE = /((?:src|href|poster)\s*=\s*["'])(\/)(?!\/)/g;
|
|
function rewriteMarkup(html) {
|
|
if (typeof html !== 'string' || html.indexOf('/') === -1) {
|
|
return html;
|
|
}
|
|
return html.replace(ATTR_URL_RE, function (match, head, slash, offset, whole) {
|
|
var rest = whole.slice(offset + match.length);
|
|
if (rest === prefixBody || rest.indexOf(prefixBody + '/') === 0) {
|
|
return match;
|
|
}
|
|
return head + prefix + '/';
|
|
});
|
|
}
|
|
|
|
function patchMarkupProp(proto, prop) {
|
|
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
if (!desc || !desc.set) {
|
|
return;
|
|
}
|
|
Object.defineProperty(proto, prop, {
|
|
configurable: true,
|
|
enumerable: desc.enumerable,
|
|
get: desc.get,
|
|
set: function (value) { desc.set.call(this, rewriteMarkup(value)); },
|
|
});
|
|
}
|
|
|
|
if (window.Element) {
|
|
patchMarkupProp(window.Element.prototype, 'innerHTML');
|
|
patchMarkupProp(window.Element.prototype, 'outerHTML');
|
|
var nativeInsertAdjacentHTML = window.Element.prototype.insertAdjacentHTML;
|
|
if (nativeInsertAdjacentHTML) {
|
|
window.Element.prototype.insertAdjacentHTML = function (position, html) {
|
|
return nativeInsertAdjacentHTML.call(this, position, rewriteMarkup(html));
|
|
};
|
|
}
|
|
|
|
// Direct DOM assignments: img.src = model.preview_url, anchor.href, ...
|
|
var nativeSetAttribute = window.Element.prototype.setAttribute;
|
|
window.Element.prototype.setAttribute = function (name, value) {
|
|
if (typeof value === 'string' && /^(src|href|poster)$/i.test(name)) {
|
|
value = prefixUrl(value);
|
|
}
|
|
return nativeSetAttribute.call(this, name, value);
|
|
};
|
|
}
|
|
|
|
function patchUrlProp(proto, prop) {
|
|
if (!proto) {
|
|
return;
|
|
}
|
|
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
if (!desc || !desc.set) {
|
|
return;
|
|
}
|
|
Object.defineProperty(proto, prop, {
|
|
configurable: true,
|
|
enumerable: desc.enumerable,
|
|
get: desc.get,
|
|
set: function (value) { desc.set.call(this, prefixUrl(value)); },
|
|
});
|
|
}
|
|
|
|
patchUrlProp(window.HTMLImageElement && window.HTMLImageElement.prototype, 'src');
|
|
patchUrlProp(window.HTMLMediaElement && window.HTMLMediaElement.prototype, 'src');
|
|
patchUrlProp(window.HTMLSourceElement && window.HTMLSourceElement.prototype, 'src');
|
|
patchUrlProp(window.HTMLVideoElement && window.HTMLVideoElement.prototype, 'poster');
|
|
patchUrlProp(window.HTMLAnchorElement && window.HTMLAnchorElement.prototype, 'href');
|
|
patchUrlProp(window.HTMLScriptElement && window.HTMLScriptElement.prototype, 'src');
|
|
patchUrlProp(window.HTMLLinkElement && window.HTMLLinkElement.prototype, 'href');
|
|
})();
|
|
</script>
|