Skip to content

Commit

Permalink
Fix FormData handling in fetch instrumentation (#139)
Browse files Browse the repository at this point in the history
* Fix FormData handling in fetch instrumentation

* fixed types

* changeset
  • Loading branch information
dvoytenko authored Jan 24, 2025
1 parent 2f43edb commit 4019976
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 94 deletions.
8 changes: 7 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["e2e", "sample", "bridge-emulator", "collector"],
"ignore": [
"e2e",
"sample",
"bridge-emulator",
"collector",
"multiple-exporters"
],
"snapshot": {
"useCalculatedVersion": true
}
Expand Down
5 changes: 5 additions & 0 deletions .changeset/twelve-items-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vercel/otel": patch
---

Fix FormData in fetch instrumentation
13 changes: 12 additions & 1 deletion apps/sample/app/api/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@ export function runService(request: Request): Promise<string> {
return "<no data>";
}

let body: BodyInit | null = null;
const isFormData = url.searchParams.get("mode") === "formdata";
if (isFormData) {
const fd = new FormData();
fd.set("cmd", "echo");
fd.set("data.foo", "bar");
body = fd;
} else {
body = JSON.stringify({ cmd: "echo", data: { foo: "bar" } });
}

const response = await fetch(dataUrl, {
method: "POST",
body: JSON.stringify({ cmd: "echo", data: { foo: "bar" } }),
body,
headers: { "X-Cmd": "echo" },
cache: "no-store",
});
Expand Down
6 changes: 4 additions & 2 deletions packages/bridge-emulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,19 @@
"author": "",
"license": "ISC",
"dependencies": {
"formidable": "3.5.2",
"next": "14.2.1-canary.7"
},
"peerDependencies": {
"@opentelemetry/api": "^1.7.0",
"@vercel/otel": "workspace:^"
},
"devDependencies": {
"@types/formidable": "3.4.5",
"@types/node": "^20",
"@vercel/otel": "workspace:^",
"eslint-config": "workspace:*",
"typescript-config": "workspace:*",
"typescript": "^5"
"typescript": "^5",
"typescript-config": "workspace:*"
}
}
52 changes: 39 additions & 13 deletions packages/bridge-emulator/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Server, type IncomingMessage, type ServerResponse } from "node:http";
import formidable from "formidable";

export interface Bridge {
port: number;
Expand Down Expand Up @@ -59,26 +60,51 @@ class BridgeEmulatorServer implements Bridge {
req: IncomingMessage,
res: ServerResponse
): Promise<void> => {
const body = await new Promise<Buffer>((resolve, reject) => {
const acc: Buffer[] = [];
req.on("data", (chunk: Buffer) => {
acc.push(chunk);
let json: BridgeEmulatorRequest;
if ((req.headers["content-type"] ?? "").includes("multipart/form-data")) {
json = await new Promise<BridgeEmulatorRequest>((resolve, reject) => {
const inst = formidable({});
inst.parse(req, (err, fields, _files) => {
if (err) {
reject(err);
return;
}
resolve({
cmd: Array.isArray(fields.cmd) ? fields.cmd[0] : fields.cmd,
data: Object.fromEntries(
Object.entries(fields)
.filter(([key]) => key.startsWith("data."))
.map(([key, value]) => [
key.slice(5),
Array.isArray(value) ? value[0] : value,
])
),
} as unknown as BridgeEmulatorRequest);
});
});
req.on("end", () => {
resolve(Buffer.concat(acc));
} else {
const body = await new Promise<Buffer>((resolve, reject) => {
const acc: Buffer[] = [];
req.on("data", (chunk: Buffer) => {
acc.push(chunk);
});
req.on("end", () => {
resolve(Buffer.concat(acc));
});
req.on("error", reject);
});
req.on("error", reject);
});

const json = JSON.parse(
body.toString("utf-8") || "{}"
) as BridgeEmulatorRequest;
json = JSON.parse(
body.toString("utf-8") || "{}"
) as BridgeEmulatorRequest;
}

if (json.cmd === "ack") {
const waiting = this.waitingAck.get(json.testId);
if (waiting) {
waiting.finally(() => {
this.waitingAck.delete(json.testId);
if ("testId" in json) {
this.waitingAck.delete(json.testId);
}
res.writeHead(200, "OK", { "X-Server": "bridge" });
res.write("{}");
res.end();
Expand Down
5 changes: 5 additions & 0 deletions packages/otel/src/instrumentations/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ export class FetchInstrumentation implements Instrumentation {

try {
const startTime = Date.now();
// Remove "content-type" for a FormData body because undici regenerates
// a new multipart separator each time.
if (init?.body && init.body instanceof FormData) {
req.headers.delete("content-type");
}
const res = await originalFetch(input, {
...init,
headers: req.headers,
Expand Down
115 changes: 38 additions & 77 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4019976

Please sign in to comment.