fix: UDP upload was completely broken — relay never received sessions or chunks
Root cause: three critical bugs in the UDP upload flow: 1. Main server never registered sessions on the relay — it stored them in its own memory but never called POST /session on the relay, so the relay had no idea about any upload sessions. 2. Relay had no HTTP chunk endpoint — the Chrome extension sends chunks via HTTP POST to /session/:id/chunk/:index, but the relay only had a binary UDP listener. Added the HTTP fallback endpoint. 3. Relay had no CORS headers — browser requests from chrome-extension:// origins were blocked. Added CORS middleware. The flow now works: Browser → POST /api/udp/session (main server) Main server → POST /session (relay, with s3Config) Browser → POST /session/:id/chunk/:n (relay, via public URL) Relay → S3 multipart upload Browser → POST /api/udp/session/:id/complete (main server) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
42fead82aa
commit
efdaa48cb6
2 changed files with 73 additions and 4 deletions
34
server.js
34
server.js
|
|
@ -587,25 +587,51 @@ app.post("/api/presigned", requireAuth, async (req, res) => {
|
|||
// Sessions stored in memory; relay handles actual transfer
|
||||
const udpSessions = new Map();
|
||||
|
||||
app.post("/api/udp/session", requireAuth, (req, res) => {
|
||||
app.post("/api/udp/session", requireAuth, async (req, res) => {
|
||||
const { filename, size, prefix } = req.body;
|
||||
if (!filename || !size) return res.status(400).json({ success: false, error: "filename and size required" });
|
||||
const relayUrl = db.relayConfig?.relayUrl || "";
|
||||
const udpPort = db.relayConfig?.udpPort || 5000;
|
||||
if (!relayUrl) return res.status(503).json({ success: false, error: "UDP relay not configured. Go to Admin → Relay Settings." });
|
||||
// Public URL is what the browser connects to — falls back to internal if not set
|
||||
const publicRelayUrl = (db.relayConfig?.publicRelayUrl || "").trim() || relayUrl;
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
const key = (prefix ? `${prefix.replace(/[-\/]+$/, "")}--${filename}` : filename);
|
||||
const s3Cfg = db.s3Config || {};
|
||||
|
||||
// Register session on the relay server so it can accept chunks and upload to S3
|
||||
try {
|
||||
const relayResp = await fetch(`${relayUrl}/session`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId, filename, size, key,
|
||||
bucket: s3Cfg.bucket || "",
|
||||
s3Config: {
|
||||
endpoint: s3Cfg.endpoint || "",
|
||||
region: s3Cfg.region || "us-east-1",
|
||||
accessKeyId: s3Cfg.accessKeyId || "",
|
||||
secretAccessKey: s3Cfg.secretAccessKey || "",
|
||||
},
|
||||
}),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!relayResp.ok) {
|
||||
const err = await relayResp.json().catch(() => ({}));
|
||||
return res.status(502).json({ success: false, error: `Relay error: ${err.error || relayResp.status}` });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[UDP] Failed to register session on relay:", err.message);
|
||||
return res.status(502).json({ success: false, error: `Cannot reach relay: ${err.message}` });
|
||||
}
|
||||
|
||||
udpSessions.set(sessionId, {
|
||||
sessionId, filename, size, key,
|
||||
status: "pending",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
// Cleanup after 2 hours
|
||||
setTimeout(() => udpSessions.delete(sessionId), 2 * 60 * 60 * 1000);
|
||||
res.json({ success: true, sessionId, relayUrl: publicRelayUrl, udpPort, key, s3Bucket: db.s3Config?.bucket || "" });
|
||||
res.json({ success: true, sessionId, relayUrl: publicRelayUrl, udpPort, key, s3Bucket: s3Cfg.bucket || "" });
|
||||
});
|
||||
|
||||
app.get("/api/udp/session/:id", requireAuth, (req, res) => {
|
||||
|
|
|
|||
|
|
@ -235,7 +235,18 @@ udpServer.bind(UDP_PORT, "0.0.0.0", () => {
|
|||
|
||||
// ==================== HTTP CONTROL API ====================
|
||||
const app = express();
|
||||
|
||||
// CORS — browsers (Chrome extension) send chunks via HTTP fallback
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
|
||||
if (req.method === "OPTIONS") return res.sendStatus(204);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.raw({ type: "application/octet-stream", limit: "100mb" }));
|
||||
|
||||
// Health check
|
||||
app.get("/health", (req, res) => {
|
||||
|
|
@ -266,6 +277,38 @@ app.post("/session", (req, res) => {
|
|||
res.json({ success: true, sessionId, udpPort: UDP_PORT, chunkSize: CHUNK_SIZE, expectedChunks: session.expectedChunks });
|
||||
});
|
||||
|
||||
// HTTP chunk fallback — Chrome extensions can't send raw UDP, so they POST chunks over HTTP
|
||||
app.post("/session/:id/chunk/:index", async (req, res) => {
|
||||
const session = sessions.get(req.params.id);
|
||||
if (!session) return res.status(404).json({ error: "Session not found" });
|
||||
const chunkIndex = parseInt(req.params.index);
|
||||
if (isNaN(chunkIndex)) return res.status(400).json({ error: "Invalid chunk index" });
|
||||
|
||||
try {
|
||||
// Initialize multipart upload on first chunk
|
||||
if (session.status === "pending") {
|
||||
await session.initMultipart();
|
||||
}
|
||||
await session.receiveChunk(chunkIndex, req.body);
|
||||
|
||||
// Auto-complete when all chunks received
|
||||
if (session.chunks.size === session.expectedChunks && session.status === "receiving") {
|
||||
await session.complete();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
chunkIndex,
|
||||
receivedChunks: session.chunks.size,
|
||||
expectedChunks: session.expectedChunks,
|
||||
percent: session.progress().percent,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[HTTP] Chunk ${chunkIndex} error for ${req.params.id.slice(0, 8)}:`, err.message);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get session progress
|
||||
app.get("/session/:id", (req, res) => {
|
||||
const session = sessions.get(req.params.id);
|
||||
|
|
|
|||
Loading…
Reference in a new issue