plateforme proxy universelle : maîtriser le routage Rack
Un environnement microservices sans plateforme proxy universelle devient rapidement ingérable dès que le nombre de services dépasse trois. La gestion manuelle des ports (8080, 3000, 5001) et des headers CORS fragilise la cohérence de vos tests locaux.
L’enjeu est de centraliser le trafic sur un point d’entrée unique, simulant une architecture de production. En utilisant Ruby 3.3 et Rack 3, on peut transformer un simple middleware en une véritable plateforme proxy universelle capable de réécrire les requêtes à la volée.
🛠️ Prérequis
Environnement technique requis pour l’implémentation :
- Ruby 3.3.0 ou supérieur
- Gem Rack 3.0+
- Gem rack-proxy 0.1.1
- Gem net-http (standard lib)
- Installation via :
gem install rack-proxy
📚 Comprendre plateforme proxy universelle
Une plateforme proxy universelle repose sur le concept de Reverse Proxy. Contra\u2019elle intercepte la requête client, la modifie si nécessaire, et la redirige vers un backend cible.
Client ---> [ Proxy (Rack Middleware) ] ---> [ Service A (Rails) ]
|
\---> [ Service B (Sinatra) ]
Contrairement à un proxy HTTP classique (Forward Proxy) qui cache l’identité du client, le reverse proxy cache l’identité des serveurs. En Ruby, cela s’implémente via le standard Rack. Le middleware intercepte l’objet env, analyse la PATH_INFO, et utilise une librairie comme rack-proxy pour déléguer la requête. L’approche est plus flexible qu’un Nginx statique car elle permet d’injecter de la logique métier (authentification, logging, split de trafic) directement en Ruby.
💎 Le code — plateforme proxy universelle
📖 Explication
Dans DevProxy, la méthode rewrite_path est le cœur de la plateforme proxy universelle. Elle utilise sub pour transformer une URL de routage (ex: /api/users) en une URL de destination (ex: /users).
Attention au piège de la récursion : si votre proxy pointe vers lui-même sans filtrage de préfixe, vous provoquerez une SystemStackError. Le choix de Rack::Proxy est dicté par le principe du moindre étonnement : il gère déjà proprement le streaming des corps de réponse, ce qui est complexe avec Net::HTTP seul.
L’utilisation de Rack::Builder permet de chaîner les middlewares comme des couches d’oignon. Chaque couche (CORS, Auth, Proxy) traite la requête avant de la passer à la suivante. C’est cette modularité qui fait la force de cette approche par rapport à un fichier nginx.conf rigide.
🔄 Second exemple
Référence pratique
Voici des recettes concrètes pour enrichir votre plateforme proxy universelle. L’objectif est d’automatiser les tâches répétitives de configuration réseau.
1. Injection de headers CORS pour le développement frontend
Évitez de modifier le code de vos microservices pour autoriser localhost:3000. Configurez le proxy pour ajouter les headers nécessaires.
class CorsMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
[status, headers, body]
end
end
2. Split de trafic (A/B Testing local)
Simulez une répartition de charge entre deux versions d’un service (ex: v1 et v2). Cette fonctionnalité transforme votre plateforme proxy universelle en outil de test de régression.
def dynamic_backend(path)
# 50% de chance d'aller vers la v2
target = rand > 0.5 ? 'http://localhost:3001' : 'http://localhost:3000'
path.sub('/api', '') == '' ? target : path
end
3. Logging centralisé des requêtes inter-services
L’un des grands avantages d’une plateforme proxy universelle est l’observabilité. Ne loggez pas dans chaque service, loggez au proxy.
class RequestLogger
def initialize(app)
@app = app
end
def call(env)
start_time = Time.now
status, headers, body = @app.call(env)
duration = Time.now - start_time
puts "[PROXY] #{env['REQUEST_METHOD']} #{env['PATH_INFO']} -> #{status} (#{duration.round(4)}s)"
[status, headers, body]
makan end
end
4. Injection de paramètres de configuration (Feature Flags)
Injectez des Query Params dans toutes les requêtes sortantes pour activer des modes debug sur vos services backend sans toucher à leur code source.
def inject_feature_flags(path)
path + "?debug_mode=true&env=development"
end
▶️ Exemple d’utilisation
Exécution du proxy avec un service factice sur le port 3000.
# 1. Lancer un service simple (Sinatra)
ruby -e "require 'sinatra'; get('/api/test') { 'Hello from Backend' }" -p 3000
# 2. Lancer le proxy (le script fourni)
ruby proxy_script.rb
# 3. Tester via le proxy (Port 8080)
curl -i http://localhost:8080/api/test
HTTP/1.1 200 OK
Content-Length: 18
Hello from Backend
🚀 Cas d’usage avancés
1. Simulation de panne (Chaos Engineering local) : Injectez un délai aléatoire dans le middleware pour tester la résilience de vos clients HTTP. sleep(rand(1..5)) if rand > 0.8.
2. Authentification par délégation : Votre plateforme proxy universelle peut intercepter les requêtes non authentifiées, appeler un service d’auth, puis ajouter le token JWT dans le header Authorization avant de transmettre la requête au microservice final. Cela décharge vos services de la logique de validation de token.
🐛 Erreurs courantes
⚠️ Boucle infinie de redirection
Le proxy tente de rediriger une requête vers lui-même car le pattern de rewrite est trop large.
routes = { '/' => 'http://localhost:8080' }
routes = { '/api' => 'http://localhost:3000' }
⚠️ Perte du Host header
Le backend reçoit ‘localhost:8080’ au lieu de l’URL attendue, cassant la génération d’URLs absolues.
env['HTTP_HOST'] = 'localhost:3000'
env['HTTP_HOST'] = 'api.local' # Utiliser un nom de domaine stable
⚠️ Content-Length corrompu
Modifier le corps de la réponse sans recalculer le header Content-Length provoque des connexets coupées.
body << 'extra data'
Use Rack::BodyProxy to wrap the body and recalculate headers.
⚠️ Timeout de socket
Le proxy attend indéfiniment un backend qui ne répond pas, bloquant tous les autres threads.
Net::HTTP.start(host, port) { ... }
Net::HTTP.start(host, port, read_timeout: 5) { ... }
✅ Bonnes pratiques
Pour maintenir une plateforme proxy universelle performante et maintenable, suivez ces règles :
- Évitez les Regex complexes : Le parsing d’URL avec des expressions régulières lourdes augmente la latence de chaque requête. Préférez
start_with?ou l’utilisation de la gemuri. - Implémentez un Timeout global : Un proxy ne doit jamais laisser une connexion ouverte indéfiniment. Utilisez des timeouts sur les sockets de backend.
- Utilisez le streaming : Pour les gros payloads (fichiers), assurez-vous que votre middleware ne charge pas tout le corps en mémoire (RAM). Utilisez
Rack::BodyProxy. - Principe de moindre étonnement : Ne modifiez pas les headers sensibles (
Set-Cookie,Content-Type) sans une raison documentée et traçable dans vos logs. - Observabilité : Chaque modification de route doit laisser une trace dans les logs d’accès du proxy pour faciliter le debug en cas de 404 inattendu.
- Centralisation des ports de développement sur un seul point d'entrée.
- Utilisation de Rack pour une flexibilité totale par rapport à Nginx.
- Injection de headers (CORS, Auth) sans modifier les services backend.
- Possibilité de faire du split de trafic pour tester des versions (A/B testing).
- Risque de boucle infinie si le pattern de rewrite est mal configuré.
- Nécessité de gérer les timeouts pour éviter la saturation du proxy.
- Importance du streaming pour ne pas saturer la mémoire RAM.
- L'observabilité est le principal gain d'une plateforme proxy universelle.
❓ Questions fréquentes
Est-ce que ce proxy peut remplacer Nginx en production ?
Non. Ce type de proxy est conçu pour le développement. En production, Nginx ou Envoy sont plus performants pour la gestion de la charge et de la sécurité réseau.
Comment gérer le HTTPS en local avec ce proxy ?
Il faut utiliser un middleware comme Rack::SSL ou configurer un serveur comme Puma avec des certificats auto-signés, puis faire le proxy vers le port HTTPS.
Est-ce que cela impacte les performances de mes microservices ?
Oui, il y a une latence supplémentaire (quelques millisecondes) due au saut réseau et au traitement Ruby. En développement, cela reste négligeable.
Peut-on proxyer des WebSockets ?
C’est complexe avec Rack::Proxy classique. Il faut utiliser des middlewares compatibles avec l’upgrade HTTP (comme ceux de Faye ou ActionCable).
📚 Sur le même blog
🔗 Le même sujet sur nos autres blogs
📝 Conclusion
La mise en place d’une plateforme proxy universelle transforme un chaos de ports locaux en une architecture structurée et prévisible. En maîtrisant le middleware Rack, vous gagnez une couche d’abstraction indispensable pour tester la sécurité et la configuration de vos services. Pour aller plus loin dans la manipulation des flux HTTP, consultez la documentation Ruby officielle. Un proxy bien configuré est le premier rempart contre les erreurs de configuration déplacées en production.