Skip to content

Commit

Permalink
feat: Capture HTTP response body
Browse files Browse the repository at this point in the history
  • Loading branch information
zermelo-wisen committed Feb 23, 2024
1 parent d8200c8 commit 3a8a0f1
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/AppMap.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ namespace AppMap {

export interface HttpServerResponseEvent extends ReturnEventBase {
http_server_response: HttpResponse;
return_value?: string | Parameter;
}

export interface HttpClientResponseEvent extends ReturnEventBase {
Expand Down
2 changes: 2 additions & 0 deletions src/Recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export default class Recording {
elapsed: number,
status: number,
headers?: Record<string, string>,
body?: string,
): AppMap.HttpServerResponseEvent {
assert(this.stream);

Expand All @@ -172,6 +173,7 @@ export default class Recording {
thread_id: 0,
parent_id: callId,
elapsed,
return_value: body,
};
this.stream.emit(event);

Expand Down
90 changes: 88 additions & 2 deletions src/hooks/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "node:assert";
import { readFileSync, rmSync } from "node:fs";
import type http from "node:http";
import type { ClientRequest, IncomingMessage, ServerResponse } from "node:http";
import { type ClientRequest, type IncomingMessage, type ServerResponse } from "node:http";
import type https from "node:https";
import { URL } from "node:url";

Expand Down Expand Up @@ -155,6 +155,79 @@ function shouldIgnoreRequest(request: http.IncomingMessage) {
return false;
}

type Chunk = string | Buffer;
const maxResponseBodyCaptureLength = 1000;

function captureResponseBody(response: http.ServerResponse, chunks: Chunk[]) {
let currentLength = 0;
function captureChunk(chunk: Chunk) {
if (maxResponseBodyCaptureLength <= currentLength) return;

chunks.push(chunk);
currentLength += chunk.length;
}

const originalWrite = response.write.bind(response);
const originalEnd = response.end.bind(response);

response.write = function (...args: unknown[]) {
console.log("WRITE", args.length);
let callback: unknown;
let encoding: unknown;
const chunk = args[0] as Buffer;
// write has these call signatures
// (chunk, callback?)
// (chunk, encoding, callback?)
let argsToConsider = args.length;
// If the last parameter is a function than it's the callback
if (args.length > 0 && typeof args[args.length - 1] === "function") {
callback = args[args.length - 1];
argsToConsider--;
}
if (argsToConsider > 1) encoding = args[1];

captureChunk(chunk);

if (encoding != null)
return originalWrite(
chunk,
encoding as BufferEncoding,
callback as ((error: Error | null | undefined) => void) | undefined,
);

return originalWrite(
chunk,
callback as ((error: Error | null | undefined) => void) | undefined,
);
};

response.end = function (...args: unknown[]) {
console.log("ARGS", args);
console.log("ARGS", args);

// end has these call signatures:
// (cb?); (chunk, cb?); (chunk, encoding, cb?)
// If the last parameter is a function than it's the callback
const argsCountBeforeCallback =
args.length > 0 && typeof args[args.length - 1] === "function"
? args.length - 1
: args.length;
if (argsCountBeforeCallback > 0) captureChunk(args[0] as Chunk);

//console.log(args);
//console.log("ARGS", args);

let result: ReturnType<typeof response.end>;
if (args.length == 3)
result = originalEnd(args[0], args[1] as BufferEncoding, args[2] as () => void);
else if (args.length == 2) result = originalEnd(args[0], args[1] as () => void);
else if (args.length == 1) result = originalEnd(args[0]);
else result = originalEnd();

return result;
};
}

function handleRequest(request: http.IncomingMessage, response: http.ServerResponse) {
if (!(request.method && request.url)) return;

Expand All @@ -169,9 +242,13 @@ function handleRequest(request: http.IncomingMessage, response: http.ServerRespo
normalizeHeaders(request.headers),
url.searchParams,
);

const bodyChunks: Chunk[] = [];
captureResponseBody(response, bodyChunks);

const startTime = getTime();
response.once("finish", () => {
handleResponse(request, requestEvent, startTime, timestamp, response);
handleResponse(request, requestEvent, startTime, timestamp, response, bodyChunks);
});
}

Expand Down Expand Up @@ -238,13 +315,22 @@ function handleResponse(
startTime: number,
timestamp: string | undefined,
response: http.ServerResponse<http.IncomingMessage>,
chunks: Chunk[],
): void {
if (fixupEvent(request, requestEvent)) recording.fixup(requestEvent);

let body =
chunks.length > 0 && typeof chunks[0] === "string"
? chunks.join("")
: Buffer.concat(chunks as Buffer[]).toString("utf8");
body = body.substring(0, maxResponseBodyCaptureLength);

recording.httpResponse(
requestEvent.id,
getTime() - startTime,
response.statusCode,
normalizeHeaders(response.getHeaders()),
body,
);
if (remoteRunning) return;
const { request_method, path_info } = requestEvent.http_server_request;
Expand Down
36 changes: 36 additions & 0 deletions test/__snapshots__/httpServer.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ exports[`mapping Express.js requests 1`] = `
},
"id": 4,
"parent_id": 1,
"return_value": "Hello World!",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -139,6 +140,17 @@ exports[`mapping Express.js requests 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /nonexistent</pre>
</body>
</html>
",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -309,6 +321,7 @@ exports[`mapping Express.js requests 1`] = `
},
"id": 4,
"parent_id": 1,
"return_value": "{"api":"result","param1":"3","param2":"4"}",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -574,6 +587,7 @@ exports[`mapping Express.js requests 1`] = `
},
"id": 4,
"parent_id": 1,
"return_value": "{"key":"value","obj":{"foo":42,"arr":[44]},"arr":[{"foo":43},{"foo":44}],"heterogenous":[42,"str"]}",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -799,6 +813,7 @@ exports[`mapping Express.js requests with remote recording 1`] = `
},
"id": 4,
"parent_id": 1,
"return_value": "Hello World!",
"thread_id": 0,
},
{
Expand Down Expand Up @@ -829,6 +844,17 @@ exports[`mapping Express.js requests with remote recording 1`] = `
},
"id": 6,
"parent_id": 5,
"return_value": "<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Cannot GET /nonexistent</pre>
</body>
</html>
",
"thread_id": 0,
},
{
Expand Down Expand Up @@ -923,6 +949,7 @@ exports[`mapping Express.js requests with remote recording 1`] = `
},
"id": 10,
"parent_id": 7,
"return_value": "{"api":"result","param1":"3","param2":"4"}",
"thread_id": 0,
},
{
Expand Down Expand Up @@ -1073,6 +1100,7 @@ exports[`mapping Express.js requests with remote recording 1`] = `
},
"id": 14,
"parent_id": 11,
"return_value": "{"key":"value","obj":{"foo":42,"arr":[44]},"arr":[{"foo":43},{"foo":44}],"heterogenous":[42,"str"]}",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -1125,6 +1153,7 @@ exports[`mapping node:http requests 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -1173,6 +1202,7 @@ exports[`mapping node:http requests 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -1233,6 +1263,7 @@ exports[`mapping node:http requests 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -1283,6 +1314,7 @@ exports[`mapping node:http requests 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "",
"thread_id": 0,
},
],
Expand Down Expand Up @@ -1335,6 +1367,7 @@ exports[`mapping node:http requests with remote recording 1`] = `
},
"id": 2,
"parent_id": 1,
"return_value": "",
"thread_id": 0,
},
{
Expand All @@ -1359,6 +1392,7 @@ exports[`mapping node:http requests with remote recording 1`] = `
},
"id": 4,
"parent_id": 3,
"return_value": "",
"thread_id": 0,
},
{
Expand Down Expand Up @@ -1395,6 +1429,7 @@ exports[`mapping node:http requests with remote recording 1`] = `
},
"id": 6,
"parent_id": 5,
"return_value": "",
"thread_id": 0,
},
{
Expand All @@ -1421,6 +1456,7 @@ exports[`mapping node:http requests with remote recording 1`] = `
},
"id": 8,
"parent_id": 7,
"return_value": "",
"thread_id": 0,
},
],
Expand Down

0 comments on commit 3a8a0f1

Please sign in to comment.