Created
December 4, 2025 04:52
-
-
Save lawrencecchen/755a82c2b391da7c8ab24cb0c940e506 to your computer and use it in GitHub Desktop.
Freestyle WebSocket repro - headers stripped
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Minimal WebSocket Reproduction for Freestyle | |
| * | |
| * ISSUE: WebSocket connections fail through Freestyle web deployments. | |
| * | |
| * Expected behavior: WebSocket upgrade should work | |
| * Actual behavior: Connection/Upgrade headers are stripped by the platform | |
| * | |
| * Test endpoints: | |
| * GET /debug-headers - Shows what headers arrive (demonstrates stripped headers) | |
| * GET /ws-echo - WebSocket echo endpoint (never receives upgrade event) | |
| * GET / - Simple health check | |
| * | |
| * Test commands: | |
| * curl https://<deployment>/debug-headers -H "Connection: Upgrade" -H "Upgrade: websocket" | |
| * websocat wss://<deployment>/ws-echo | |
| */ | |
| import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; | |
| import { createHash } from "node:crypto"; | |
| import type { Socket } from "node:net"; | |
| function handleRequest(req: IncomingMessage, res: ServerResponse): void { | |
| const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); | |
| // Health check | |
| if (url.pathname === "/") { | |
| res.writeHead(200, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify({ status: "ok", version: "ws-repro-1.0" })); | |
| return; | |
| } | |
| // Debug headers - shows what the server receives | |
| // Use: curl <url>/debug-headers -H "Connection: Upgrade" -H "Upgrade: websocket" | |
| if (url.pathname === "/debug-headers") { | |
| res.writeHead(200, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify({ | |
| info: "Headers received by server", | |
| method: req.method, | |
| headers: req.headers, | |
| note: "Check if 'connection' and 'upgrade' headers are present", | |
| }, null, 2)); | |
| return; | |
| } | |
| // WebSocket endpoint - will never receive upgrade event if headers are stripped | |
| if (url.pathname === "/ws-echo") { | |
| // If we reach here, the upgrade event was NOT triggered | |
| // This means Connection/Upgrade headers were stripped | |
| res.writeHead(400, { "Content-Type": "application/json" }); | |
| res.end(JSON.stringify({ | |
| error: "WebSocket upgrade failed", | |
| reason: "Server received regular HTTP request instead of WebSocket upgrade", | |
| headers_received: { | |
| connection: req.headers.connection, | |
| upgrade: req.headers.upgrade, | |
| "sec-websocket-key": req.headers["sec-websocket-key"], | |
| }, | |
| expected: { | |
| connection: "Upgrade", | |
| upgrade: "websocket", | |
| }, | |
| }, null, 2)); | |
| return; | |
| } | |
| res.writeHead(404, { "Content-Type": "text/plain" }); | |
| res.end("Not found"); | |
| } | |
| const server = createServer((req, res) => { | |
| handleRequest(req, res); | |
| }); | |
| // WebSocket upgrade handler - should be triggered for WebSocket requests | |
| server.on("upgrade", (req: IncomingMessage, socket: Socket, head: Buffer) => { | |
| console.log("[WS] Upgrade event received!"); | |
| const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`); | |
| if (url.pathname === "/ws-echo") { | |
| const wsKey = req.headers["sec-websocket-key"]; | |
| if (!wsKey) { | |
| socket.write("HTTP/1.1 400 Bad Request\r\n\r\n"); | |
| socket.destroy(); | |
| return; | |
| } | |
| // Calculate WebSocket accept key | |
| const acceptKey = createHash("sha1") | |
| .update(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") | |
| .digest("base64"); | |
| // Send 101 Switching Protocols | |
| socket.write( | |
| "HTTP/1.1 101 Switching Protocols\r\n" + | |
| "Upgrade: websocket\r\n" + | |
| "Connection: Upgrade\r\n" + | |
| `Sec-WebSocket-Accept: ${acceptKey}\r\n` + | |
| "\r\n" | |
| ); | |
| console.log("[WS] Upgrade successful, echoing messages"); | |
| // Echo back any data received (raw frames) | |
| socket.on("data", (data) => { | |
| socket.write(data); | |
| }); | |
| socket.on("error", (err) => { | |
| console.log("[WS] Socket error:", err.message); | |
| }); | |
| socket.on("close", () => { | |
| console.log("[WS] Socket closed"); | |
| }); | |
| return; | |
| } | |
| socket.write("HTTP/1.1 404 Not Found\r\n\r\n"); | |
| socket.destroy(); | |
| }); | |
| server.listen(3000, () => { | |
| console.log("WebSocket repro server running on port 3000"); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment