Hay un momento específico en el que te das cuenta de que esperaste demasiado. Para mí fue un martes a las 11 de la noche, con tres ventanas de VS Code abiertas, un HAProxy 2.8 corriendo en Docker, y un error de validación que no entendía por qué estaba fallando. Abro la extensión de syntax highlighting que tenía instalada — la única que existía en el marketplace — y veo que el último commit fue en 2019.
- Cinco años sin un solo cambio. HAProxy pasó de la versión 2.0 a la 3.1 en ese tiempo. Metieron
log-format-sd, reescribieron el comportamiento deoption http-server-close, deprecaron directivas enteras. Y la extensión ahí, congelada en el tiempo como una momia digital, sin saber nada de nada.
Ese martes dije: basta. Si nadie lo va a hacer, lo hago yo.
Por qué HAProxy merece una extensión decente (y por qué casi nadie habla de esto)
Antes de meterme en el código, necesito darte contexto porque sé que el 80% de los devs que leen esto trabajan con Nginx o Traefik y creen que HAProxy es "esa cosa vieja que usan los bancos". Y sí, tienen razón en la segunda parte — los bancos lo usan, las telcos lo usan, los exchanges de crypto lo usan. Pero no porque sea viejo. Sino porque es brutalmente eficiente y tiene el modelo de configuración más expresivo que existe para un proxy.
Yo lo uso todos los días. En el trabajo para balancear tráfico entre microservicios. En mi homelab tengo un stack con HAProxy al frente, tres backends de servicios internos, rate limiting por IP, ACLs que distinguen tráfico de la LAN del tráfico que viene por VPN, y health checks cada cinco segundos. Todo en un archivo .cfg que tiene más de 400 líneas.
El problema es que ese archivo .cfg es básicamente texto plano para cualquier editor. Sin schema, sin LSP, sin nada. Escribís frontend mi-frontend y el editor no sabe que adentro de ese bloque hay directivas específicas que no existen en ningún otro contexto. Escribís backend y no te sugiere balance roundrobin versus balance leastconn. Usás una directiva que fue deprecada en 2.6 y nadie te avisa.
Eso es exactamente lo que fui a arreglar.
La arquitectura: no era tan simple como "un JSON con keywords"
La primera semana pensé que iba a ser fácil. "Meto todas las keywords en un archivo de gramática TextMate, le doy colores, listo". Esa ingenuidad duró exactamente hasta que abrí el spec completo de configuración de HAProxy.
HAProxy tiene una arquitectura de secciones: global, defaults, frontend, backend, listen, peers, resolvers, userlist, cache, program. Y cada sección acepta un subconjunto diferente de directivas. bind solo existe en frontend y listen. server solo existe en backend y listen. mode existe en varias pero con diferentes valores permitidos dependiendo del contexto.
Eso no se puede resolver con TextMate grammars. Eso necesita un Language Server Protocol real.
// src/server/haproxy-language-server.ts
// El corazón del LSP — inicializamos las capacidades que vamos a soportar
import {
createConnection,
TextDocuments,
ProposedFeatures,
CompletionItem,
CompletionItemKind,
TextDocumentSyncKind,
} from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { HaproxyParser } from './parser/haproxy-parser';
import { CompletionProvider } from './providers/completion-provider';
import { DiagnosticsProvider } from './providers/diagnostics-provider';
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);
// Cuando el cliente (VS Code) nos pide completions, necesitamos saber
// en qué sección estamos parados para dar sugerencias contextuales
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: {
resolveProvider: true, // habilitamos el detalle de cada ítem
triggerCharacters: [' ', '\t'] // autocompletado al tipear espacio o tab
},
definitionProvider: true, // go-to-definition para backends
codeActionProvider: true, // quickfix para directivas deprecadas
diagnosticProvider: {
interFileDependencies: false,
workspaceDiagnostics: false
}
}
}));
El parser fue la parte más complicada y la que más tiempo me llevó. HAProxy no tiene un formato estricto tipo YAML o JSON — es un lenguaje de configuración propio con indentación opcional, comentarios con #, continuación de línea con \, y una semántica de contexto que depende enteramente de en qué sección estás.
// src/server/parser/haproxy-parser.ts
// Parser que entiende el contexto de sección — clave para todo lo demás
export interface ParsedSection {
type: SectionType; // 'global' | 'defaults' | 'frontend' | 'backend' | etc.
name: string | null; // nombre de la sección (null para global/defaults)
startLine: number;
endLine: number;
directives: ParsedDirective[];
}
export class HaproxyParser {
parse(text: string): ParsedSection[] {
const lines = text.split('\n');
const sections: ParsedSection[] = [];
let currentSection: ParsedSection | null = null;
lines.forEach((line, lineNumber) => {
const trimmed = line.trim();
// Ignoramos comentarios y líneas vacías
if (trimmed.startsWith('#') || trimmed === '') return;
// Detectamos el inicio de una nueva sección
const sectionMatch = trimmed.match(
/^(global|defaults|frontend|backend|listen|peers|resolvers|userlist|cache|program)\s*(\S*)$/
);
if (sectionMatch) {
// Cerramos la sección anterior si existe
if (currentSection) {
currentSection.endLine = lineNumber - 1;
sections.push(currentSection);
}
// Iniciamos la nueva sección con su tipo y nombre
currentSection = {
type: sectionMatch[1] as SectionType,
name: sectionMatch[2] || null,
startLine: lineNumber,
endLine: -1, // se completa cuando encontramos la próxima sección
directives: []
};
return;
}
// Si estamos dentro de una sección, parseamos la directiva
if (currentSection) {
currentSection.directives.push(
this.parseDirective(trimmed, lineNumber)
);
}
});
// No olvidemos cerrar la última sección
if (currentSection) {
(currentSection as ParsedSection).endLine = lines.length - 1;
sections.push(currentSection as ParsedSection);
}
return sections;
}
}
El autocompletado contextual: la feature que cambió todo
Una vez que tenía el parser funcionando, el autocompletado contextual fue casi natural. La idea es simple: cuando VS Code te pide completions, le preguntás al parser "¿en qué sección está el cursor?" y filtrás las sugerencias en base a eso.
¿Estás en un frontend? Te ofrezco bind, mode, acl, use_backend, default_backend, option, timeout... pero NO te ofrezco server ni balance, que son de backend. ¿Estás en global? Te ofrezco maxconn, daemon, log, ssl-default-bind-options... y nada más.
Esto parece trivial pero la diferencia en la experiencia de uso es brutal. En una configuración de HAProxy compleja con 10 secciones, el autocompletado que no entiende contexto te tira 200 opciones mezcladas. El mío te tira exactamente las que aplican a donde estás parado.
Pero el feature que más me enorgullece es el go-to-definition para backends. Si en tu frontend tenés default_backend mi-api y presionás F12, te lleva directo a la sección backend mi-api. Suena simple. Pero cuando tu config tiene 400 líneas y 15 backends, ese F12 te ahorra literalmente minutos de scroll todos los días.
Validación multi-versión: el quilombo de HAProxy 2.4 a 3.1
Acá es donde me volví un poco loco. HAProxy cambió bastante entre versiones. Cosas que eran válidas en 2.4 quedaron deprecadas en 2.6, y otras directamente removidas en 3.0. Si la extensión no sabe qué versión estás usando, los diagnósticos van a estar llenos de falsos positivos o falsos negativos.
La solución fue agregar una setting en VS Code donde el usuario declara su versión de HAProxy:
// .vscode/settings.json — configuración por workspace
{
"gmm-haproxy.version": "2.8",
"gmm-haproxy.strictMode": true
}
Y del lado del servidor, mantengo un registro de qué directivas existen en qué versión, cuáles fueron deprecadas y cuándo, y cuáles fueron removidas:
// src/server/schema/version-registry.ts
// Registry de directivas por versión — acá está el conocimiento duro de HAProxy
export interface DirectiveInfo {
name: string;
sections: SectionType[]; // en qué secciones es válida
since: string; // versión en que fue introducida
deprecated?: string; // versión en que fue deprecada
removed?: string; // versión en que fue removida
replacement?: string; // directiva recomendada si fue deprecada
description: string;
}
// Ejemplo real de directivas con su historial de versiones
export const DIRECTIVE_REGISTRY: DirectiveInfo[] = [
{
name: 'option forwardfor',
sections: ['frontend', 'backend', 'listen', 'defaults'],
since: '1.3',
description: 'Agrega el header X-Forwarded-For con la IP real del cliente'
},
{
name: 'reqadd',
sections: ['frontend', 'listen', 'backend'],
since: '1.3',
deprecated: '2.2', // deprecada en 2.2
removed: '3.0', // removida en 3.0
replacement: 'http-request set-header', // la alternativa moderna
description: '[DEPRECADA] Agregaba headers a la request. Usá http-request set-header'
},
{
name: 'http-request set-header',
sections: ['frontend', 'backend', 'listen'],
since: '2.2',
description: 'Modifica o agrega headers HTTP en la request entrante'
}
// ... y así con ~400 directivas más
];
Cuando el DiagnosticsProvider detecta que usás reqadd en una config con version 3.0, te tira un error con quickfix incluido: "Reemplazar por http-request set-header". Un click y listo.
Los errores que cometí (y que vos vas a cometer si hacés algo parecido)
Error 1: Subestimar el tiempo de startup del LSP. El primer prototipo parseaba el documento entero en cada keystroke. En archivos grandes, el lag era notable. La solución fue parsing incremental — solo re-parseás las secciones que cambiaron.
Error 2: No manejar configs incompletas. Mientras escribís, tu config está rota la mayoría del tiempo. El parser tiene que ser tolerante a errores y producir un AST parcial útil en lugar de explotar. Tomó dos semanas extra hacer el error recovery decente.
Error 3: Creer que la API de VS Code es estable. Entre la versión que leí en la doc y la versión que tenía instalada, había diferencias sutiles en cómo funcionaba onDocumentDiagnostic. Aprendí a siempre testear contra la versión mínima declarada en el engines.vscode del package.json.
Error 4: No tener un corpus de configs reales para testear. Armé un directorio con configs reales anonimizadas de mi homelab y del trabajo. Eso solo encontró más bugs que cualquier test unitario que escribí.
El resultado: lo que uso todos los días
Hoy gmm-haproxy-vscode tiene:
- Syntax highlighting contextual — diferencia visualmente entre nombres de sección, directivas, valores, ACL names y comentarios
- LSP propio con autocompletado filtrado por sección
- Validación en tiempo real contra el schema de la versión declarada (2.4, 2.6, 2.8, 3.0, 3.1)
-
Go-to-definition para backends referenciados en
use_backendydefault_backend - Quickfix automático para directivas deprecadas
- Hover documentation — pasás el mouse por cualquier directiva y te explica qué hace
- Snippets para estructuras comunes: frontend básico, backend con health check, ACL de rate limiting
La semana pasada la usé para refactorizar toda la config de mi homelab de HAProxy 2.8 a 3.1. Sin la extensión, ese proceso hubiera sido un domingo entero de revisar el changelog y buscar directivas obsoletas a mano. Con la extensión, fueron dos horas — la mayoría del tiempo la pasé aplicando quickfixes.
Por qué hice esto y no simplemente usé Nginx
Alguien me va a preguntar eso, así que lo respondo antes. HAProxy hace cosas que Nginx no hace igual de bien. El modelo de ACLs de HAProxy es extraordinariamente expresivo. Podés tomar decisiones de routing basadas en headers, paths, source IPs, tiempo del día, peso del backend, número de conexiones activas — todo en el archivo de config, sin scripting. El health checking es más granular. El modelo de estadísticas via socket es más completo.
¿Es la herramienta correcta para todo? No. Para un proyecto personal chico, Traefik o Caddy son más cómodos. Pero cuando tenés tráfico real y necesitás control fino, HAProxy sigue siendo el rey. Y el rey se merece una extensión que no sea un zombie del 2019.
Si querés probar gmm-haproxy-vscode, la vas a encontrar en el VS Code Marketplace. Si encontrás un bug o una directiva que no reconoce — y seguro vas a encontrar, el spec de HAProxy es enorme — abrí un issue. Lo maintaingo activamente porque lo uso todos los días. Esa es la mejor garantía que puedo darte.
Este artículo fue publicado originalmente en juanchi.dev
Top comments (0)