diff --git a/backend/routers/backend.py b/backend/routers/backend.py
index 0b51ceb..a7256b1 100644
--- a/backend/routers/backend.py
+++ b/backend/routers/backend.py
@@ -13,6 +13,33 @@ from services.haproxy_config import generate_haproxy_config_for_cluster
router = APIRouter(prefix="/api/backends", tags=["backends", "servers"])
logger = logging.getLogger(__name__)
+def filter_httpchk_from_options(options: Optional[str]) -> Optional[str]:
+ """
+ Filter out 'option httpchk' directives from options field.
+ These should be configured via the health_check_uri field instead to avoid duplication.
+
+ Args:
+ options: Multi-line string containing HAProxy option directives
+
+ Returns:
+ Filtered options string without 'option httpchk' lines, or None if empty
+ """
+ if not options:
+ return options
+
+ # Split by newline, filter out httpchk lines, rejoin
+ filtered_lines = [
+ line for line in options.split('\n')
+ if line.strip() and 'httpchk' not in line.lower()
+ ]
+
+ # Return None if no lines remain after filtering
+ if not filtered_lines:
+ return None
+
+ return '\n'.join(filtered_lines)
+
+
async def validate_user_cluster_access(user_id: int, cluster_id: int, conn):
"""Validate that user has access to the specified cluster"""
# Check if cluster exists
@@ -308,6 +335,17 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
"balance_method": backend["balance_method"],
"mode": backend["mode"],
"health_check_uri": backend.get("health_check_uri"),
+ "health_check_interval": backend.get("health_check_interval"),
+ "health_check_expected_status": backend.get("health_check_expected_status"),
+ "fullconn": backend.get("fullconn"),
+ "cookie_name": backend.get("cookie_name"),
+ "cookie_options": backend.get("cookie_options"),
+ "default_server_inter": backend.get("default_server_inter"),
+ "default_server_fall": backend.get("default_server_fall"),
+ "default_server_rise": backend.get("default_server_rise"),
+ "request_headers": backend.get("request_headers"),
+ "response_headers": backend.get("response_headers"),
+ "options": backend.get("options"),
"timeout_connect": backend.get("timeout_connect"),
"timeout_server": backend.get("timeout_server"),
"timeout_queue": backend.get("timeout_queue"),
@@ -441,6 +479,11 @@ async def create_backend(backend: BackendConfig, authorization: str = Header(Non
await close_database_connection(conn)
raise HTTPException(status_code=400, detail=f"Backend '{backend.name}' already exists")
+ # Filter out 'option httpchk' from options field (should use health_check_uri instead)
+ filtered_options = filter_httpchk_from_options(backend.options)
+ if filtered_options != backend.options and backend.options:
+ logger.info(f"Backend '{backend.name}': Filtered 'option httpchk' from options field. Use Health Check URI field instead.")
+
# Insert new backend
backend_id = await conn.fetchval("""
INSERT INTO backends (name, balance_method, mode, health_check_uri, health_check_interval,
@@ -453,7 +496,7 @@ async def create_backend(backend: BackendConfig, authorization: str = Header(Non
backend.health_check_uri, backend.health_check_interval,
backend.health_check_expected_status, backend.fullconn, backend.cookie_name, backend.cookie_options,
backend.default_server_inter, backend.default_server_fall, backend.default_server_rise,
- backend.request_headers, backend.response_headers, backend.options,
+ backend.request_headers, backend.response_headers, filtered_options,
backend.timeout_connect, backend.timeout_server, backend.timeout_queue, backend.cluster_id)
# If cluster_id provided, create new config version for agents
@@ -741,6 +784,13 @@ async def update_backend(backend_id: int, backend_update: BackendConfigUpdate, r
old_backend_name = existing_backend['name']
new_backend_name = update_data.get('name', old_backend_name)
backend_name_changed = old_backend_name != new_backend_name
+
+ # Filter out 'option httpchk' from options field if present (should use health_check_uri instead)
+ if 'options' in update_data and update_data['options']:
+ filtered_options = filter_httpchk_from_options(update_data['options'])
+ if filtered_options != update_data['options']:
+ logger.info(f"Backend '{old_backend_name}': Filtered 'option httpchk' from options field. Use Health Check URI field instead.")
+ update_data['options'] = filtered_options
for field, value in update_data.items():
if field in ['name', 'balance_method', 'mode', 'health_check_uri', 'health_check_interval',
diff --git a/backend/routers/frontend.py b/backend/routers/frontend.py
index 12dcb0a..3d47e05 100644
--- a/backend/routers/frontend.py
+++ b/backend/routers/frontend.py
@@ -13,6 +13,33 @@ from services.haproxy_config import generate_haproxy_config_for_cluster
router = APIRouter(prefix="/api/frontends", tags=["frontends"])
logger = logging.getLogger(__name__)
+def filter_httpchk_from_options(options: Optional[str]) -> Optional[str]:
+ """
+ Filter out 'option httpchk' directives from options field.
+ These are not applicable to frontends (health checks are for backends).
+
+ Args:
+ options: Multi-line string containing HAProxy option directives
+
+ Returns:
+ Filtered options string without 'option httpchk' lines, or None if empty
+ """
+ if not options:
+ return options
+
+ # Split by newline, filter out httpchk lines, rejoin
+ filtered_lines = [
+ line for line in options.split('\n')
+ if line.strip() and 'httpchk' not in line.lower()
+ ]
+
+ # Return None if no lines remain after filtering
+ if not filtered_lines:
+ return None
+
+ return '\n'.join(filtered_lines)
+
+
async def validate_user_cluster_access(user_id: int, cluster_id: int, conn):
"""Validate that user has access to the specified cluster"""
# Check if cluster exists
@@ -376,6 +403,11 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat
# Convert ssl_certificate_ids to JSONB for database
ssl_cert_ids_json = json.dumps(frontend.ssl_certificate_ids) if frontend.ssl_certificate_ids else '[]'
+ # Filter out 'option httpchk' from options field (not applicable to frontends)
+ filtered_options = filter_httpchk_from_options(frontend.options)
+ if filtered_options != frontend.options and frontend.options:
+ logger.info(f"Frontend '{frontend.name}': Filtered 'option httpchk' from options field. Health checks are for backends.")
+
# Insert new frontend with all form fields
frontend_id = await conn.fetchval("""
INSERT INTO frontends (
@@ -391,7 +423,7 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat
frontend.default_backend, frontend.mode, frontend.ssl_enabled,
frontend.ssl_certificate_id, ssl_cert_ids_json, frontend.ssl_port, frontend.ssl_cert_path, frontend.ssl_cert, frontend.ssl_verify,
json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []),
- frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
+ frontend.request_headers, frontend.response_headers, filtered_options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri,
frontend.cluster_id, frontend.maxconn)
@@ -623,6 +655,11 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R
# ENTERPRISE DUAL-MODE: Save ssl_certificate_ids (NEW) and ssl_certificate_id (OLD - backward compat)
ssl_cert_ids_json = json.dumps(frontend.ssl_certificate_ids) if frontend.ssl_certificate_ids else '[]'
+ # Filter out 'option httpchk' from options field (not applicable to frontends)
+ filtered_options = filter_httpchk_from_options(frontend.options)
+ if filtered_options != frontend.options and frontend.options:
+ logger.info(f"Frontend '{frontend.name}': Filtered 'option httpchk' from options field. Health checks are for backends.")
+
# Update frontend with all form fields (using preserved SSL values if needed)
await conn.execute("""
UPDATE frontends SET
@@ -638,7 +675,7 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R
frontend.default_backend, frontend.mode, ssl_enabled,
ssl_certificate_id, ssl_cert_ids_json, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
json.dumps(frontend.acl_rules or []), json.dumps(frontend.redirect_rules or []), json.dumps(frontend.use_backend_rules or []),
- frontend.request_headers, frontend.response_headers, frontend.options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
+ frontend.request_headers, frontend.response_headers, filtered_options, frontend.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
frontend.rate_limit, frontend.compression, frontend.log_separate, frontend.monitor_uri,
frontend.cluster_id, frontend.maxconn, frontend_id)
diff --git a/backend/utils/haproxy_config_parser.py b/backend/utils/haproxy_config_parser.py
index 5ae42b5..8bda456 100644
--- a/backend/utils/haproxy_config_parser.py
+++ b/backend/utils/haproxy_config_parser.py
@@ -310,7 +310,8 @@ class HAProxyConfigParser:
frontend.maxconn = int(maxconn_match.group(1))
# Parse options (httplog, forwardfor, etc.) - NEW for frontend support
- if line.startswith('option '):
+ # Note: option httpchk is NOT applicable to frontends (health checks are for backends)
+ if line.startswith('option ') and 'httpchk' not in line:
# Validate option directive - check for known HAProxy options
option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE)
if option_match:
@@ -318,7 +319,7 @@ class HAProxyConfigParser:
# List of valid HAProxy frontend options
valid_options = [
'httplog', 'tcplog', 'dontlognull', 'http-keep-alive', 'http-server-close',
- 'forwardfor', 'redispatch', 'http-use-proxy-header', 'httpchk', 'tcp-check',
+ 'forwardfor', 'redispatch', 'http-use-proxy-header', 'tcp-check',
'contstats', 'http-pretend-keepalive', 'logasap', 'nolinger', 'persist',
'prefer-last-server', 'splice-auto', 'splice-request', 'splice-response',
'transparent', 'abortonclose', 'allbackups', 'checkcache', 'clitcpka',
@@ -337,6 +338,13 @@ class HAProxyConfigParser:
# Collect option for frontend.options field
options_list.append(line.strip())
continue # Skip options - don't add to request_headers
+ elif line.startswith('option ') and 'httpchk' in line:
+ # Warn user that httpchk is not applicable to frontends
+ self.warnings.append(
+ f"Frontend '{name}': 'option httpchk' is not applicable to frontends and will be skipped. "
+ "Health checks are configured in backend definitions."
+ )
+ continue
# Parse HTTP request headers
if line.startswith('http-request '):
diff --git a/frontend/src/components/BackendServers.js b/frontend/src/components/BackendServers.js
index 2c2a188..c03c136 100644
--- a/frontend/src/components/BackendServers.js
+++ b/frontend/src/components/BackendServers.js
@@ -484,9 +484,11 @@ const BackendServers = () => {
const handleEditBackend = (backend) => {
setEditingBackend(backend);
- // Debug: Check if options field exists in backend object
+ // Debug: Check if multi-line text fields exist in backend object
console.log('🔍 BACKEND EDIT DEBUG: Raw backend data:', backend);
console.log('🔍 BACKEND EDIT DEBUG: Options field:', backend.options);
+ console.log('🔍 BACKEND EDIT DEBUG: Request headers:', backend.request_headers);
+ console.log('🔍 BACKEND EDIT DEBUG: Response headers:', backend.response_headers);
backendForm.setFieldsValue({
...backend,
@@ -494,8 +496,10 @@ const BackendServers = () => {
timeout_connect: backend.timeout_connect || 10000,
timeout_server: backend.timeout_server || 60000,
timeout_queue: backend.timeout_queue || 60000,
- // Explicitly set options to handle null/undefined cases
- options: backend.options || ''
+ // Explicitly set multi-line text fields to handle null/undefined cases
+ options: backend.options || '',
+ request_headers: backend.request_headers || '',
+ response_headers: backend.response_headers || ''
});
setBackendModalVisible(true);
};
@@ -1792,10 +1796,22 @@ const BackendServers = () => {
name="options"
label="Backend Options"
tooltip="HAProxy backend options (one per line). Examples: option http-keep-alive, option forwardfor, option redispatch"
+ help={
+ backendForm.getFieldValue('options') &&
+ backendForm.getFieldValue('options').toLowerCase().includes('httpchk') ? (
+
+ ⚠️ Do not use "option httpchk" here. Use the "Health Check URI" field above instead to avoid duplication.
+
+ ) : null
+ }
>
{
+ // Force re-render to show/hide warning
+ backendForm.setFieldsValue({ options: e.target.value });
+ }}
/>
diff --git a/frontend/src/components/FrontendManagement.js b/frontend/src/components/FrontendManagement.js
index 7c4eab2..9149674 100644
--- a/frontend/src/components/FrontendManagement.js
+++ b/frontend/src/components/FrontendManagement.js
@@ -583,7 +583,9 @@ const FrontendManagement = () => {
ssl_certificate_ids: sslCertIds,
acl_rules: formattedAclRules,
redirect_rules: formattedRedirectRules,
- use_backend_rules: formattedUseBackendRules
+ use_backend_rules: formattedUseBackendRules,
+ // Explicitly set options field to handle null/undefined case (NEW field)
+ options: frontend.options || ''
});
// Update SSL field visibility after setting values
@@ -1649,6 +1651,14 @@ http-response del-header Server`}
label="Frontend Options"
extra="HAProxy frontend options (one per line)"
tooltip="Examples: option httplog, option forwardfor, option dontlognull, option http-keep-alive"
+ help={
+ form.getFieldValue('options') &&
+ form.getFieldValue('options').toLowerCase().includes('httpchk') ? (
+
+ ⚠️ "option httpchk" is not applicable to frontends. Health checks are configured in backend definitions.
+
+ ) : null
+ }
>