diff --git a/apps/api/src/services/health-check-service.ts b/apps/api/src/services/health-check-service.ts
index 7a6dddf..ec39c16 100644
--- a/apps/api/src/services/health-check-service.ts
+++ b/apps/api/src/services/health-check-service.ts
@@ -1,5 +1,5 @@
import { connect } from "node:net";
-import { Agent } from "undici";
+import { Agent, fetch as undiciFetch } from "undici";
import type { Db } from "@cfdm/db";
import { repos } from "@cfdm/db";
import type { HealthCheckTarget, IpHealthState } from "@cfdm/shared";
@@ -67,14 +67,13 @@ async function httpProbe(
const path = target.path?.trim() || "/";
const pathWithSlash = path.startsWith("/") ? path : `/${path}`;
const port = target.port ?? 80;
- const hostHeader = target.hostname || ip;
const useTls = port === 443;
- // For HTTPS probing against a raw IP, set SNI/Host so TLS handshake and vhost routing work.
- const authorityHost = target.hostname || ip;
- const url = useTls
- ? `https://${authorityHost}${pathWithSlash}`
- : `http://${ip}${pathWithSlash}`;
- // Custom dispatcher for HTTPS-to-IP: connect to the literal IP, but present hostname via SNI.
+ // For HTTPS on 443, probe via the hostname (URL host = hostname) so TLS SNI, Host header
+ // and any edge/vhost protection (e.g. Cloudflare origin 421 on direct-IP) all line up.
+ // For multi-A records this loses strict per-IP HTTPS granularity — use TCP probe for that.
+ // For plain HTTP we still hit the literal IP (per-record target).
+ const urlHost = useTls ? target.hostname || ip : ip;
+ const url = `${useTls ? "https" : "http"}://${urlHost}${pathWithSlash}`;
const dispatcher =
useTls && target.hostname
? new Agent({
@@ -85,12 +84,11 @@ async function httpProbe(
})
: undefined;
try {
- const response = await fetch(url, {
+ const response = await undiciFetch(url, {
method: "GET",
- headers: { Host: hostHeader },
+ headers: { Host: target.hostname || ip },
signal: AbortSignal.timeout(timeoutMs),
redirect: "manual",
- // @ts-expect-error undici dispatcher option is not in fetch types
dispatcher,
});
const latency = Date.now() - started;
diff --git a/apps/web/src/components/form-field.tsx b/apps/web/src/components/form-field.tsx
index 7fb6dc7..061a075 100644
--- a/apps/web/src/components/form-field.tsx
+++ b/apps/web/src/components/form-field.tsx
@@ -63,6 +63,7 @@ interface FormFieldSimpleProps {
label: string
htmlFor: string
error?: { message?: string }
+ hint?: string
className?: string
children: ReactNode
}
@@ -71,6 +72,7 @@ export function FormFieldSimple({
label,
htmlFor,
error,
+ hint,
className,
children,
}: FormFieldSimpleProps) {
@@ -78,6 +80,9 @@ export function FormFieldSimple({
{label}
{children}
+ {hint && !error && (
+ {hint}
+ )}
)
diff --git a/apps/web/src/components/health-check-config-fields.tsx b/apps/web/src/components/health-check-config-fields.tsx
index 5a48d43..4b504ea 100644
--- a/apps/web/src/components/health-check-config-fields.tsx
+++ b/apps/web/src/components/health-check-config-fields.tsx
@@ -141,10 +141,11 @@ export function HealthCheckConfigFields({
patch({ path: e.target.value === '' ? null : e.target.value })