Skip to content

Instantly share code, notes, and snippets.

@mattzcarey
Last active December 24, 2025 11:58
Show Gist options
  • Select an option

  • Save mattzcarey/4eedfe2b266d378474fa40e1f0d9a911 to your computer and use it in GitHub Desktop.

Select an option

Save mattzcarey/4eedfe2b266d378474fa40e1f0d9a911 to your computer and use it in GitHub Desktop.
Test SSE endpoints for newline terminator stripping (WARP/proxy issues)
#!/usr/bin/env npx tsx
/**
* Script to test if SSE responses have proper newline terminators.
*
* SSE (Server-Sent Events) requires messages to be terminated with \n\n (two newlines).
* Some proxies (like Cloudflare WARP) may strip trailing newlines, causing EventSource
* to hang waiting for message terminators that never arrive.
*
* Usage:
* npx tsx test-sse-newlines.ts [url]
*
* Example:
* npx tsx test-sse-newlines.ts https://example.com/sse
*/
const DEFAULT_URL = "https://docs.mcp.cloudflare.com/sse";
const url = process.argv[2] || DEFAULT_URL;
async function testSSENewlines(url: string): Promise<void> {
console.log(`Testing SSE endpoint: ${url}\n`);
const controller = new AbortController();
const rawBytes: number[] = [];
let buffer = "";
let messageCount = 0;
let hasDoubleNewline = false;
const timeout = setTimeout(() => {
console.log("\n⏱️ Timeout reached (5s) - aborting request\n");
controller.abort();
}, 5000);
try {
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "text/event-stream",
},
signal: controller.signal,
});
if (!response.ok) {
console.error(`❌ HTTP error: ${response.status} ${response.statusText}`);
return;
}
const contentType = response.headers.get("content-type");
console.log(`Content-Type: ${contentType}`);
if (!contentType?.includes("text/event-stream")) {
console.warn("⚠️ Warning: Content-Type is not text/event-stream");
}
const reader = response.body?.getReader();
if (!reader) {
console.error("❌ No response body");
return;
}
const decoder = new TextDecoder();
console.log("\n--- Raw SSE Data ---\n");
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log("\n--- End of stream ---\n");
break;
}
// Collect raw bytes for analysis
for (const byte of value) {
rawBytes.push(byte);
}
const chunk = decoder.decode(value, { stream: true });
buffer += chunk;
// Print chunk with visible newlines
const visibleChunk = chunk
.replace(/\r\n/g, "\\r\\n⏎\n")
.replace(/\n/g, "\\n⏎\n")
.replace(/\r/g, "\\r");
process.stdout.write(visibleChunk);
// Check for double newlines (SSE message terminators)
if (buffer.includes("\n\n") || buffer.includes("\r\n\r\n")) {
hasDoubleNewline = true;
messageCount++;
// Keep only the last incomplete message
const lastDoubleNewline = Math.max(
buffer.lastIndexOf("\n\n"),
buffer.lastIndexOf("\r\n\r\n")
);
if (lastDoubleNewline !== -1) {
buffer = buffer.slice(lastDoubleNewline + 2);
}
}
// Stop after collecting enough data
if (rawBytes.length >= 2048 || messageCount >= 3) {
console.log("\n--- Collected enough data ---\n");
reader.cancel();
break;
}
}
clearTimeout(timeout);
} catch (error) {
clearTimeout(timeout);
if (!(error instanceof Error && error.name === "AbortError")) {
console.error("Error:", error);
return;
}
// AbortError is expected on timeout - continue to analysis
}
// Always analyze what we received
analyzeResults(rawBytes, messageCount, hasDoubleNewline);
}
function analyzeResults(rawBytes: number[], messageCount: number, hasDoubleNewline: boolean): void {
console.log("--- Byte Analysis ---\n");
// Find newline patterns
const newlinePatterns: { position: number; pattern: string }[] = [];
for (let i = 0; i < rawBytes.length; i++) {
if (rawBytes[i] === 0x0a) {
// \n
if (i + 1 < rawBytes.length && rawBytes[i + 1] === 0x0a) {
newlinePatterns.push({ position: i, pattern: "\\n\\n" });
i++; // Skip next \n
} else if (
i > 0 &&
rawBytes[i - 1] === 0x0d &&
i + 1 < rawBytes.length &&
rawBytes[i + 1] === 0x0d &&
i + 2 < rawBytes.length &&
rawBytes[i + 2] === 0x0a
) {
newlinePatterns.push({ position: i - 1, pattern: "\\r\\n\\r\\n" });
i += 2; // Skip remaining bytes
}
}
}
console.log(`Total bytes received: ${rawBytes.length}`);
console.log(`Complete SSE messages: ${messageCount}`);
console.log(`Double-newline terminators found: ${newlinePatterns.length}`);
if (newlinePatterns.length > 0) {
console.log("\nTerminator positions:");
for (const { position, pattern } of newlinePatterns.slice(0, 10)) {
console.log(` Byte ${position}: ${pattern}`);
}
}
// Check the end of the buffer for proper termination
if (rawBytes.length > 0) {
console.log("\n--- Last 30 bytes (hex) ---\n");
const lastBytes = rawBytes.slice(-30);
const hexDump = lastBytes
.map((b) => b.toString(16).padStart(2, "0"))
.join(" ");
const charDump = lastBytes
.map((b) => {
if (b === 0x0a) return "⏎";
if (b === 0x0d) return "↵";
return b >= 32 && b < 127 ? String.fromCharCode(b) : ".";
})
.join("");
console.log(`Hex: ${hexDump}`);
console.log(`Char: ${charDump}`);
// Check if last bytes end with \n\n
const endsWithDoubleNewline =
(rawBytes.length >= 2 && rawBytes[rawBytes.length - 1] === 0x0a && rawBytes[rawBytes.length - 2] === 0x0a) ||
(rawBytes.length >= 4 && rawBytes[rawBytes.length - 1] === 0x0a && rawBytes[rawBytes.length - 2] === 0x0d &&
rawBytes[rawBytes.length - 3] === 0x0a && rawBytes[rawBytes.length - 4] === 0x0d);
console.log(`\nEnds with \\n\\n: ${endsWithDoubleNewline ? "YES" : "NO"}`);
}
// Final verdict
console.log("\n--- Verdict ---\n");
if (rawBytes.length === 0) {
console.log("⚠️ No data received - check if the endpoint is working");
} else if (!hasDoubleNewline && newlinePatterns.length === 0) {
console.log("❌ PROBLEM DETECTED: No SSE message terminators (\\n\\n) found!");
console.log(" A proxy is likely stripping trailing newlines.");
console.log(" EventSource clients will hang waiting for message boundaries.\n");
console.log("Possible causes:");
console.log(" - Cloudflare WARP proxy stripping newlines");
console.log(" - Corporate proxy modifying response");
console.log(" - VPN intercepting traffic");
} else if (messageCount > 0 || hasDoubleNewline) {
console.log(`✅ SSE format looks correct`);
console.log(" Double-newline terminators (\\n\\n) are present.");
console.log(" EventSource should parse messages correctly.");
} else {
console.log("⚠️ Inconclusive - received data but no complete messages");
}
}
testSSENewlines(url);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment