accept non-HTTP answers from guessed service ports

This commit is contained in:
MacRimi
2026-10-17 20:47:24 +02:00
parent 4bce49eb46
commit e38c085e5f
5 changed files with 138 additions and 12 deletions
+10 -4
View File
@@ -359,13 +359,18 @@ cleanup_canonical_inputs() {
# A derived check accepts any HTTP answer below 500: the application is up,
# even when its first page is a redirect or asks for a login.
healthcheck_probe() {
local code status=0
if [[ ${HC_ANY_STATUS:-false} == true ]]; then
local code
code=$(curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null) || return 1
[[ $code =~ ^[1-4][0-9][0-9]$ ]]
code=$(curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null) || status=$?
(( status == 0 )) && [[ $code =~ ^[1-4][0-9][0-9]$ ]] && return 0
else
curl "${CURL_ARGS[@]}" "$HC_URL" 2>/dev/null
curl "${CURL_ARGS[@]}" "$HC_URL" >/dev/null 2>&1 || status=$?
(( status == 0 )) && return 0
fi
# A port taken from the compose file may serve a database or SSH: an answer
# in another protocol (HTTP/0.9, weird or empty reply, reset) means it is up.
[[ ${HC_ACCEPT_NON_HTTP:-false} == true ]] \
&& (( status == 1 || status == 8 || status == 52 || status == 56 ))
}
cleanup_failed_install() {
@@ -2267,6 +2272,7 @@ if [[ $START_AFTER == 1 && $HAS_STARTUP_HEALTHCHECK == 1 ]]; then
[[ -n $IP ]] || die "$(translate "The healthcheck cannot run without an IP address")"
HC_URL="${HC_SCHEME}://${IP}:${HC_PORT}${HC_PATH}"
HC_ANY_STATUS=$(jq -r '.accept_any_status // false' <<<"$STARTUP_HEALTHCHECK")
HC_ACCEPT_NON_HTTP=$(jq -r '.accept_non_http // false' <<<"$STARTUP_HEALTHCHECK")
if [[ $HC_ANY_STATUS == true ]]; then
CURL_ARGS=(-sS --noproxy '*' -o /dev/null -w '%{http_code}' --max-time "$HC_REQUEST_TIMEOUT")
else
+28 -8
View File
@@ -352,6 +352,22 @@ def profile_keys(deployment):
return keys
GUESSED_ENDPOINT = 'compose-first-tcp-port-fallback'
# curl exit codes for a port that accepted the connection and answered in a
# protocol other than HTTP: HTTP/0.9 refused, weird reply, empty reply, reset.
NON_HTTP_ANSWER = (1, 8, 52, 56)
def probe_http(args):
"""Run one curl probe and return its output and exit code."""
log('$ ' + shlex.join(str(arg) for arg in args))
try:
result = subprocess.run(args, capture_output=True, timeout=60, close_fds=True)
except subprocess.TimeoutExpired:
return b'', -1
return result.stdout, result.returncode
def effective_healthcheck(template):
"""The template's own health check or, when it has none, one derived from
its web address: any HTTP answer below 500 means the application is up."""
@@ -367,9 +383,14 @@ def effective_healthcheck(template):
path = str(endpoint.get('path') or '/')
if (endpoint.get('scheme') in ('http', 'https') and str(endpoint.get('port', '')).isdigit()
and path.startswith('/') and not any(c.isspace() for c in path)):
return {'scheme': endpoint['scheme'], 'port': int(endpoint['port']), 'path': path,
'timeout_seconds': 300, 'request_timeout_seconds': 10, 'stability_seconds': 4,
'verify_tls': False, 'accept_any_status': True}
check = {'scheme': endpoint['scheme'], 'port': int(endpoint['port']), 'path': path,
'timeout_seconds': 300, 'request_timeout_seconds': 10, 'stability_seconds': 4,
'verify_tls': False, 'accept_any_status': True}
# A port taken from the compose file because the recipe names no
# web address may serve a database or SSH: any answer counts.
if endpoint.get('source') == GUESSED_ENDPOINT:
check['accept_non_http'] = True
return check
# Without a web address, the new image must at least keep the container running.
return {'type': 'running', 'timeout_seconds': 90, 'stability_seconds': 20}
@@ -733,11 +754,10 @@ def healthcheck(vmid, template):
args += ['-w', '%{http_code}']
if not check.get('verify_tls', False):
args.append('-k')
try:
answer = run(*args, f'{scheme}://{ip}:{port}{path}')
ok = not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None
except RuntimeError:
pass
answer, status = probe_http([*args, f'{scheme}://{ip}:{port}{path}'])
ok = status == 0 and (not any_status or re.fullmatch(rb'[1-4][0-9][0-9]', answer.strip()) is not None)
if not ok and check.get('accept_non_http') and status in NON_HTTP_ANSWER:
ok = True
if ok:
stable_since = stable_since or time.monotonic()
if time.monotonic() - stable_since >= stability: