Phase 2 : tripoli-edge, service Rust interne derrière Caddy
- tripoli-edge (hyper/tokio bas niveau, pas de framework type axum) : /healthz, /metrics (Prometheus, compteurs à la main), reverse-proxy vers tripoli-api avec vrais 502 tant que ce service n'existe pas (Phase 3), logs JSON structurés par requête. - Écoute sur 127.0.0.1 + 172.17.0.1 (passerelle docker0), jamais sur l'interface publique - même pattern que le Nginx de VoltVision déjà sur ce serveur. - Déployé en prod : systemd (utilisateur système dédié, sans privilège), bloc Caddy ajouté pour tripoli.cloud/www.tripoli.cloud, règle ufw ajoutée (Ansible) pour que Caddy puisse joindre le port 9000 - sans quoi le chemin INPUT de l'hôte bloque le trafic inter-bridges même vers 172.17.0.1. - Vérifié bout-en-bout via https://tripoli.cloud, sans régression sur les 6 autres domaines déjà servis par ce Caddy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2fa16083c3
commit
8f70c2aae7
12 changed files with 1219 additions and 1 deletions
14
tripoli-edge/Cargo.toml
Normal file
14
tripoli-edge/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "tripoli-edge"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "time", "signal"] }
|
||||
hyper = { version = "1", features = ["full"] }
|
||||
hyper-util = { version = "0.1", features = ["full"] }
|
||||
http-body-util = "0.1"
|
||||
bytes = "1"
|
||||
tokio-util = "0.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
60
tripoli-edge/README.md
Normal file
60
tripoli-edge/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# tripoli-edge
|
||||
|
||||
Service Rust interne placé derrière le Caddy déjà en place sur le
|
||||
serveur (voir `README.md` à la racine du dépôt pour le pourquoi : ce
|
||||
Caddy tient déjà les ports 80/443 pour six autres domaines).
|
||||
|
||||
Ce que fait tripoli-edge, concrètement :
|
||||
|
||||
- `GET /healthz` : répond directement, sans toucher à tripoli-api.
|
||||
- `GET /metrics` : métriques au format d'exposition Prometheus
|
||||
(compteurs à la main, voir `src/metrics.rs`).
|
||||
- Tout le reste : reverse-proxy vers `tripoli-api` (méthode, en-têtes et
|
||||
corps conservés). Tant que `tripoli-api` n'existe pas (Phase 3), ça
|
||||
répond un vrai `502 Bad Gateway`, pas une réponse simulée.
|
||||
- Logs structurés JSON (`tracing` + `tracing-subscriber`) pour chaque
|
||||
requête : méthode, chemin, host, statut, latence.
|
||||
|
||||
## Configuration (variables d'environnement)
|
||||
|
||||
| Variable | Défaut | Rôle |
|
||||
|---|---|---|
|
||||
| `TRIPOLI_EDGE_LISTEN` | `127.0.0.1:9000,172.17.0.1:9000` | Adresses d'écoute (jamais `0.0.0.0` — voir plus bas) |
|
||||
| `TRIPOLI_API_UPSTREAM` | `127.0.0.1:8081` | Adresse de tripoli-api |
|
||||
| `RUST_LOG` | — | Niveau de logs (`info`, `debug`, ...) |
|
||||
|
||||
`172.17.0.1` est la passerelle `docker0` du serveur : c'est ce qui permet
|
||||
au conteneur Caddy (sur un réseau Docker différent) de joindre
|
||||
tripoli-edge, qui tourne nativement sur l'hôte — même pattern que le
|
||||
Nginx de VoltVision sur ce même serveur. tripoli-edge n'écoute jamais sur
|
||||
l'interface publique.
|
||||
|
||||
## Déploiement (fait manuellement pour cette phase)
|
||||
|
||||
L'automatisation complète (build + déploiement via CI) est prévue dans
|
||||
la section CI/CD du brief, pas encore faite. Pour l'instant, sur le
|
||||
serveur :
|
||||
|
||||
```bash
|
||||
# build (rustc/cargo installés via apt)
|
||||
cd /opt/tripoli-edge/src && cargo build --release
|
||||
install -m 755 target/release/tripoli-edge /usr/local/bin/tripoli-edge
|
||||
systemctl restart tripoli-edge
|
||||
```
|
||||
|
||||
Le service tourne sous l'utilisateur système `tripoli` (sans shell,
|
||||
sans privilège) via `deploy/tripoli-edge.service`, déjà installé dans
|
||||
`/etc/systemd/system/`.
|
||||
|
||||
Le bloc Caddy est dans `deploy/Caddyfile.snippet` — ajouté à
|
||||
`/opt/edge/Caddyfile` sur le serveur (fichier partagé, donc pas commité
|
||||
dans son intégralité ici). Une règle `ufw` supplémentaire était
|
||||
nécessaire pour que Caddy puisse joindre `172.17.0.1:9000` (voir
|
||||
`ansible/roles/base/tasks/main.yml`).
|
||||
|
||||
## Vérifié en production
|
||||
|
||||
```
|
||||
curl https://tripoli.cloud/healthz -> 200 "ok"
|
||||
curl https://tripoli.cloud/metrics -> métriques Prometheus
|
||||
```
|
||||
25
tripoli-edge/deploy/Caddyfile.snippet
Normal file
25
tripoli-edge/deploy/Caddyfile.snippet
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Bloc ajouté à /opt/edge/Caddyfile sur le serveur (fichier partagé avec
|
||||
# les 6 autres projets hébergés, donc pas dans ce dépôt en entier — voir
|
||||
# le README de ce dossier).
|
||||
#
|
||||
# Pourquoi Caddy et pas tripoli-edge lui-même sur 80/443 : les ports 80 et
|
||||
# 443 appartiennent déjà à ce Caddy. tripoli-edge est un service interne
|
||||
# (172.17.0.1:9000, passerelle docker0, jamais exposé publiquement) :
|
||||
# Caddy fait TLS/ACME et le routage par Host, tripoli-edge fait le reste.
|
||||
|
||||
tripoli.cloud, www.tripoli.cloud {
|
||||
reverse_proxy 172.17.0.1:9000
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "SAMEORIGIN"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/tripoli-cloud.log
|
||||
format json
|
||||
}
|
||||
}
|
||||
25
tripoli-edge/deploy/tripoli-edge.service
Normal file
25
tripoli-edge/deploy/tripoli-edge.service
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[Unit]
|
||||
Description=tripoli-edge - service interne derrière Caddy pour tripoli.cloud
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=tripoli
|
||||
Group=tripoli
|
||||
ExecStart=/usr/local/bin/tripoli-edge
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
Environment=RUST_LOG=info
|
||||
Environment=TRIPOLI_EDGE_LISTEN=127.0.0.1:9000,172.17.0.1:9000
|
||||
Environment=TRIPOLI_API_UPSTREAM=127.0.0.1:8081
|
||||
|
||||
# Un utilisateur système dédié, sans shell, plutôt que root : ce service
|
||||
# n'a besoin d'aucun privilège (pas de port <1024, pas d'accès disque
|
||||
# hors ses propres logs via journald).
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
42
tripoli-edge/src/config.rs
Normal file
42
tripoli-edge/src/config.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
/// Configuration minimale, entièrement par variables d'environnement :
|
||||
/// pas de fichier de config à ce stade, ce serait de la complexité pour
|
||||
/// rien tant qu'il n'y a qu'un seul déploiement (derrière Caddy, sur ce
|
||||
/// même serveur).
|
||||
pub struct Config {
|
||||
/// Adresses d'écoute internes. Toujours la boucle locale et/ou une
|
||||
/// passerelle docker (jamais 0.0.0.0) : c'est le même choix que pour
|
||||
/// le Nginx de VoltVision sur ce serveur (172.17.0.1, passerelle
|
||||
/// docker0, atteignable depuis le conteneur Caddy). Une valeur reste
|
||||
/// une adresse unique ; plusieurs sont séparées par une virgule.
|
||||
pub listen_addrs: Vec<SocketAddr>,
|
||||
/// Adresse de tripoli-api. Tant que ce service n'existe pas (Phase 3),
|
||||
/// les requêtes proxyées échoueront en 502 - un vrai 502, pas une
|
||||
/// réponse simulée.
|
||||
pub upstream_addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_env() -> Self {
|
||||
let listen_addrs = std::env::var("TRIPOLI_EDGE_LISTEN")
|
||||
.unwrap_or_else(|_| "127.0.0.1:9000,172.17.0.1:9000".to_string())
|
||||
.split(',')
|
||||
.map(|addr| {
|
||||
addr.trim()
|
||||
.parse()
|
||||
.expect("TRIPOLI_EDGE_LISTEN doit être une liste d'adresses host:port valides")
|
||||
})
|
||||
.collect();
|
||||
|
||||
let upstream_addr = std::env::var("TRIPOLI_API_UPSTREAM")
|
||||
.unwrap_or_else(|_| "127.0.0.1:8081".to_string())
|
||||
.parse()
|
||||
.expect("TRIPOLI_API_UPSTREAM doit être une adresse host:port valide");
|
||||
|
||||
Self {
|
||||
listen_addrs,
|
||||
upstream_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
106
tripoli-edge/src/main.rs
Normal file
106
tripoli-edge/src/main.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
mod config;
|
||||
mod metrics;
|
||||
mod proxy;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use config::Config;
|
||||
use metrics::Metrics;
|
||||
use proxy::UpstreamClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.json()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let config = Config::from_env();
|
||||
let metrics = Arc::new(Metrics::default());
|
||||
let client = Arc::new(proxy::build_client());
|
||||
let shutdown = CancellationToken::new();
|
||||
|
||||
let mut listeners = Vec::new();
|
||||
for addr in &config.listen_addrs {
|
||||
let listener = TcpListener::bind(addr)
|
||||
.await
|
||||
.unwrap_or_else(|err| panic!("impossible d'écouter sur {addr}: {err}"));
|
||||
listeners.push(listener);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
listen_addrs = ?config.listen_addrs,
|
||||
upstream_addr = %config.upstream_addr,
|
||||
"tripoli-edge démarré"
|
||||
);
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
for listener in listeners {
|
||||
let client = Arc::clone(&client);
|
||||
let metrics = Arc::clone(&metrics);
|
||||
let shutdown = shutdown.clone();
|
||||
let upstream_addr = config.upstream_addr;
|
||||
tasks.push(tokio::spawn(accept_loop(
|
||||
listener,
|
||||
client,
|
||||
metrics,
|
||||
upstream_addr,
|
||||
shutdown,
|
||||
)));
|
||||
}
|
||||
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("impossible d'installer le gestionnaire de signal");
|
||||
tracing::info!("signal d'arrêt reçu, extinction de tripoli-edge");
|
||||
shutdown.cancel();
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn accept_loop(
|
||||
listener: TcpListener,
|
||||
client: Arc<UpstreamClient>,
|
||||
metrics: Arc<Metrics>,
|
||||
upstream_addr: SocketAddr,
|
||||
shutdown: CancellationToken,
|
||||
) {
|
||||
loop {
|
||||
let (stream, peer_addr) = tokio::select! {
|
||||
accepted = listener.accept() => match accepted {
|
||||
Ok(pair) => pair,
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "échec d'acceptation d'une connexion");
|
||||
continue;
|
||||
}
|
||||
},
|
||||
_ = shutdown.cancelled() => return,
|
||||
};
|
||||
|
||||
let client = Arc::clone(&client);
|
||||
let metrics = Arc::clone(&metrics);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let io = TokioIo::new(stream);
|
||||
let service = service_fn(move |req| {
|
||||
proxy::handle(req, Arc::clone(&client), upstream_addr, Arc::clone(&metrics))
|
||||
});
|
||||
|
||||
if let Err(err) = http1::Builder::new()
|
||||
.serve_connection(io, service)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(peer = %peer_addr, error = %err, "connexion terminée en erreur");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
84
tripoli-edge/src/metrics.rs
Normal file
84
tripoli-edge/src/metrics.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
|
||||
|
||||
/// Compteurs à la main plutôt qu'une crate de métriques : pour cinq
|
||||
/// compteurs, une lib dédiée serait une abstraction de plus à défendre
|
||||
/// sans bénéfice réel. `Ordering::Relaxed` suffit ici - on n'a besoin
|
||||
/// d'aucune synchronisation entre compteurs, juste que chaque incrément
|
||||
/// soit atomique.
|
||||
#[derive(Default)]
|
||||
pub struct Metrics {
|
||||
requests_total: AtomicU64,
|
||||
responses_1xx: AtomicU64,
|
||||
responses_2xx: AtomicU64,
|
||||
responses_3xx: AtomicU64,
|
||||
responses_4xx: AtomicU64,
|
||||
responses_5xx: AtomicU64,
|
||||
in_flight: AtomicI64,
|
||||
duration_ms_sum: AtomicU64,
|
||||
}
|
||||
|
||||
pub struct InFlightGuard<'a>(&'a Metrics);
|
||||
|
||||
impl<'a> InFlightGuard<'a> {
|
||||
pub fn enter(metrics: &'a Metrics) -> Self {
|
||||
metrics.in_flight.fetch_add(1, Ordering::Relaxed);
|
||||
Self(metrics)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InFlightGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.in_flight.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
pub fn record(&self, status: u16, duration_ms: u64) {
|
||||
self.requests_total.fetch_add(1, Ordering::Relaxed);
|
||||
self.duration_ms_sum.fetch_add(duration_ms, Ordering::Relaxed);
|
||||
let bucket = match status {
|
||||
100..=199 => &self.responses_1xx,
|
||||
200..=299 => &self.responses_2xx,
|
||||
300..=399 => &self.responses_3xx,
|
||||
400..=499 => &self.responses_4xx,
|
||||
_ => &self.responses_5xx,
|
||||
};
|
||||
bucket.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Format texte Prometheus (exposition format), à la main : pas de
|
||||
/// registre global, pas de types de métriques (counter/gauge) au-delà
|
||||
/// de ce qui est vraiment utile ici.
|
||||
pub fn render(&self) -> String {
|
||||
let total = self.requests_total.load(Ordering::Relaxed);
|
||||
let sum_ms = self.duration_ms_sum.load(Ordering::Relaxed);
|
||||
let avg_ms = if total > 0 { sum_ms as f64 / total as f64 } else { 0.0 };
|
||||
|
||||
format!(
|
||||
"# HELP tripoli_edge_requests_total Nombre total de requêtes traitées.\n\
|
||||
# TYPE tripoli_edge_requests_total counter\n\
|
||||
tripoli_edge_requests_total {total}\n\
|
||||
# HELP tripoli_edge_responses_total Réponses par classe de statut HTTP.\n\
|
||||
# TYPE tripoli_edge_responses_total counter\n\
|
||||
tripoli_edge_responses_total{{class=\"1xx\"}} {c1}\n\
|
||||
tripoli_edge_responses_total{{class=\"2xx\"}} {c2}\n\
|
||||
tripoli_edge_responses_total{{class=\"3xx\"}} {c3}\n\
|
||||
tripoli_edge_responses_total{{class=\"4xx\"}} {c4}\n\
|
||||
tripoli_edge_responses_total{{class=\"5xx\"}} {c5}\n\
|
||||
# HELP tripoli_edge_in_flight_requests Requêtes en cours de traitement.\n\
|
||||
# TYPE tripoli_edge_in_flight_requests gauge\n\
|
||||
tripoli_edge_in_flight_requests {in_flight}\n\
|
||||
# HELP tripoli_edge_request_duration_ms_avg Latence moyenne, en millisecondes.\n\
|
||||
# TYPE tripoli_edge_request_duration_ms_avg gauge\n\
|
||||
tripoli_edge_request_duration_ms_avg {avg_ms}\n",
|
||||
total = total,
|
||||
c1 = self.responses_1xx.load(Ordering::Relaxed),
|
||||
c2 = self.responses_2xx.load(Ordering::Relaxed),
|
||||
c3 = self.responses_3xx.load(Ordering::Relaxed),
|
||||
c4 = self.responses_4xx.load(Ordering::Relaxed),
|
||||
c5 = self.responses_5xx.load(Ordering::Relaxed),
|
||||
in_flight = self.in_flight.load(Ordering::Relaxed),
|
||||
avg_ms = avg_ms,
|
||||
)
|
||||
}
|
||||
}
|
||||
123
tripoli-edge/src/proxy.rs
Normal file
123
tripoli-edge/src/proxy.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::combinators::BoxBody;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::body::Incoming;
|
||||
use hyper::{Request, Response, StatusCode, Uri};
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
|
||||
use crate::metrics::{InFlightGuard, Metrics};
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub type EdgeBody = BoxBody<Bytes, BoxError>;
|
||||
pub type UpstreamClient = Client<HttpConnector, Incoming>;
|
||||
|
||||
pub fn build_client() -> UpstreamClient {
|
||||
Client::builder(TokioExecutor::new()).build(HttpConnector::new())
|
||||
}
|
||||
|
||||
fn text_body(text: impl Into<Bytes>) -> EdgeBody {
|
||||
Full::new(text.into())
|
||||
.map_err(|never| match never {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn text_response(status: StatusCode, body: impl Into<Bytes>) -> Response<EdgeBody> {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "text/plain; charset=utf-8")
|
||||
.body(text_body(body))
|
||||
.expect("réponse texte statique toujours valide")
|
||||
}
|
||||
|
||||
/// Point d'entrée de chaque requête reçue de Caddy. Deux routes sont
|
||||
/// gérées par tripoli-edge lui-même (`/healthz`, `/metrics`) ; tout le
|
||||
/// reste est transmis tel quel à tripoli-api.
|
||||
pub async fn handle(
|
||||
req: Request<Incoming>,
|
||||
client: Arc<UpstreamClient>,
|
||||
upstream_addr: SocketAddr,
|
||||
metrics: Arc<Metrics>,
|
||||
) -> Result<Response<EdgeBody>, hyper::Error> {
|
||||
let start = Instant::now();
|
||||
let _in_flight = InFlightGuard::enter(&metrics);
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let host_header = req
|
||||
.headers()
|
||||
.get("host")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("-")
|
||||
.to_string();
|
||||
|
||||
let response = match path.as_str() {
|
||||
"/healthz" => text_response(StatusCode::OK, "ok"),
|
||||
"/metrics" => text_response(StatusCode::OK, metrics.render()),
|
||||
_ => proxy_to_upstream(req, &client, upstream_addr).await,
|
||||
};
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
metrics.record(status, duration_ms);
|
||||
|
||||
tracing::info!(
|
||||
method = %method,
|
||||
path = %path,
|
||||
host = %host_header,
|
||||
status = status,
|
||||
duration_ms = duration_ms,
|
||||
"requête traitée"
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Reconstruit la requête vers tripoli-api en conservant méthode,
|
||||
/// en-têtes et corps ; seule l'autorité de l'URI change, pour pointer
|
||||
/// vers l'adresse interne au lieu de `tripoli.cloud`.
|
||||
async fn proxy_to_upstream(
|
||||
req: Request<Incoming>,
|
||||
client: &UpstreamClient,
|
||||
upstream_addr: SocketAddr,
|
||||
) -> Response<EdgeBody> {
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let path_and_query = parts
|
||||
.uri
|
||||
.path_and_query()
|
||||
.map(|pq| pq.as_str())
|
||||
.unwrap_or("/");
|
||||
|
||||
let upstream_uri: Uri = match format!("http://{upstream_addr}{path_and_query}").parse() {
|
||||
Ok(uri) => uri,
|
||||
Err(_) => return text_response(StatusCode::BAD_GATEWAY, "URI amont invalide"),
|
||||
};
|
||||
|
||||
let mut upstream_req = Request::builder()
|
||||
.method(parts.method)
|
||||
.uri(upstream_uri)
|
||||
.body(body)
|
||||
.expect("requête amont toujours valide : mêmes en-têtes/corps que l'entrante");
|
||||
*upstream_req.headers_mut() = parts.headers;
|
||||
|
||||
match client.request(upstream_req).await {
|
||||
Ok(resp) => {
|
||||
let (parts, body) = resp.into_parts();
|
||||
let boxed_body = body.map_err(|e| Box::new(e) as BoxError).boxed();
|
||||
Response::from_parts(parts, boxed_body)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "tripoli-api injoignable");
|
||||
text_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("tripoli-api injoignable : {err}"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue