fix gateway probe bad port transport
This commit is contained in:
@@ -57,6 +57,8 @@ NODE
|
||||
- 不要把多层脚本内联穿过 `ssh -> bash -lc -> kubectl exec -> sh -lc -> node/python/powershell`。这种路径会让 heredoc、`$!`、反引号、JSON 字符串、PowerShell 管道和中文内容被不同 shell 多次解释,常见结果是实验命令还没进入 pod 就被截断或改写。
|
||||
- 可靠做法是把实验逻辑沉淀为 repo-owned 脚本,或先写入远端 `/tmp/*.mjs|*.sh|*.ps1`,再用 `kubectl cp`/ConfigMap 挂载到 pod,最后用 `kubectl exec -- node /tmp/script.mjs --arg value` 或等价 argv 形态执行。只允许在外层 shell 保留短命令和固定参数,不在命令行里嵌大段 JS/PowerShell/heredoc。
|
||||
- 需要从 pod 内验证同一份实验时,优先复用同一个脚本在 D601、pod 和 CI 中运行;不要把探测逻辑在每个通道手写一遍。若必须临时写脚本,文件名包含目标和 commit,issue 中记录脚本路径、commit、命令和输出摘要。
|
||||
- pod 内部、D601 host、FRP 公网入口可能使用不同端口语义。`127.0.0.1:6667` 是 HWLAB 内部 cloud-api 常用端口,但 `6667` 属于 WHATWG bad port;Node/undici `fetch` 会在发包前直接报 `bad port`,包括临时 gateway 自己 poll `http://127.0.0.1:6667` 的场景。运行面实验脚本和 gateway 传输层必须用 Node `http/https` 原生 request,或改走公网 `http://74.48.78.17:16667` / service mesh 中不被 bad-port 拦截的入口。
|
||||
- 遇到 `fetch failed`、`bad port`、公网通而 pod loopback 不通时,先判断是不是实验工具栈限制,不要立即归因 cloud-api、FRP、k3s Service 或 gateway 离线。issue 复盘要记录具体 URL、调用库、错误字符串和替代入口。
|
||||
- 不要对已经很大的 hotfix ConfigMap 使用 `kubectl apply -f -`;`apply` 会尝试写入 `kubectl.kubernetes.io/last-applied-configuration`,可能超过 Kubernetes annotation 256 KiB 限制。局部更新用 `patch --type merge --patch-file`。
|
||||
- 多文件运行面覆盖第一次创建 ConfigMap 时也不要默认用 `kubectl apply -f -`;如果 data 可能超过 annotation 限制,先 `kubectl delete configmap <name> --ignore-not-found`,再 `kubectl create configmap <name> --from-file=...`,随后用 Deployment annotation 记录 hotfix 版本并 rollout。正式 CD 收口时必须删除这些 unmanaged ConfigMap/volumeMount,不让热修覆盖继续漂在运行面。
|
||||
- 不要通过会截断 stdout 的控制 CLI 把 `kubectl get configmap -o json` 回读到本地再 `replace`;大 data 可能被日志/截断污染成非法 JSON。只有在确认完整无截断的原生通道中,才允许对完整对象做 `kubectl replace -f -`。
|
||||
|
||||
@@ -60,7 +60,7 @@ npm run gateway:demo:edge-smoke
|
||||
- `gateway:demo:smoke` 启动本地 `hwlab-cloud-api` 和 `hwlab-gateway`,验证 `hardware.invoke.shell` 返回 `stdout=hwlab-demo`。
|
||||
- `gateway:demo:edge-smoke` 额外启动本地 `hwlab-edge-proxy`,验证普通 HTTP proxy 能转发 `/v1/gateway/poll`、`/v1/gateway/result` 和 `/json-rpc`。
|
||||
- 两个 smoke 都会设置 `NO_PROXY/no_proxy`,用于规避本地代理误触发。
|
||||
- 对 D601 pod 内部 `127.0.0.1:6667` 或 service `:6667` 做 Node 探测时,不要用 Web/undici `fetch`;`6667` 属于 WHATWG bad port,Node 会直接报 `bad port`。repo-owned 探测脚本应使用 `http/https` 原生 request,公网 `:16667` 或浏览器入口不受这个限制。
|
||||
- 对 D601 pod 内部 `127.0.0.1:6667` 或 service `:6667` 做 Node 探测时,不要用 Web/undici `fetch`;`6667` 属于 WHATWG bad port,Node 会直接报 `bad port`。gateway 传输层和 repo-owned 探测脚本应使用 `http/https` 原生 request,公网 `:16667` 或浏览器入口不受这个限制。
|
||||
|
||||
## DEV 使用
|
||||
|
||||
|
||||
+42
-22
@@ -1,4 +1,5 @@
|
||||
import { createServer } from "node:http";
|
||||
import { createServer, request as httpRequest } from "node:http";
|
||||
import { request as httpsRequest } from "node:https";
|
||||
|
||||
export class HttpError extends Error {
|
||||
constructor(statusCode, body) {
|
||||
@@ -76,28 +77,47 @@ export function createJsonServer({ routes }) {
|
||||
|
||||
export async function fetchJson(url, options = {}) {
|
||||
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
||||
const response = await fetch(url, {
|
||||
method: options.method ?? (body === undefined ? "GET" : "POST"),
|
||||
headers: {
|
||||
...(body === undefined
|
||||
? {}
|
||||
: {
|
||||
"content-type": "application/json"
|
||||
}),
|
||||
...(options.headers ?? {})
|
||||
},
|
||||
body,
|
||||
signal: options.signal
|
||||
});
|
||||
const text = await response.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: json
|
||||
const target = url instanceof URL ? url : new URL(String(url));
|
||||
const client = target.protocol === "https:" ? httpsRequest : httpRequest;
|
||||
const headers = {
|
||||
...(body === undefined
|
||||
? {}
|
||||
: {
|
||||
"content-type": "application/json",
|
||||
"content-length": Buffer.byteLength(body)
|
||||
}),
|
||||
...(options.headers ?? {})
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = client(target, {
|
||||
method: options.method ?? (body === undefined ? "GET" : "POST"),
|
||||
headers,
|
||||
signal: options.signal
|
||||
}, (response) => {
|
||||
let text = "";
|
||||
response.setEncoding("utf8");
|
||||
response.on("data", (chunk) => {
|
||||
text += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
try {
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
const status = response.statusCode ?? 0;
|
||||
resolve({
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: response.statusMessage ?? "",
|
||||
body: json
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on("error", reject);
|
||||
if (body !== undefined) request.write(body);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export function parsePort(value, fallback) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer } from "node:net";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -183,26 +185,56 @@ async function waitForJson(url) {
|
||||
}
|
||||
|
||||
async function getJson(url) {
|
||||
const response = await fetch(url, {
|
||||
return requestJson(url, {
|
||||
method: "GET",
|
||||
headers: { accept: "application/json" }
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
async function postJson(url, body) {
|
||||
const response = await fetch(url, {
|
||||
return requestJson(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
function requestJson(url, { method, headers = {}, body }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = new URL(url);
|
||||
const client = target.protocol === "https:" ? https : http;
|
||||
const payload = body === undefined ? "" : JSON.stringify(body);
|
||||
const request = client.request(target, {
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
...(payload ? { "content-length": Buffer.byteLength(payload) } : {})
|
||||
}
|
||||
}, (response) => {
|
||||
let text = "";
|
||||
response.setEncoding("utf8");
|
||||
response.on("data", (chunk) => {
|
||||
text += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
if ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${text}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resolve(text ? JSON.parse(text) : null);
|
||||
} catch (error) {
|
||||
reject(new Error(`non-JSON response: ${error.message}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
request.on("error", reject);
|
||||
if (payload) request.write(payload);
|
||||
request.end();
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
return text ? JSON.parse(text) : null;
|
||||
}
|
||||
|
||||
function shellRpc({ id, traceId, command, cwd, timeoutMs }) {
|
||||
|
||||
@@ -72,6 +72,7 @@ const gatewayDemoRegistry = fs.readFileSync(path.resolve(repoRoot, "internal/clo
|
||||
const gatewayMain = fs.readFileSync(path.resolve(repoRoot, "cmd/hwlab-gateway/main.mjs"), "utf8");
|
||||
const gatewayDemoSmoke = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-demo-smoke.mjs"), "utf8");
|
||||
const gatewayNonblockingProbe = fs.readFileSync(path.resolve(repoRoot, "scripts/gateway-outbound-nonblocking-probe.mjs"), "utf8");
|
||||
const simHttp = fs.readFileSync(path.resolve(repoRoot, "internal/sim/http.mjs"), "utf8");
|
||||
const codexStdioSession = fs.readFileSync(path.resolve(repoRoot, "internal/cloud/codex-stdio-session.mjs"), "utf8");
|
||||
const gatewayShellTool = fs.readFileSync(path.resolve(repoRoot, "tools/hwlab-gateway-shell.mjs"), "utf8");
|
||||
const frontendSource = `${html}\n${auth}\n${app}\n${artifactPublisher}`;
|
||||
@@ -1163,7 +1164,13 @@ assert.match(gatewayMain, /function startCloudRequest/);
|
||||
assert.match(gatewayMain, /Promise\.resolve\(\)\s*\n\s*\.then\(\(\) => handleCloudRequest/);
|
||||
assert.doesNotMatch(gatewayMain, /await handleCloudRequest\(response\.body\.request\)/);
|
||||
assert.match(gatewayDemoSmoke, /quick request was blocked by slow request/);
|
||||
assert.match(gatewayDemoSmoke, /function requestJson/);
|
||||
assert.doesNotMatch(functionBody(gatewayDemoSmoke, "getJson"), /\bfetch\(/);
|
||||
assert.doesNotMatch(functionBody(gatewayDemoSmoke, "postJson"), /\bfetch\(/);
|
||||
assert.match(gatewayNonblockingProbe, /quick request was blocked by slow request/);
|
||||
assert.match(simHttp, /request as httpRequest/);
|
||||
assert.match(functionBody(simHttp, "fetchJson"), /httpRequest/);
|
||||
assert.doesNotMatch(functionBody(simHttp, "fetchJson"), /\bfetch\(/);
|
||||
assert.match(cloudApiServer, /HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS,\s*120000/);
|
||||
assert.match(codexStdioSession, /detached:\s*childDetached/);
|
||||
assert.match(codexStdioSession, /function terminateChildProcess/);
|
||||
|
||||
Reference in New Issue
Block a user