Download src/plugin-sdk/test-helpers/raw-http-request.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 6.19 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/plugin-sdk/test-helpers/raw-http-request.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/plugin-sdk/test-helpers/raw-http-request.ts
-
curl -L -o raw-http-request.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/plugin-sdk/test-helpers/raw-http-request.ts
6.19 kB
| /** | |
| * Raw-socket HTTP request driver for webhook tests. | |
| * | |
| * `fetch` cannot express a server that answers while the sender is still uploading and | |
| * then closes: undici surfaces the failed upload and discards the response it already | |
| * received. Body-limit rejections are exactly that shape, so asserting them needs a | |
| * client that reports the status line and the connection outcome independently. | |
| */ | |
| import net from "node:net"; | |
| export type RawHttpResult = { | |
| /** First line of the response, or an empty string when the server sent nothing. */ | |
| statusLine: string; | |
| /** Lowercase response header names and their final values. */ | |
| headers: Record<string, string>; | |
| /** Response body after the header block. */ | |
| body: string; | |
| /** True when the server closed the connection rather than leaving it open. */ | |
| closedByServer: boolean; | |
| }; | |
| /** | |
| * Reassembles a chunked response body so callers can assert the payload the server sent. | |
| * | |
| * Chunk sizes count bytes, so the scan stays on the Buffer: decoding first would make a | |
| * multi-byte character consume one slice position and misplace every later boundary. | |
| */ | |
| function decodeChunkedBody(raw: Buffer): Buffer { | |
| const parts: Buffer[] = []; | |
| let offset = 0; | |
| for (;;) { | |
| const lineEnd = raw.indexOf("\r\n", offset, "latin1"); | |
| if (lineEnd === -1) { | |
| break; | |
| } | |
| const size = Number.parseInt(raw.toString("latin1", offset, lineEnd).trim(), 16); | |
| if (!Number.isFinite(size) || size <= 0) { | |
| break; | |
| } | |
| parts.push(raw.subarray(lineEnd + 2, lineEnd + 2 + size)); | |
| offset = lineEnd + 2 + size + 2; | |
| } | |
| return Buffer.concat(parts); | |
| } | |
| export async function postRawWebhook(params: { | |
| /** Absolute URL of the webhook endpoint. */ | |
| url: string; | |
| /** Request body. */ | |
| body: string; | |
| headers?: Record<string, string>; | |
| /** How long to keep the socket open before reporting it as retained. */ | |
| idleTimeoutMs?: number; | |
| /** | |
| * Send the body incrementally instead of in one write, so the server decides while the | |
| * upload is still active rather than from the declared length alone. | |
| */ | |
| chunk?: { bytes: number; intervalMs: number }; | |
| /** Content-Length to declare; defaults to the body's real length. */ | |
| contentLength?: number; | |
| /** | |
| * Send with chunked transfer encoding and no Content-Length, so a size limit can only be | |
| * detected from the bytes that actually arrive rather than from a declared length. | |
| */ | |
| chunkedEncoding?: boolean; | |
| }): Promise<RawHttpResult> { | |
| const target = new URL(params.url); | |
| const port = Number(target.port); | |
| const payload = Buffer.from(params.body, "utf-8"); | |
| const idleTimeoutMs = params.idleTimeoutMs ?? 2_000; | |
| const headerLines = Object.entries(params.headers ?? {}) | |
| .map(([name, value]) => `${name}: ${value}\r\n`) | |
| .join(""); | |
| const head = | |
| `POST ${target.pathname}${target.search} HTTP/1.1\r\n` + | |
| `Host: ${target.hostname}:${port}\r\n` + | |
| headerLines + | |
| (params.chunkedEncoding | |
| ? `Transfer-Encoding: chunked\r\n\r\n` | |
| : `Content-Length: ${params.contentLength ?? payload.length}\r\n\r\n`); | |
| return await new Promise<RawHttpResult>((resolve) => { | |
| const socket = net.connect(port, target.hostname); | |
| const received: Buffer[] = []; | |
| let settled = false; | |
| let timer: ReturnType<typeof setTimeout> | undefined; | |
| const settle = (closedByServer: boolean) => { | |
| if (settled) { | |
| return; | |
| } | |
| settled = true; | |
| if (timer) { | |
| clearTimeout(timer); | |
| } | |
| socket.destroy(); | |
| const raw = Buffer.concat(received); | |
| const headerEnd = raw.indexOf("\r\n\r\n", 0, "latin1"); | |
| const headBlock = | |
| headerEnd === -1 ? raw.toString("latin1") : raw.toString("latin1", 0, headerEnd); | |
| const rawBody = headerEnd === -1 ? Buffer.alloc(0) : raw.subarray(headerEnd + 4); | |
| const chunked = /transfer-encoding:\s*chunked/i.test(headBlock); | |
| const headers = Object.fromEntries( | |
| headBlock | |
| .split("\r\n") | |
| .slice(1) | |
| .map((line) => { | |
| const separator = line.indexOf(":"); | |
| return [line.slice(0, separator).toLowerCase(), line.slice(separator + 1).trim()]; | |
| }), | |
| ); | |
| resolve({ | |
| statusLine: (headBlock.split("\r\n")[0] ?? "").trim(), | |
| headers, | |
| body: (chunked ? decodeChunkedBody(rawBody) : rawBody).toString("utf-8"), | |
| closedByServer, | |
| }); | |
| }; | |
| // Chunk framing is ASCII and the body is raw bytes. Writing them together through a | |
| // string would re-encode every byte above 0x7f after its length was already declared. | |
| const writeBody = (slice: Buffer) => { | |
| if (!params.chunkedEncoding) { | |
| socket.write(slice); | |
| return; | |
| } | |
| socket.write(`${slice.length.toString(16)}\r\n`); | |
| socket.write(slice); | |
| socket.write("\r\n"); | |
| }; | |
| socket.on("connect", () => { | |
| socket.write(head); | |
| if (!params.chunk) { | |
| writeBody(payload); | |
| if (params.chunkedEncoding) { | |
| socket.write("0\r\n\r\n"); | |
| } | |
| } else { | |
| const { bytes, intervalMs } = params.chunk; | |
| let offset = 0; | |
| const pump = () => { | |
| if (settled || socket.destroyed) { | |
| return; | |
| } | |
| if (offset >= payload.length) { | |
| if (params.chunkedEncoding) { | |
| socket.write(`0\r\n\r\n`); | |
| } | |
| return; | |
| } | |
| const end = Math.min(offset + bytes, payload.length); | |
| writeBody(payload.subarray(offset, end)); | |
| offset = end; | |
| setTimeout(pump, intervalMs).unref?.(); | |
| }; | |
| pump(); | |
| } | |
| // Only the idle timeout reports a retained connection; a server that answers and | |
| // closes always reaches "close" below. | |
| timer = setTimeout(() => settle(false), idleTimeoutMs); | |
| timer.unref?.(); | |
| }); | |
| socket.on("data", (chunk: Buffer) => { | |
| received.push(chunk); | |
| }); | |
| // Rejecting mid-upload makes the write fail on this side; the response may already be | |
| // buffered, so wait for "close" rather than settling on the write error. | |
| socket.on("error", () => {}); | |
| socket.on("close", () => settle(true)); | |
| }); | |
| } | |