import fs from "node:fs"; import dns from "node:dns/promises"; import { pathToFileURL } from "node:url"; export function redact(value, secrets = []) { let text = String(value); for (const secret of secrets.filter(Boolean)) { for (const variant of new Set([secret, encodeURIComponent(secret)])) text = text.split(variant).join("[REDACTED]"); } return text .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/gi, "$1[REDACTED]@") .replace( /((?:authorization|x-api-key|cookie|set-cookie)\s*:\s*)[^\r\n]*/gi, "$1[REDACTED]", ) .replace(/\b(Bearer|Basic)\s+[^\s"<>]+/gi, "$1 [REDACTED]") .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ""); } export function responseDetails(headers, body) { const lines = headers .split(/\r?\n/) .filter((line) => /^(HTTP\/|content-type:|content-length:|server:|location:|cf-ray:|x-request-id:|retry-after:)/i.test( line, ), ); if (!body) return [...lines, "Response body: empty"]; try { const data = JSON.parse(body); // Deployment lists may contain source/configuration. Never dump the payload. lines.push( `Response JSON shape: ${Array.isArray(data) ? `array (${data.length} items)` : data && typeof data === "object" ? Object.keys(data).slice(0, 20).join(", ") : typeof data}`, ); for (const field of ["message", "error", "code"]) { const value = data?.[field]; if (typeof value === "string" || typeof value === "number") lines.push(`${field}: ${String(value).slice(0, 2000)}`); else if (value && typeof value === "object") { for (const key of ["message", "code"]) if (typeof value[key] === "string" || typeof value[key] === "number") lines.push(`${field}.${key}: ${String(value[key]).slice(0, 2000)}`); } } } catch { const title = body.match(/]*>([^<]{0,300})<\/title>/i)?.[1]; lines.push( title ? `Response HTML title: ${title}` : `Response is not JSON (${Buffer.byteLength(body)} bytes); raw content withheld.`, ); } return lines; } async function main([mode, ...args]) { if (mode === "environment") { const url = new URL(args[0]); console.error( `[Dokploy] Target: ${url.origin}; Node ${process.version}; runner ${process.platform}/${process.arch}`, ); for (const name of [ "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", ]) { if (!process.env[name]) continue; try { const proxy = new URL(process.env[name]); console.error( `[Dokploy] ${name}: ${proxy.protocol}//${proxy.host} (credentials omitted)`, ); } catch { console.error(`[Dokploy] ${name}: configured (value withheld)`); } } console.error( `[Dokploy] NO_PROXY/no_proxy: ${process.env.NO_PROXY || process.env.no_proxy ? "configured" : "unset"}`, ); for (const name of ["CURL_CA_BUNDLE", "SSL_CERT_FILE", "SSL_CERT_DIR"]) console.error( `[Dokploy] ${name}: ${process.env[name] ? `configured; path exists=${fs.existsSync(process.env[name])}` : "default"}`, ); let timer; try { const addresses = await Promise.race([ dns.lookup(url.hostname, { all: true }), new Promise((_, reject) => { timer = setTimeout( () => reject(new Error("DNS lookup exceeded 5 seconds")), 5000, ); }), ]); console.error( `[Dokploy] DNS ${url.hostname}: ${addresses.map((a) => `IPv${a.family} ${a.address}`).join(", ") || "no addresses"}`, ); } catch (error) { console.error( `[Dokploy] DNS lookup failed: ${error.code ?? ""} ${error.message}`, ); } finally { clearTimeout(timer); } return; } if (mode !== "request") throw new Error("Expected environment or request mode"); const [ label, exitCode, metricsPath, errorPath, headersPath, bodyPath, authPath, ] = args; const read = (file) => fs.existsSync(file) ? fs.readFileSync(file, "utf8") : ""; const key = read(authPath) .replace(/^x-api-key:\s*/i, "") .trim(); const safe = (text) => redact(text, [key, process.env.DOKPLOY_APPLICATION_ID]); const metrics = read(metricsPath).trim(); const status = metrics.split(/\r?\n/)[0] || "000"; console.error( safe( `[Dokploy] ${label}: curl exit=${exitCode}\n${metrics || "No transfer metrics returned"}`, ), ); const stderr = read(errorPath).trim(); if (stderr) console.error( safe(`[Dokploy] curl diagnostics:\n${stderr.slice(0, 8000)}`), ); const hints = { 5: "Proxy DNS failed. Check proxy configuration in the runner container.", 6: "Target DNS failed. Check the domain records and runner container DNS.", 7: "Connection failed. Check port 443, reverse proxy, firewall and runner routing.", 28: "Request timed out. Transfer timings show whether DNS, connection or the server stalled.", 35: "TLS handshake failed. Check the reverse proxy HTTPS listener and TLS configuration.", 52: "The server closed the connection without an HTTP response.", 60: "TLS certificate verification failed. Check the domain certificate, intermediate chain and runner CA bundle.", 97: "Proxy handshake failed. Check the runner proxy configuration.", }; if (hints[exitCode]) console.error(`[Dokploy] ${hints[exitCode]}`); if (exitCode !== "0" || !/^2\d\d$/.test(status)) { for (const line of responseDetails(read(headersPath), read(bodyPath))) console.error(safe(`[Dokploy] ${line}`)); if (status === "000") console.error( "[Dokploy] HTTP 000 means no HTTP response was received; inspect the curl error and network timings above.", ); if (/^3\d\d$/.test(status)) console.error( "[Dokploy] Unexpected redirect. Check the Location header and proxy/auth routing; authenticated requests do not follow redirects.", ); if (["401", "403"].includes(status)) console.error( "[Dokploy] Access rejected by Dokploy or a proxy. Check API key permissions and proxy access rules.", ); } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(process.argv.slice(2)) .then(() => process.exit(0)) .catch((error) => { console.error(`[Dokploy] Diagnostics failed: ${error.message}`); process.exit(1); });