diff --git a/changelog.d/fixes/9580-standalone-ws-multipart-uploads.md b/changelog.d/fixes/9580-standalone-ws-multipart-uploads.md new file mode 100644 index 0000000000..b22b6f7fc2 --- /dev/null +++ b/changelog.d/fixes/9580-standalone-ws-multipart-uploads.md @@ -0,0 +1 @@ +- **fix(standalone):** multipart uploads (`POST /v1/audio/transcriptions`) no longer hang — the WebDAV wrapper hands non-WebDAV requests to Next synchronously instead of losing the start of a streaming body ([#9580](https://github.com/diegosouzapw/OmniRoute/pull/9580)) diff --git a/scripts/dev/standalone-server-ws.mjs b/scripts/dev/standalone-server-ws.mjs index 439a9c5171..ee5f0a1bec 100644 --- a/scripts/dev/standalone-server-ws.mjs +++ b/scripts/dev/standalone-server-ws.mjs @@ -3,7 +3,7 @@ import net from "node:net"; import { randomUUID } from "node:crypto"; import { createResponsesWsProxy } from "./responses-ws-proxy.mjs"; import { ensurePeerStampToken, wrapRequestListenerWithPeerStamp } from "./peer-stamp.mjs"; -import { maybeHandleWebdav } from "./webdav-handler.mjs"; +import { maybeHandleWebdav, WEBDAV_PREFIX } from "./webdav-handler.mjs"; import methodGuard from "./http-method-guard.cjs"; import headResponseGuard from "./head-response-guard.cjs"; import { resolveTlsOptions, createServerListener } from "./tls-options.mjs"; @@ -122,14 +122,20 @@ function wrapUpgradeListener(server, listener) { * Returns true if the request was handled; the wrapped listener is never called. */ function wrapRequestListenerWithWebdav(listener) { - return async function webdavAwareRequestHandler(req, res) { - try { - const handled = await maybeHandleWebdav(req, res); - if (handled) return; - } catch { - // Never block a request on WebDAV errors — fall through to Next + return function webdavAwareRequestHandler(req, res) { + if (!(req.url || "").startsWith(WEBDAV_PREFIX)) { + return listener.call(this, req, res); } - return listener.call(this, req, res); + const self = this; + (async () => { + try { + const handled = await maybeHandleWebdav(req, res); + if (handled) return; + } catch { + // Never block a request on WebDAV errors — fall through to Next + } + return listener.call(self, req, res); + })(); }; } diff --git a/tests/unit/standalone-server-ws-webdav-sync-listener.test.ts b/tests/unit/standalone-server-ws-webdav-sync-listener.test.ts new file mode 100644 index 0000000000..40b7e490d6 --- /dev/null +++ b/tests/unit/standalone-server-ws-webdav-sync-listener.test.ts @@ -0,0 +1,60 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// The WebDAV wrapper used to be `async` and awaited maybeHandleWebdav() for every +// request. Even when it returned false, that await deferred listener.call() by a +// microtask, so Next attached its 'data'/'end' handlers one tick late and lost the +// beginning of a streaming request body — multipart uploads (POST +// /v1/audio/transcriptions) then hung forever in request.formData(). +// +// standalone-server-ws.mjs has top-level side effects (it monkeypatches +// http.createServer and awaits ./server.js, which only exists in the assembled +// standalone output), so it cannot be imported in-process. Guard the fix by +// inspecting the source, mirroring standalone-server-ws-keepalive-timeout-7003.test.ts. +const here = path.dirname(fileURLToPath(import.meta.url)); +const source = fs.readFileSync( + path.resolve(here, "../../scripts/dev/standalone-server-ws.mjs"), + "utf8" +); + +const wrapper = source.slice( + source.indexOf("function wrapRequestListenerWithWebdav"), + source.indexOf("http.createServer = function createServerWithResponsesWs") +); + +test("standalone-server-ws.mjs imports WEBDAV_PREFIX to gate the async branch", () => { + assert.match( + source, + /import\s*\{[^}]*WEBDAV_PREFIX[^}]*\}\s*from\s*["']\.\/webdav-handler\.mjs["']/, + "expected WEBDAV_PREFIX to come from the shipped sibling ./webdav-handler.mjs" + ); +}); + +test("the WebDAV request wrapper is not async", () => { + assert.ok(wrapper.length > 0, "expected to find wrapRequestListenerWithWebdav"); + assert.doesNotMatch( + wrapper, + /return\s+async\s+function\s+webdavAwareRequestHandler/, + "an async handler defers listener.call() by a microtask and truncates streaming bodies" + ); +}); + +test("non-WebDAV requests reach the wrapped listener before any await", () => { + const prefixGuard = wrapper.indexOf("WEBDAV_PREFIX"); + const firstListenerCall = wrapper.indexOf("listener.call"); + const firstAwait = wrapper.indexOf("await "); + + assert.ok(prefixGuard >= 0, "expected the handler to test req.url against WEBDAV_PREFIX"); + assert.ok(firstListenerCall >= 0, "expected the handler to call the wrapped listener"); + assert.ok( + prefixGuard < firstListenerCall, + "expected the URL guard to run before the listener is invoked" + ); + assert.ok( + firstListenerCall < firstAwait, + "expected the synchronous listener.call() to precede any await" + ); +});