mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-14 22:54:01 -06:00
445e262d26
* fix(proxy-capture): respect response backpressure * test(proxy-capture): avoid unbound prototype method lint * test(proxy-capture): stabilize backpressure regression * test(proxy-capture): fix backpressure write typing * test(proxy-capture): satisfy lint for method helper * fix(proxy-capture): settle forwarding only after downstream finish --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
647 lines
20 KiB
TypeScript
647 lines
20 KiB
TypeScript
// Proxy capture server tests cover request recording and response handling.
|
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import {
|
|
request as httpRequest,
|
|
createServer as createHttpServer,
|
|
type IncomingMessage,
|
|
} from "node:http";
|
|
import net, { type AddressInfo } from "node:net";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
|
import type { DebugProxySettings } from "./env.js";
|
|
import { startDebugProxyServer } from "./proxy-server.js";
|
|
import { closeDebugProxyCaptureStore, getDebugProxyCaptureStore } from "./store.sqlite.js";
|
|
|
|
let testRoot: string | undefined;
|
|
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
|
|
async function cleanupTestRoot(): Promise<void> {
|
|
closeDebugProxyCaptureStore();
|
|
closeOpenClawStateDatabaseForTest();
|
|
if (originalStateDir === undefined) {
|
|
delete process.env.OPENCLAW_STATE_DIR;
|
|
} else {
|
|
process.env.OPENCLAW_STATE_DIR = originalStateDir;
|
|
}
|
|
if (!testRoot) {
|
|
return;
|
|
}
|
|
const root = testRoot;
|
|
testRoot = undefined;
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
|
|
async function makeSettings(): Promise<DebugProxySettings> {
|
|
testRoot = await mkdtemp(join(tmpdir(), "openclaw-debug-proxy-server-"));
|
|
const certDir = join(testRoot, "certs");
|
|
await mkdir(certDir, { recursive: true });
|
|
await writeFile(join(certDir, "root-ca.pem"), "test root cert\n", "utf8");
|
|
await writeFile(join(certDir, "root-ca-key.pem"), "test root key\n", "utf8");
|
|
process.env.OPENCLAW_STATE_DIR = testRoot;
|
|
return {
|
|
enabled: true,
|
|
required: false,
|
|
dbPath: join(testRoot, "capture.sqlite"),
|
|
blobDir: join(testRoot, "blobs"),
|
|
certDir,
|
|
sessionId: "debug-proxy-server-test",
|
|
sourceProcess: "test",
|
|
};
|
|
}
|
|
|
|
async function startLargeBodyOrigin(responseBody: string): Promise<{
|
|
receivedRequestBody: () => string;
|
|
responseBody: string;
|
|
stop: () => Promise<void>;
|
|
url: string;
|
|
}> {
|
|
let receivedBody = "";
|
|
const server = createHttpServer((req, res) => {
|
|
req.setEncoding("utf8");
|
|
req.on("data", (chunk) => {
|
|
receivedBody += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
res.writeHead(200, {
|
|
"content-length": Buffer.byteLength(responseBody),
|
|
"content-type": "text/plain; charset=utf-8",
|
|
});
|
|
res.end(responseBody);
|
|
});
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
server.off("error", reject);
|
|
resolve();
|
|
});
|
|
});
|
|
const address = server.address() as AddressInfo;
|
|
return {
|
|
receivedRequestBody: () => receivedBody,
|
|
responseBody,
|
|
stop: async () =>
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
}),
|
|
url: `http://127.0.0.1:${address.port}/capture`,
|
|
};
|
|
}
|
|
|
|
async function startResponseErrorOrigin(): Promise<{
|
|
stop: () => Promise<void>;
|
|
url: string;
|
|
}> {
|
|
const server = createHttpServer((req, res) => {
|
|
if (req.url === "/before-headers") {
|
|
res.socket?.destroy();
|
|
return;
|
|
}
|
|
if (req.url === "/after-headers") {
|
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
res.flushHeaders();
|
|
res.write("partial");
|
|
setTimeout(() => res.socket?.destroy(), 50);
|
|
return;
|
|
}
|
|
res.writeHead(200, {
|
|
"content-length": 2,
|
|
"content-type": "text/plain; charset=utf-8",
|
|
});
|
|
res.end("ok");
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
server.off("error", reject);
|
|
resolve();
|
|
});
|
|
});
|
|
const address = server.address() as AddressInfo;
|
|
return {
|
|
stop: async () =>
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
}),
|
|
url: `http://127.0.0.1:${address.port}`,
|
|
};
|
|
}
|
|
|
|
type ProxyResponseResult = {
|
|
body: string;
|
|
complete: boolean;
|
|
errorMessage?: string;
|
|
statusCode?: number;
|
|
};
|
|
|
|
async function getThroughProxy(proxyUrl: string, targetUrl: string): Promise<ProxyResponseResult> {
|
|
const proxy = new URL(proxyUrl);
|
|
return await new Promise<ProxyResponseResult>((resolve) => {
|
|
let settled = false;
|
|
let body = "";
|
|
let response: IncomingMessage | undefined;
|
|
const finish = (error?: Error) => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
resolve({
|
|
body,
|
|
complete: response?.complete ?? false,
|
|
...(error ? { errorMessage: error.message } : {}),
|
|
...(response?.statusCode === undefined ? {} : { statusCode: response.statusCode }),
|
|
});
|
|
};
|
|
const req = httpRequest(
|
|
{
|
|
host: proxy.hostname,
|
|
port: Number(proxy.port),
|
|
method: "GET",
|
|
path: targetUrl,
|
|
headers: { connection: "close" },
|
|
},
|
|
(res) => {
|
|
response = res;
|
|
res.setEncoding("utf8");
|
|
res.on("data", (chunk) => {
|
|
body += chunk;
|
|
});
|
|
res.on("end", () => finish());
|
|
res.on("error", finish);
|
|
res.on("close", () => {
|
|
if (!res.complete) {
|
|
finish(new Error("response closed before completion"));
|
|
}
|
|
});
|
|
},
|
|
);
|
|
req.on("error", finish);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
async function postThroughProxy(params: {
|
|
body: string;
|
|
proxyUrl: string;
|
|
targetUrl: string;
|
|
}): Promise<string> {
|
|
const proxy = new URL(params.proxyUrl);
|
|
return await new Promise<string>((resolve, reject) => {
|
|
const req = httpRequest(
|
|
{
|
|
host: proxy.hostname,
|
|
port: Number(proxy.port),
|
|
method: "POST",
|
|
path: params.targetUrl,
|
|
headers: {
|
|
connection: "close",
|
|
"content-length": Buffer.byteLength(params.body),
|
|
"content-type": "text/plain; charset=utf-8",
|
|
},
|
|
},
|
|
(res) => {
|
|
const chunks: Buffer[] = [];
|
|
res.on("data", (chunk) => {
|
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
});
|
|
res.on("end", () => {
|
|
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
});
|
|
},
|
|
);
|
|
req.on("error", reject);
|
|
req.end(params.body);
|
|
});
|
|
}
|
|
|
|
type StreamingProxyOrigin = {
|
|
state: {
|
|
closedResponses: number;
|
|
drainWaits: number;
|
|
finishedResponses: number;
|
|
queuedBytes: number;
|
|
};
|
|
stop: () => Promise<void>;
|
|
url: string;
|
|
};
|
|
|
|
async function startStreamingProxyOrigin(responseBodyBytes: number): Promise<StreamingProxyOrigin> {
|
|
const state = {
|
|
closedResponses: 0,
|
|
drainWaits: 0,
|
|
finishedResponses: 0,
|
|
queuedBytes: 0,
|
|
};
|
|
const chunk = Buffer.alloc(64 * 1024, "x");
|
|
const server = createHttpServer((req, res) => {
|
|
req.resume();
|
|
if (req.url === "/healthy") {
|
|
res.writeHead(200, {
|
|
"content-length": 2,
|
|
"content-type": "text/plain; charset=utf-8",
|
|
});
|
|
res.end("ok");
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
"content-length": responseBodyBytes,
|
|
"content-type": "text/plain; charset=utf-8",
|
|
});
|
|
res.on("close", () => {
|
|
state.closedResponses++;
|
|
});
|
|
res.on("finish", () => {
|
|
state.finishedResponses++;
|
|
});
|
|
|
|
const writeNextChunk = () => {
|
|
while (state.queuedBytes < responseBodyBytes && !res.destroyed) {
|
|
const chunkBytes = Math.min(chunk.byteLength, responseBodyBytes - state.queuedBytes);
|
|
state.queuedBytes += chunkBytes;
|
|
if (!res.write(chunk.subarray(0, chunkBytes))) {
|
|
state.drainWaits++;
|
|
res.once("drain", writeNextChunk);
|
|
return;
|
|
}
|
|
}
|
|
if (state.queuedBytes === responseBodyBytes && !res.destroyed) {
|
|
res.end();
|
|
}
|
|
};
|
|
writeNextChunk();
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", () => {
|
|
server.off("error", reject);
|
|
resolve();
|
|
});
|
|
});
|
|
const address = server.address() as AddressInfo;
|
|
return {
|
|
state,
|
|
stop: async () =>
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
}),
|
|
url: `http://127.0.0.1:${address.port}`,
|
|
};
|
|
}
|
|
|
|
async function rawSlowGetThroughProxy(params: {
|
|
abortAfterBytes?: number;
|
|
abortWithReset?: boolean;
|
|
onResume?: () => void;
|
|
pauseBeforeReadMs: number;
|
|
proxyUrl: string;
|
|
targetUrl: string;
|
|
}): Promise<{
|
|
aborted: boolean;
|
|
bodyBytes: number;
|
|
receivedBytes: number;
|
|
resumed: boolean;
|
|
statusLine: string;
|
|
}> {
|
|
const proxy = new URL(params.proxyUrl);
|
|
return await new Promise((resolve, reject) => {
|
|
let aborted = false;
|
|
let bodyBytes = 0;
|
|
let contentLength: number | undefined;
|
|
let headerBytes = Buffer.alloc(0);
|
|
let receivedBytes = 0;
|
|
let resumed = false;
|
|
let settled = false;
|
|
let statusLine = "";
|
|
let resumeTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
const socket = net.connect(Number(proxy.port), proxy.hostname);
|
|
const timeout = setTimeout(() => {
|
|
socket.destroy();
|
|
if (!settled) {
|
|
settled = true;
|
|
reject(new Error("slow proxy client timed out"));
|
|
}
|
|
}, 15000);
|
|
const finish = () => {
|
|
clearTimeout(timeout);
|
|
clearTimeout(resumeTimeout);
|
|
if (!settled) {
|
|
settled = true;
|
|
resolve({ aborted, bodyBytes, receivedBytes, resumed, statusLine });
|
|
}
|
|
};
|
|
const maybeFinishCompleteBody = () => {
|
|
if (contentLength !== undefined && bodyBytes >= contentLength) {
|
|
socket.destroy();
|
|
finish();
|
|
}
|
|
};
|
|
socket.on("connect", () => {
|
|
socket.write(
|
|
`GET ${params.targetUrl} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n`,
|
|
);
|
|
socket.pause();
|
|
resumeTimeout = setTimeout(() => {
|
|
resumed = true;
|
|
params.onResume?.();
|
|
socket.resume();
|
|
}, params.pauseBeforeReadMs);
|
|
});
|
|
socket.on("data", (chunk) => {
|
|
receivedBytes += chunk.length;
|
|
if (contentLength === undefined) {
|
|
headerBytes = Buffer.concat([headerBytes, Buffer.from(chunk)]);
|
|
const headerEnd = headerBytes.indexOf("\r\n\r\n");
|
|
if (headerEnd === -1) {
|
|
return;
|
|
}
|
|
const headerText = headerBytes.subarray(0, headerEnd).toString("latin1");
|
|
statusLine = headerText.split("\r\n")[0] ?? "";
|
|
const contentLengthHeader = headerText
|
|
.split("\r\n")
|
|
.find((line) => line.toLowerCase().startsWith("content-length:"));
|
|
const parsedContentLength = Number(contentLengthHeader?.split(":")[1]?.trim());
|
|
contentLength = Number.isFinite(parsedContentLength) ? parsedContentLength : 0;
|
|
bodyBytes += headerBytes.byteLength - headerEnd - 4;
|
|
headerBytes = Buffer.alloc(0);
|
|
} else {
|
|
bodyBytes += chunk.length;
|
|
}
|
|
if (params.abortAfterBytes !== undefined && bodyBytes >= params.abortAfterBytes) {
|
|
aborted = true;
|
|
if (params.abortWithReset) {
|
|
socket.resetAndDestroy();
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
finish();
|
|
return;
|
|
}
|
|
maybeFinishCompleteBody();
|
|
});
|
|
socket.on("error", (error) => {
|
|
clearTimeout(timeout);
|
|
clearTimeout(resumeTimeout);
|
|
if (!settled) {
|
|
settled = true;
|
|
reject(error);
|
|
}
|
|
});
|
|
socket.on("close", finish);
|
|
});
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestRoot();
|
|
});
|
|
|
|
describe("startDebugProxyServer", () => {
|
|
it("caps UTF-8 previews on character boundaries while forwarding full bodies", async () => {
|
|
const settings = await makeSettings();
|
|
const origin = await startLargeBodyOrigin(`${"r".repeat(8191)}😀tail`);
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
const requestBody = `${"q".repeat(8191)}étail`;
|
|
|
|
try {
|
|
const responseBody = await postThroughProxy({
|
|
body: requestBody,
|
|
proxyUrl: proxy.proxyUrl,
|
|
targetUrl: origin.url,
|
|
});
|
|
|
|
expect(origin.receivedRequestBody()).toBe(requestBody);
|
|
expect(responseBody).toBe(origin.responseBody);
|
|
const events = getDebugProxyCaptureStore().getSessionEvents(settings.sessionId, 10);
|
|
const capturedRequest = events.find((event) => event.kind === "request");
|
|
const capturedResponse = events.find((event) => event.kind === "response");
|
|
expect(capturedRequest?.dataText).toBe("q".repeat(8191));
|
|
expect(capturedResponse?.dataText).toBe("r".repeat(8191));
|
|
expect(JSON.parse(String(capturedRequest?.metaJson))).toMatchObject({
|
|
bodyBytes: Buffer.byteLength(requestBody),
|
|
capturePreviewBytes: 8192,
|
|
captureTruncated: true,
|
|
});
|
|
expect(JSON.parse(String(capturedResponse?.metaJson))).toMatchObject({
|
|
bodyBytes: Buffer.byteLength(origin.responseBody),
|
|
capturePreviewBytes: 8192,
|
|
captureTruncated: true,
|
|
});
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
|
|
it("pauses the upstream response while downstream forwarding is backpressured", async () => {
|
|
const settings = await makeSettings();
|
|
const responseBodyBytes = 16 * 1024 * 1024;
|
|
const origin = await startStreamingProxyOrigin(responseBodyBytes);
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
let queuedBytesAtResume = 0;
|
|
|
|
try {
|
|
const forwarded = await rawSlowGetThroughProxy({
|
|
onResume: () => {
|
|
queuedBytesAtResume = origin.state.queuedBytes;
|
|
},
|
|
pauseBeforeReadMs: 150,
|
|
proxyUrl: proxy.proxyUrl,
|
|
targetUrl: `${origin.url}/capture`,
|
|
});
|
|
|
|
expect(forwarded).toMatchObject({
|
|
aborted: false,
|
|
bodyBytes: responseBodyBytes,
|
|
resumed: true,
|
|
statusLine: "HTTP/1.1 200 OK",
|
|
});
|
|
expect(queuedBytesAtResume).toBeLessThan(responseBodyBytes);
|
|
expect(origin.state.drainWaits).toBeGreaterThan(0);
|
|
expect(origin.state.queuedBytes).toBe(responseBodyBytes);
|
|
const captureEvents = getDebugProxyCaptureStore()
|
|
.getSessionEvents(settings.sessionId, 20)
|
|
.filter((event) => event.path === "/capture");
|
|
expect(captureEvents.filter((event) => event.kind === "error")).toEqual([]);
|
|
expect(captureEvents.filter((event) => event.kind === "response")).toEqual([
|
|
expect.objectContaining({ direction: "inbound", status: 200 }),
|
|
]);
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
|
|
it("closes the upstream response when a slow downstream client aborts", async () => {
|
|
const settings = await makeSettings();
|
|
const responseBodyBytes = 16 * 1024 * 1024;
|
|
const origin = await startStreamingProxyOrigin(responseBodyBytes);
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
|
|
try {
|
|
const aborted = await rawSlowGetThroughProxy({
|
|
abortAfterBytes: 64 * 1024,
|
|
pauseBeforeReadMs: 0,
|
|
proxyUrl: proxy.proxyUrl,
|
|
targetUrl: `${origin.url}/capture`,
|
|
});
|
|
|
|
expect(aborted).toMatchObject({
|
|
aborted: true,
|
|
resumed: true,
|
|
statusLine: "HTTP/1.1 200 OK",
|
|
});
|
|
expect(aborted.bodyBytes).toBeGreaterThanOrEqual(64 * 1024);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(origin.state.closedResponses).toBe(1);
|
|
expect(origin.state.finishedResponses).toBe(0);
|
|
const captureEvents = getDebugProxyCaptureStore()
|
|
.getSessionEvents(settings.sessionId, 20)
|
|
.filter((event) => event.path === "/capture");
|
|
const capturedRequest = captureEvents.find((event) => event.kind === "request");
|
|
expect(capturedRequest).toBeDefined();
|
|
expect(captureEvents.filter((event) => event.kind === "error")).toEqual([
|
|
expect.objectContaining({
|
|
direction: "local",
|
|
errorText: "Downstream response closed before completion",
|
|
flowId: capturedRequest?.flowId,
|
|
}),
|
|
]);
|
|
expect(captureEvents.filter((event) => event.kind === "response")).toEqual([]);
|
|
});
|
|
|
|
const healthy = await getThroughProxy(proxy.proxyUrl, `${origin.url}/healthy`);
|
|
expect(healthy).toMatchObject({ body: "ok", complete: true, statusCode: 200 });
|
|
expect(
|
|
getDebugProxyCaptureStore()
|
|
.getSessionEvents(settings.sessionId, 20)
|
|
.filter((event) => event.path === "/healthy" && event.kind === "error"),
|
|
).toEqual([]);
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
|
|
it("records a reset downstream client as exactly one local capture error", async () => {
|
|
const settings = await makeSettings();
|
|
const origin = await startStreamingProxyOrigin(16 * 1024 * 1024);
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
|
|
try {
|
|
const aborted = await rawSlowGetThroughProxy({
|
|
abortAfterBytes: 64 * 1024,
|
|
abortWithReset: true,
|
|
pauseBeforeReadMs: 0,
|
|
proxyUrl: proxy.proxyUrl,
|
|
targetUrl: `${origin.url}/capture`,
|
|
});
|
|
|
|
expect(aborted).toMatchObject({
|
|
aborted: true,
|
|
resumed: true,
|
|
statusLine: "HTTP/1.1 200 OK",
|
|
});
|
|
|
|
await vi.waitFor(() => {
|
|
expect(origin.state.closedResponses).toBe(1);
|
|
expect(origin.state.finishedResponses).toBe(0);
|
|
const captureEvents = getDebugProxyCaptureStore()
|
|
.getSessionEvents(settings.sessionId, 20)
|
|
.filter((event) => event.path === "/capture");
|
|
const capturedRequest = captureEvents.find((event) => event.kind === "request");
|
|
expect(capturedRequest).toBeDefined();
|
|
expect(captureEvents.filter((event) => event.kind === "error")).toEqual([
|
|
expect.objectContaining({
|
|
direction: "local",
|
|
errorText: expect.any(String),
|
|
flowId: capturedRequest?.flowId,
|
|
}),
|
|
]);
|
|
expect(captureEvents.filter((event) => event.kind === "response")).toEqual([]);
|
|
});
|
|
|
|
const healthy = await getThroughProxy(proxy.proxyUrl, `${origin.url}/healthy`);
|
|
expect(healthy).toMatchObject({ body: "ok", complete: true, statusCode: 200 });
|
|
expect(
|
|
getDebugProxyCaptureStore()
|
|
.getSessionEvents(settings.sessionId, 20)
|
|
.filter((event) => event.path === "/healthy" && event.kind === "error"),
|
|
).toEqual([]);
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
|
|
it("returns a complete 502 and survives an upstream failure before response headers", async () => {
|
|
const settings = await makeSettings();
|
|
const origin = await startResponseErrorOrigin();
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
|
|
try {
|
|
const failed = await getThroughProxy(proxy.proxyUrl, `${origin.url}/before-headers`);
|
|
expect(failed).toMatchObject({
|
|
body: "Bad Gateway\n",
|
|
complete: true,
|
|
statusCode: 502,
|
|
});
|
|
|
|
const healthy = await getThroughProxy(proxy.proxyUrl, `${origin.url}/healthy`);
|
|
expect(healthy).toMatchObject({ body: "ok", complete: true, statusCode: 200 });
|
|
expect(getDebugProxyCaptureStore().getSessionEvents(settings.sessionId, 20)).toContainEqual(
|
|
expect.objectContaining({ direction: "local", kind: "error" }),
|
|
);
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
|
|
it("aborts a partial response after headers and survives the upstream stream error", async () => {
|
|
const settings = await makeSettings();
|
|
const origin = await startResponseErrorOrigin();
|
|
const proxy = await startDebugProxyServer({ settings });
|
|
|
|
try {
|
|
const failed = await getThroughProxy(proxy.proxyUrl, `${origin.url}/after-headers`);
|
|
expect(failed).toMatchObject({
|
|
body: "partial",
|
|
complete: false,
|
|
statusCode: 200,
|
|
});
|
|
expect(failed.errorMessage).toBeDefined();
|
|
|
|
const healthy = await getThroughProxy(proxy.proxyUrl, `${origin.url}/healthy`);
|
|
expect(healthy).toMatchObject({ body: "ok", complete: true, statusCode: 200 });
|
|
expect(getDebugProxyCaptureStore().getSessionEvents(settings.sessionId, 20)).toContainEqual(
|
|
expect.objectContaining({ direction: "inbound", kind: "error" }),
|
|
);
|
|
} finally {
|
|
await proxy.stop();
|
|
await origin.stop();
|
|
}
|
|
});
|
|
});
|