diff --git a/backend/database/migrations.py b/backend/database/migrations.py
index c0c61b1..98c7753 100644
--- a/backend/database/migrations.py
+++ b/backend/database/migrations.py
@@ -1555,6 +1555,8 @@ async def run_all_migrations():
await add_cluster_id_to_agent_config_requests()
await fix_users_unique_constraints_for_soft_delete()
await add_ssl_certificate_id_to_backend_servers()
+ await add_options_to_backends()
+ await add_options_to_frontends()
logger.info("Database migrations completed successfully.")
@@ -2754,3 +2756,71 @@ async def fix_users_unique_constraints_for_soft_delete():
finally:
if conn:
await close_database_connection(conn)
+
+async def add_options_to_backends():
+ """Add options column to backends table for HAProxy backend options (option http-keep-alive, etc.)"""
+ conn = None
+ try:
+ conn = await get_database_connection()
+
+ # Check if options column already exists
+ column_exists = await conn.fetchval("""
+ SELECT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'backends'
+ AND column_name = 'options'
+ )
+ """)
+
+ if not column_exists:
+ # Add options column as TEXT (multi-line HAProxy options)
+ await conn.execute("""
+ ALTER TABLE backends
+ ADD COLUMN options TEXT
+ """)
+
+ logger.info("✅ Added options column to backends table for HAProxy backend options")
+ else:
+ logger.info("ℹ️ options column already exists in backends table")
+
+ await close_database_connection(conn)
+
+ except Exception as e:
+ if conn:
+ await close_database_connection(conn)
+ logger.error(f"❌ Error adding options to backends: {e}")
+ # Don't raise - we'll try to proceed
+
+async def add_options_to_frontends():
+ """Add options column to frontends table for HAProxy frontend options (option httplog, option forwardfor, etc.)"""
+ conn = None
+ try:
+ conn = await get_database_connection()
+
+ # Check if options column already exists
+ column_exists = await conn.fetchval("""
+ SELECT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'frontends'
+ AND column_name = 'options'
+ )
+ """)
+
+ if not column_exists:
+ # Add options column as TEXT (multi-line HAProxy options)
+ await conn.execute("""
+ ALTER TABLE frontends
+ ADD COLUMN options TEXT
+ """)
+
+ logger.info("✅ Added options column to frontends table for HAProxy frontend options")
+ else:
+ logger.info("ℹ️ options column already exists in frontends table")
+
+ await close_database_connection(conn)
+
+ except Exception as e:
+ if conn:
+ await close_database_connection(conn)
+ logger.error(f"❌ Error adding options to frontends: {e}")
+ # Don't raise - we'll try to proceed
diff --git a/backend/models/backend.py b/backend/models/backend.py
index 015aa32..b405941 100644
--- a/backend/models/backend.py
+++ b/backend/models/backend.py
@@ -38,6 +38,7 @@ class BackendConfig(BaseModel):
timeout_connect: Optional[int] = 10000
timeout_server: Optional[int] = 60000
timeout_queue: Optional[int] = 60000
+ options: Optional[str] = None
servers: List[ServerConfig] = []
@validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn')
@@ -70,6 +71,7 @@ class BackendConfigUpdate(BaseModel):
timeout_connect: Optional[int] = None
timeout_server: Optional[int] = None
timeout_queue: Optional[int] = None
+ options: Optional[str] = None
servers: Optional[List[ServerConfig]] = None
@validator('health_check_interval', 'timeout_connect', 'timeout_server', 'timeout_queue', 'fullconn')
diff --git a/backend/models/frontend.py b/backend/models/frontend.py
index de006cb..e7e84ba 100644
--- a/backend/models/frontend.py
+++ b/backend/models/frontend.py
@@ -23,6 +23,7 @@ class FrontendConfig(BaseModel):
use_backend_rules: Any = [] # Changed from Optional[str] to Any for list support
request_headers: Optional[str] = None
response_headers: Optional[str] = None
+ options: Optional[str] = None
tcp_request_rules: Optional[str] = None
timeout_client: Optional[int] = None
timeout_http_request: Optional[int] = None
diff --git a/backend/routers/backend.py b/backend/routers/backend.py
index 85f53e9..0b51ceb 100644
--- a/backend/routers/backend.py
+++ b/backend/routers/backend.py
@@ -184,7 +184,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, balance_method, mode, health_check_uri,
health_check_interval, health_check_expected_status, fullconn,
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
- request_headers, response_headers,
+ request_headers, response_headers, options,
is_active, created_at, updated_at, cluster_id, last_config_status,
timeout_connect, timeout_server, timeout_queue
FROM backends WHERE cluster_id = $1 ORDER BY name
@@ -194,7 +194,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, balance_method, mode, health_check_uri,
health_check_interval, health_check_expected_status, fullconn,
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
- request_headers, response_headers,
+ request_headers, response_headers, options,
is_active, created_at, updated_at, cluster_id, last_config_status,
timeout_connect, timeout_server, timeout_queue
FROM backends WHERE cluster_id = $1 AND is_active = TRUE ORDER BY name
@@ -205,7 +205,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, balance_method, mode, health_check_uri,
health_check_interval, health_check_expected_status, fullconn,
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
- request_headers, response_headers,
+ request_headers, response_headers, options,
is_active, created_at, updated_at, cluster_id, last_config_status,
timeout_connect, timeout_server, timeout_queue
FROM backends ORDER BY name
@@ -215,7 +215,7 @@ async def get_backends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, balance_method, mode, health_check_uri,
health_check_interval, health_check_expected_status, fullconn,
cookie_name, cookie_options, default_server_inter, default_server_fall, default_server_rise,
- request_headers, response_headers,
+ request_headers, response_headers, options,
is_active, created_at, updated_at, cluster_id, last_config_status,
timeout_connect, timeout_server, timeout_queue
FROM backends WHERE is_active = TRUE ORDER BY name
@@ -446,14 +446,14 @@ async def create_backend(backend: BackendConfig, authorization: str = Header(Non
INSERT INTO backends (name, balance_method, mode, health_check_uri, health_check_interval,
health_check_expected_status, fullconn, cookie_name, cookie_options,
default_server_inter, default_server_fall, default_server_rise,
- request_headers, response_headers,
+ request_headers, response_headers, options,
timeout_connect, timeout_server, timeout_queue, cluster_id)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) RETURNING id
""", backend.name, backend.balance_method, backend.mode,
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.request_headers, backend.response_headers, backend.options,
backend.timeout_connect, backend.timeout_server, backend.timeout_queue, backend.cluster_id)
# If cluster_id provided, create new config version for agents
@@ -746,7 +746,7 @@ async def update_backend(backend_id: int, backend_update: BackendConfigUpdate, r
if field in ['name', 'balance_method', 'mode', 'health_check_uri', 'health_check_interval',
'health_check_expected_status', 'fullconn', 'cookie_name', 'cookie_options',
'default_server_inter', 'default_server_fall', 'default_server_rise',
- 'request_headers', 'response_headers', 'timeout_connect', 'timeout_server', 'timeout_queue']:
+ 'request_headers', 'response_headers', 'options', 'timeout_connect', 'timeout_server', 'timeout_queue']:
update_fields.append(f"{field} = ${param_idx}")
update_values.append(value)
param_idx += 1
diff --git a/backend/routers/config.py b/backend/routers/config.py
index 499e294..2aba57a 100644
--- a/backend/routers/config.py
+++ b/backend/routers/config.py
@@ -804,6 +804,7 @@ async def parse_bulk_config(
"maxconn": frontend.maxconn,
"request_headers": frontend.request_headers,
"response_headers": frontend.response_headers,
+ "options": frontend.options,
"tcp_request_rules": frontend.tcp_request_rules,
"acl_rules": frontend.acl_rules,
"use_backend_rules": frontend.use_backend_rules
@@ -890,6 +891,7 @@ async def parse_bulk_config(
"default_server_rise": backend.default_server_rise,
"request_headers": backend.request_headers,
"response_headers": backend.response_headers,
+ "options": backend.options,
"timeout_connect": backend.timeout_connect,
"timeout_server": backend.timeout_server,
"timeout_queue": backend.timeout_queue,
@@ -1280,6 +1282,12 @@ async def bulk_create_entities(
update_values.append(backend_data["response_headers"])
param_index += 1
+ # NEW: Options field support (option http-keep-alive, etc.)
+ if backend_data.get("options") and backend_data["options"] != existing_full.get("options"):
+ update_fields.append(f"options = ${param_index}")
+ update_values.append(backend_data["options"])
+ param_index += 1
+
# NOTE: maxconn field exists in database but is not used in normal backend UPDATE
# Preserving consistency with existing backend UPDATE endpoint (backend.py)
# maxconn field intentionally excluded from bulk import UPDATE
@@ -1331,9 +1339,9 @@ async def bulk_create_entities(
health_check_interval, health_check_expected_status, fullconn,
cookie_name, cookie_options, default_server_inter,
default_server_fall, default_server_rise, request_headers,
- response_headers, timeout_connect, timeout_server, timeout_queue, cluster_id
+ response_headers, options, timeout_connect, timeout_server, timeout_queue, cluster_id
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
RETURNING id
""",
backend_data["name"],
@@ -1350,6 +1358,7 @@ async def bulk_create_entities(
backend_data.get("default_server_rise"),
backend_data.get("request_headers"),
backend_data.get("response_headers"),
+ backend_data.get("options"),
backend_data.get("timeout_connect", 10000),
backend_data.get("timeout_server", 60000),
backend_data.get("timeout_queue", 60000),
@@ -1494,6 +1503,12 @@ async def bulk_create_entities(
update_values.append(frontend_data["tcp_request_rules"])
param_index += 1
+ # NEW: Options field support (option httplog, option forwardfor, etc.)
+ if frontend_data.get("options") and frontend_data["options"] != existing_full.get("options"):
+ update_fields.append(f"options = ${param_index}")
+ update_values.append(frontend_data["options"])
+ param_index += 1
+
if frontend_data.get("timeout_http_request") and frontend_data["timeout_http_request"] != existing_full["timeout_http_request"]:
update_fields.append(f"timeout_http_request = ${param_index}")
update_values.append(frontend_data["timeout_http_request"])
@@ -1561,11 +1576,11 @@ async def bulk_create_entities(
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port,
ssl_cert_path, ssl_cert, ssl_verify,
timeout_client, timeout_http_request, maxconn,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, tcp_request_rules, options,
rate_limit, compression, log_separate, monitor_uri,
cluster_id, acl_rules, use_backend_rules, redirect_rules, updated_at
)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP)
RETURNING id
""",
frontend_data["name"],
@@ -1586,6 +1601,7 @@ async def bulk_create_entities(
frontend_data.get("request_headers"),
frontend_data.get("response_headers"),
frontend_data.get("tcp_request_rules"),
+ frontend_data.get("options"),
frontend_data.get("rate_limit"),
frontend_data.get("compression", False),
frontend_data.get("log_separate", False),
diff --git a/backend/routers/frontend.py b/backend/routers/frontend.py
index f9e70f3..12dcb0a 100644
--- a/backend/routers/frontend.py
+++ b/backend/routers/frontend.py
@@ -145,7 +145,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -160,7 +160,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -188,7 +188,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -199,7 +199,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -213,7 +213,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -224,7 +224,7 @@ async def get_frontends(cluster_id: Optional[int] = None, include_inactive: bool
SELECT id, name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules,
+ request_headers, response_headers, options, tcp_request_rules,
timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
maxconn, is_active, created_at, updated_at, cluster_id, last_config_status
@@ -382,16 +382,16 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat
name, bind_address, bind_port, default_backend, mode,
ssl_enabled, ssl_certificate_id, ssl_certificate_ids, ssl_port, ssl_cert_path, ssl_cert, ssl_verify,
acl_rules, redirect_rules, use_backend_rules,
- request_headers, response_headers, tcp_request_rules, timeout_client, timeout_http_request,
+ request_headers, response_headers, options, tcp_request_rules, timeout_client, timeout_http_request,
rate_limit, compression, log_separate, monitor_uri,
cluster_id, maxconn, updated_at
- ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, CURRENT_TIMESTAMP)
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, CURRENT_TIMESTAMP)
RETURNING id
""", frontend.name, frontend.bind_address, frontend.bind_port,
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.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
+ frontend.request_headers, frontend.response_headers, frontend.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)
@@ -630,15 +630,15 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R
default_backend = $4, mode = $5, ssl_enabled = $6,
ssl_certificate_id = $7, ssl_certificate_ids = $8, ssl_port = $9, ssl_cert_path = $10, ssl_cert = $11, ssl_verify = $12,
acl_rules = $13, redirect_rules = $14, use_backend_rules = $15,
- request_headers = $16, response_headers = $17, tcp_request_rules = $18, timeout_client = $19, timeout_http_request = $20,
- rate_limit = $21, compression = $22, log_separate = $23, monitor_uri = $24,
- cluster_id = $25, maxconn = $26, updated_at = CURRENT_TIMESTAMP
- WHERE id = $27
+ request_headers = $16, response_headers = $17, options = $18, tcp_request_rules = $19, timeout_client = $20, timeout_http_request = $21,
+ rate_limit = $22, compression = $23, log_separate = $24, monitor_uri = $25,
+ cluster_id = $26, maxconn = $27, updated_at = CURRENT_TIMESTAMP
+ WHERE id = $28
""", frontend.name, frontend.bind_address, frontend.bind_port,
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.tcp_request_rules, frontend.timeout_client, frontend.timeout_http_request,
+ frontend.request_headers, frontend.response_headers, frontend.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/services/haproxy_config.py b/backend/services/haproxy_config.py
index 414d663..bf5e053 100644
--- a/backend/services/haproxy_config.py
+++ b/backend/services/haproxy_config.py
@@ -282,6 +282,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An
if frontend.get('monitor_uri'):
config_lines.append(f" monitor-uri {frontend['monitor_uri']}")
+ # Frontend Options (NEW: option httplog, option forwardfor, etc.)
+ if frontend.get('options'):
+ for line in frontend['options'].split('\n'):
+ if line.strip():
+ # Lines are complete HAProxy option directives
+ # Examples: "option httplog", "option forwardfor", "option dontlognull"
+ config_lines.append(f" {line.strip()}")
+
# Request Headers (includes options, http-request directives - already formatted)
if frontend.get('request_headers'):
for line in frontend['request_headers'].split('\n'):
@@ -459,6 +467,14 @@ async def generate_haproxy_config_for_cluster(cluster_id: int, conn: Optional[An
default_server_line += f" rise {backend['default_server_rise']}"
config_lines.append(default_server_line)
+ # Backend Options (NEW: option http-keep-alive, option forwardfor, etc.)
+ if backend.get('options'):
+ for line in backend['options'].split('\n'):
+ if line.strip():
+ # Lines are complete HAProxy option directives
+ # Examples: "option http-keep-alive", "option forwardfor", "option httpchk"
+ config_lines.append(f" {line.strip()}")
+
# Backend Request Headers (new field - includes options, http-request directives)
if backend.get('request_headers'):
for line in backend['request_headers'].split('\n'):
diff --git a/backend/utils/haproxy_config_parser.py b/backend/utils/haproxy_config_parser.py
index bf8cd7f..5ae42b5 100644
--- a/backend/utils/haproxy_config_parser.py
+++ b/backend/utils/haproxy_config_parser.py
@@ -47,6 +47,7 @@ class ParsedBackend:
default_server_rise: Optional[int] = None # Default rise value for all servers
request_headers: Optional[str] = None # HTTP request headers (backend level)
response_headers: Optional[str] = None # HTTP response headers (backend level)
+ options: Optional[str] = None # HAProxy backend options (option http-keep-alive, option forwardfor, etc.)
timeout_connect: Optional[int] = 10000
timeout_server: Optional[int] = 60000
timeout_queue: Optional[int] = 60000
@@ -83,6 +84,7 @@ class ParsedFrontend:
redirect_rules: Optional[list] = None
request_headers: Optional[str] = None
response_headers: Optional[str] = None
+ options: Optional[str] = None # HAProxy frontend options (option httplog, option forwardfor, etc.)
tcp_request_rules: Optional[str] = None # TCP request directives (for TCP mode)
@@ -228,9 +230,10 @@ class HAProxyConfigParser:
frontend = ParsedFrontend(name=name)
ssl_cert_found = False
- # Collect HTTP headers, ACL rules, use_backend rules, and TCP rules
+ # Collect HTTP headers, ACL rules, use_backend rules, TCP rules, and options
request_headers_list = []
response_headers_list = []
+ options_list = []
acl_rules_list = []
use_backend_rules_list = []
tcp_request_rules_list = []
@@ -306,6 +309,35 @@ class HAProxyConfigParser:
if maxconn_match:
frontend.maxconn = int(maxconn_match.group(1))
+ # Parse options (httplog, forwardfor, etc.) - NEW for frontend support
+ if line.startswith('option '):
+ # Validate option directive - check for known HAProxy options
+ option_match = re.match(r'^option\s+(\S+)', line, re.IGNORECASE)
+ if option_match:
+ option_name = option_match.group(1)
+ # 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',
+ 'contstats', 'http-pretend-keepalive', 'logasap', 'nolinger', 'persist',
+ 'prefer-last-server', 'splice-auto', 'splice-request', 'splice-response',
+ 'transparent', 'abortonclose', 'allbackups', 'checkcache', 'clitcpka',
+ 'srvtcpka', 'http-no-delay', 'socket-stats', 'tcp-smart-accept',
+ 'tcp-smart-connect', 'independant-streams', 'log-separate-errors',
+ 'log-health-checks', 'accept-invalid-http-request', 'accept-invalid-http-response'
+ ]
+
+ if option_name not in valid_options:
+ # Unknown/invalid option - add warning but still collect it
+ self.warnings.append(
+ f"Frontend '{name}': Unknown or invalid option '{option_name}' detected. "
+ "Please verify this directive is supported by your HAProxy version."
+ )
+
+ # Collect option for frontend.options field
+ options_list.append(line.strip())
+ continue # Skip options - don't add to request_headers
+
# Parse HTTP request headers
if line.startswith('http-request '):
# Check for unsupported capture directives
@@ -394,13 +426,17 @@ class HAProxyConfigParser:
if line.startswith('tcp-request '):
tcp_request_rules_list.append(line)
- # Combine collected headers and rules
+ # Combine collected headers, rules, and options
if request_headers_list:
frontend.request_headers = '\n'.join(request_headers_list)
if response_headers_list:
frontend.response_headers = '\n'.join(response_headers_list)
+ # NEW: Assign collected options to frontend
+ if options_list:
+ frontend.options = '\n'.join(options_list)
+
if acl_rules_list:
frontend.acl_rules = acl_rules_list
@@ -422,9 +458,10 @@ class HAProxyConfigParser:
try:
backend = ParsedBackend(name=name, servers=[])
- # Collect HTTP headers
+ # Collect HTTP headers and options
request_headers_list = []
response_headers_list = []
+ options_list = []
for line in lines:
# Parse mode
@@ -495,9 +532,7 @@ class HAProxyConfigParser:
# Parse options (http-keep-alive, tcp-check, etc.)
# Note: option httpchk is handled separately above
- # CRITICAL FIX: Do NOT add option directives to request_headers
- # Options are separate HAProxy config directives, not HTTP headers
- # They are skipped during bulk import as they need special handling
+ # NEW: Collect options for backend.options field
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)
@@ -516,19 +551,15 @@ class HAProxyConfigParser:
]
if option_name not in valid_options:
- # Unknown/invalid option - add warning and skip
+ # Unknown/invalid option - add warning but still collect it
self.warnings.append(
- f"Backend '{name}': Unknown or invalid option '{option_name}' detected and skipped. "
+ f"Backend '{name}': Unknown or invalid option '{option_name}' detected. "
"Please verify this directive is supported by your HAProxy version."
)
- else:
- # Valid option but skip it - bulk import doesn't support custom options yet
- self.warnings.append(
- f"Backend '{name}': HAProxy option '{option_name}' detected and skipped. "
- f"Custom HAProxy options are not supported in bulk import. "
- f"Configure options manually after import if needed."
- )
- continue # Always skip options - don't add to request_headers
+
+ # Collect option for backend.options field
+ options_list.append(line.strip())
+ continue # Skip options - don't add to request_headers
# Parse HTTP request headers
if line.startswith('http-request '):
@@ -607,6 +638,10 @@ class HAProxyConfigParser:
logger.warning(f"Backend '{name}': Excluded {len(portless_servers)} portless servers (TCP mode requires ports)")
+ # Assign collected options to backend (NEW)
+ if options_list:
+ backend.options = '\n'.join(options_list)
+
# DNS HOSTNAME DETECTION: Warn if servers use hostnames instead of IPs
if backend.servers:
hostname_servers = []
diff --git a/frontend/src/components/BackendServers.js b/frontend/src/components/BackendServers.js
index 0a49634..f9abb16 100644
--- a/frontend/src/components/BackendServers.js
+++ b/frontend/src/components/BackendServers.js
@@ -1779,6 +1779,21 @@ const BackendServers = () => {
+