Skip to content

Commit 83413ed

Browse files
rentziassCopilot
andcommitted
Log full devtunnel rejection response for triage
Expands the WS upgrade failure diagnostics so we can hand the Dev Tunnels team enough context to root-cause the 403: - All response headers (not just a whitelist) — correlation IDs and rejection reasons live in vendor-specific headers we can't predict. - Full response body up to 8 KiB (was 500 bytes), with a byte count so we can tell when it's truncated. - Target host+path and HTTP version for correlation on the server side. - A body-stream error handler so we don't silently drop the payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 02e7782 commit 83413ed

1 file changed

Lines changed: 53 additions & 37 deletions

File tree

src/debugger/webSocketDapAdapter.ts

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -53,49 +53,65 @@ export class WebSocketDapAdapter implements vscode.DebugAdapter {
5353
// Diagnostic: the tunnel server may reject the WS upgrade with an HTTP
5454
// response (e.g. 401/403). The plain `error` event only surfaces
5555
// "Unexpected server response: 403" — grab the full response here so
56-
// we can log status text and diagnostic headers (which token auth the
57-
// tunnel checked, rejection reason, etc.).
58-
let unexpectedResponse: {status: number; statusMessage: string; headers: string} | undefined;
56+
// we can log status, all headers, and the response body. This is what
57+
// we hand to the Dev Tunnels team when filing a report.
5958
const onUnexpectedResponse = (_req: unknown, res: IncomingMessage) => {
60-
const interestingHeaders = [
61-
"www-authenticate",
62-
"x-tunnel-authorization",
63-
"x-tunnel-service-version",
64-
"x-powered-by",
65-
"server",
66-
"date",
67-
"content-type",
68-
"content-length",
69-
"x-request-id",
70-
"x-correlation-id",
71-
"x-ms-request-id",
72-
"x-ms-error-code"
73-
];
74-
const headerLines = interestingHeaders
75-
.map(h => {
76-
const v = res.headers[h];
77-
return v ? `${h}=${Array.isArray(v) ? v.join(",") : v}` : undefined;
78-
})
79-
.filter(Boolean)
80-
.join(" ");
81-
unexpectedResponse = {
82-
status: res.statusCode ?? 0,
83-
statusMessage: res.statusMessage ?? "",
84-
headers: headerLines
85-
};
59+
// Parse target URL for correlation without exposing credentials.
60+
let target = "";
61+
try {
62+
const u = new URL(this._tunnelUrl);
63+
target = `${u.host}${u.pathname}`;
64+
} catch {
65+
target = "<unparseable>";
66+
}
67+
8668
log(
87-
`[debugger] Tunnel rejected WS upgrade: ${unexpectedResponse.status} ` +
88-
`${unexpectedResponse.statusMessage} | ${headerLines}`
69+
`[debugger] Tunnel rejected WS upgrade: ${res.statusCode ?? 0} ` +
70+
`${res.statusMessage ?? ""} (target=${target}, httpVersion=${res.httpVersion})`
8971
);
90-
// Drain the body so the socket can close cleanly, and capture any
91-
// error text the tunnel included (e.g. a JSON {message: "..."}).
72+
73+
// Log every response header. These are server-emitted on a rejection
74+
// response and don't contain our credentials. Dev Tunnels typically
75+
// includes correlation IDs (x-request-id / x-ms-request-id) and a
76+
// rejection reason (www-authenticate / x-tunnel-*) that pinpoint
77+
// why the upgrade was denied.
78+
const headerEntries = Object.entries(res.headers);
79+
if (headerEntries.length === 0) {
80+
log(`[debugger] Tunnel response headers: <none>`);
81+
} else {
82+
log(`[debugger] Tunnel response headers (${headerEntries.length}):`);
83+
for (const [name, value] of headerEntries) {
84+
const rendered = Array.isArray(value) ? value.join(", ") : String(value ?? "");
85+
log(`[debugger] ${name}: ${rendered}`);
86+
}
87+
}
88+
89+
// Drain the body so the socket can close cleanly, and capture the
90+
// full error text the tunnel included (e.g. a JSON {message: "..."}).
9291
const chunks: Buffer[] = [];
93-
res.on("data", (c: Buffer) => chunks.push(c));
92+
let totalLen = 0;
93+
const BODY_CAP = 8 * 1024; // 8KiB is plenty for a tunnel error payload
94+
res.on("data", (c: Buffer) => {
95+
totalLen += c.length;
96+
if (Buffer.concat(chunks).length < BODY_CAP) {
97+
chunks.push(c);
98+
}
99+
});
94100
res.on("end", () => {
95-
if (chunks.length > 0) {
96-
const body = Buffer.concat(chunks).toString("utf8").slice(0, 500);
97-
log(`[debugger] Tunnel rejection body: ${body}`);
101+
if (totalLen === 0) {
102+
log(`[debugger] Tunnel rejection body: <empty>`);
103+
return;
98104
}
105+
const buf = Buffer.concat(chunks);
106+
const truncated = totalLen > BODY_CAP;
107+
const body = buf.slice(0, BODY_CAP).toString("utf8");
108+
log(
109+
`[debugger] Tunnel rejection body (${totalLen} bytes` +
110+
`${truncated ? `, showing first ${BODY_CAP}` : ""}):\n${body}`
111+
);
112+
});
113+
res.on("error", (err: Error) => {
114+
log(`[debugger] Error draining tunnel rejection body: ${err.message}`);
99115
});
100116
};
101117
ws.on("unexpected-response", onUnexpectedResponse);

0 commit comments

Comments
 (0)