Skip to main content

netget/server/saml_idp/
mod.rs

1//! SAML Identity Provider (IDP) server implementation
2//!
3//! This module implements a SAML 2.0 Identity Provider that authenticates users
4//! and generates signed SAML assertions. The LLM controls authentication decisions,
5//! user attributes, and assertion generation.
6
7pub mod actions;
8
9use std::convert::Infallible;
10use std::net::SocketAddr;
11use std::sync::Arc;
12
13use bytes::Bytes;
14use http_body_util::{BodyExt, Full};
15use hyper::body::{Body, Incoming};
16use hyper::server::conn::http1;
17use hyper::service::service_fn;
18use hyper::{Request, Response, StatusCode};
19use hyper_util::rt::TokioIo;
20use tokio::io::AsyncWriteExt;
21use tokio::sync::mpsc;
22use tracing::{debug, error, trace, warn};
23
24use crate::llm::action_helper::call_llm;
25use crate::llm::ollama_client::OllamaClient;
26use crate::logging::emit::Log;
27use crate::protocol::Event;
28use crate::server::connection::ConnectionId;
29use crate::server::SamlIdpProtocol;
30use crate::state::app_state::AppState;
31use actions::SAML_IDP_REQUEST_EVENT;
32
33/// Largest request body this server will buffer.
34///
35/// `/acs`, `/sso` and every other route here is reachable with no credential at all, and the
36/// body is handed to the model verbatim as prompt text, so the previous unbounded
37/// `req.collect()` let one anonymous POST grow the process without limit and drive an LLM
38/// call with megabytes of attacker-chosen prompt. A base64 SAMLResponse form is a few tens of
39/// KiB; 256 KiB is generous for even a large assertion with attributes.
40pub const MAX_REQUEST_BYTES: usize = 256 * 1024;
41
42/// How long a peer that has connected and produced nothing may hold a slot.
43///
44/// HTTP is client-speaks-first: the server says nothing until a request line arrives, so a peer
45/// that has completed the TCP handshake and sent no byte has asked nothing and negotiated
46/// nothing. That state carries no protocol yet, which is why this number is the same across
47/// netget's HTTP-shaped servers while the idle bound below is not. Apache's `mod_reqtimeout`
48/// gives the request header 20s and nginx's `client_header_timeout` 60s; 30s sits between the
49/// two deployed norms.
50///
51/// hyper's own `header_read_timeout` is **not** this bound: its 30-second default is inert
52/// unless `http1::Builder::timer` is also set, which nothing here does — hyper downgrades a
53/// defaulted duration to `None` when no timer is present and applies no deadline at all.
54const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
55
56/// How long a connection with nothing outstanding may sit idle between requests.
57///
58/// These endpoints are reached two ways and the two pull in opposite directions. A **browser**
59/// does one redirect round and never comes back on that connection, which is the case Apache's
60/// `KeepAliveTimeout` default of 5s is tuned for. A **relying party's back-channel** client
61/// (token exchange, introspection, a JWKS or metadata fetch) reuses a pooled connection, which
62/// is the case nginx's 75-second `keepalive_timeout` is tuned for. 60s is comfortably past any
63/// pooled back-channel round trip while not letting a browser that navigated away hold a slot
64/// for minutes. Nothing here streams or long-polls, so no legitimate request sits open.
65///
66/// **The five-minute numbers SAML and OIDC do quote are not this number.** An assertion's
67/// `NotOnOrAfter` and an `id_token`'s `exp` bound how long a *credential* may be presented, not
68/// how long a socket may be silent; copying one into the other is the mistake this comment
69/// exists to prevent. And a request still being answered is not silence either: the watchdog
70/// reads [`ConnectionActivity`](crate::server::accept_bounded::ConnectionActivity), which
71/// reports a connection with work in flight — a model round-trip, or an event a `manual` rule
72/// parked for a human — as not idle at all.
73const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
74
75/// Concurrent connections this server admits.
76///
77/// The shared default. Each admitted connection may buffer one body of up to
78/// [`MAX_REQUEST_BYTES`] (256 KiB), so the cap is what turns that per-connection bound into a total
79/// one and holds the worst case well inside the ~1 GiB ceiling netget's HTTP family is held
80/// to. A protocol only declares a smaller number when its per-connection cost is larger.
81const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
82
83/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
84///
85/// `503 Service Unavailable` with a `Retry-After`, written directly onto the socket because the
86/// peer has not sent a request line for hyper to answer. Fixed bytes: nothing derived from an
87/// error reaches the wire (see `crate::utils::wire_failure`).
88const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
89    Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
90
91/// Narrow a model-supplied HTTP status to `u16` without wrapping.
92///
93/// `status as u16` on a `u64` truncates, and the truncation is the dangerous direction here:
94/// `65736` becomes `200`, and a 2xx is the only thing an SP treats as a completed sign-in. A
95/// status the model cannot have meant must not become the one that admits someone.
96fn status_or(value: Option<&serde_json::Value>, default: u16) -> u16 {
97    match value.and_then(|v| v.as_u64()) {
98        Some(raw) => u16::try_from(raw)
99            .ok()
100            .filter(|s| (100..=599).contains(s))
101            .unwrap_or_else(|| {
102                warn!("SAML IDP: ignoring out-of-range status {raw}, using {default}");
103                default
104            }),
105        None => default,
106    }
107}
108
109/// Build a response from parts that came from the model, without ever panicking.
110///
111/// `status`, and every response header, arrive as model output (`send_error_response` even
112/// documents `status_code` as a parameter). The previous code did
113/// `Response::builder().status(status as u16)…body(..).unwrap()`, so a `status_code` of
114/// 1000 — or a header value containing CR/LF — panicked inside the connection task instead
115/// of answering. Local copy of `http_common::handler::build_safe_response`, which the
116/// `saml-idp` feature cannot reach because `http_common` is gated on `feature = "http"`.
117fn build_safe_response(
118    status: u16,
119    headers: impl IntoIterator<Item = (String, String)>,
120    body: String,
121) -> Response<Full<Bytes>> {
122    let status_code = StatusCode::from_u16(status).unwrap_or_else(|_| {
123        error!("SAML IDP: invalid HTTP status {status}, sending 500 instead");
124        StatusCode::INTERNAL_SERVER_ERROR
125    });
126
127    let mut builder = Response::builder().status(status_code);
128    for (name, value) in headers {
129        match (
130            hyper::header::HeaderName::from_bytes(name.as_bytes()),
131            hyper::header::HeaderValue::from_str(&value),
132        ) {
133            (Ok(n), Ok(v)) => builder = builder.header(n, v),
134            _ => warn!("SAML IDP: dropping invalid response header {name:?}"),
135        }
136    }
137
138    builder
139        .body(Full::new(Bytes::from(body)))
140        .unwrap_or_else(|e| {
141            error!("SAML IDP: failed to build response ({e}), sending bare 500");
142            let mut fallback =
143                Response::new(Full::new(Bytes::from_static(b"Internal Server Error")));
144            *fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
145            fallback
146        })
147}
148
149/// The one reply this server sends when it cannot produce a SAML Response.
150///
151/// Never an assertion, never a 2xx: the only thing an SP accepts as a sign-in is a 2xx
152/// carrying a `SAMLResponse` form, so failing with anything else is failing closed. The two
153/// [`crate::utils::WireFailure`] categories map onto distinct HTTP codes — 503 + `Retry-After`
154/// for a saturated backend so the client backs off, 500 otherwise so it records a fault —
155/// and the body is `WireFailure`'s `&'static str`, which cannot carry the error.
156fn fail_closed_response(failure: crate::utils::WireFailure) -> Response<Full<Bytes>> {
157    let (status, headers): (u16, Vec<(String, String)>) = if failure.is_overloaded() {
158        (
159            503,
160            vec![
161                (
162                    "content-type".to_string(),
163                    "text/plain; charset=utf-8".to_string(),
164                ),
165                ("retry-after".to_string(), "1".to_string()),
166            ],
167        )
168    } else {
169        (
170            500,
171            vec![(
172                "content-type".to_string(),
173                "text/plain; charset=utf-8".to_string(),
174            )],
175        )
176    };
177
178    build_safe_response(status, headers, failure.prefixed_text().to_string())
179}
180
181/// SAML IDP server that delegates authentication and assertion generation to LLM
182pub struct SamlIdpServer;
183
184impl SamlIdpServer {
185    /// Spawn the SAML IDP server with LLM-controlled authentication
186    pub async fn spawn_with_llm_actions(
187        listen_addr: SocketAddr,
188        llm_client: OllamaClient,
189        app_state: Arc<AppState>,
190        status_tx: mpsc::UnboundedSender<String>,
191        server_id: crate::state::ServerId,
192    ) -> anyhow::Result<SocketAddr> {
193        let listener =
194            crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
195        let local_addr = listener.local_addr()?;
196
197        Log::new(Some(&status_tx)).info(format!("SAML IDP server listening on {}", local_addr));
198
199        let protocol = Arc::new(SamlIdpProtocol::new());
200
201        // Spawn server loop
202        let task_registrar = app_state.clone();
203        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
204        let accept_handle = tokio::spawn(async move {
205            loop {
206                match crate::server::accept_bounded::accept_bounded(
207                    &listener,
208                    &limiter,
209                    CONNECTION_CAP_REFUSAL,
210                    "SAML IDP",
211                    Some(&status_tx),
212                )
213                .await
214                {
215                    Ok((stream, remote_addr, permit)) => {
216                        let connection_id =
217                            ConnectionId::new(app_state.get_next_unified_id().await);
218                        let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
219
220                        Log::new(Some(&status_tx)).info(format!(
221                            "Accepted SAML IDP connection {} from {}",
222                            connection_id, remote_addr
223                        ));
224
225                        let status_tx_for_task = status_tx.clone();
226
227                        // Add connection to ServerInstance
228                        use crate::state::server::{
229                            ConnectionState as ServerConnectionState, ConnectionStatus,
230                            ProtocolConnectionInfo,
231                        };
232                        let now = crate::utils::clock::Instant::now();
233                        let conn_state = ServerConnectionState {
234                            id: connection_id,
235                            remote_addr,
236                            local_addr: local_addr_conn,
237                            bytes_sent: 0,
238                            bytes_received: 0,
239                            packets_sent: 0,
240                            packets_received: 0,
241                            last_activity: now,
242                            status: ConnectionStatus::Active,
243                            status_changed_at: now,
244                            protocol_info: ProtocolConnectionInfo::empty(),
245                        };
246                        app_state
247                            .add_connection_to_server(server_id, conn_state)
248                            .await;
249                        let _ = status_tx.send("__UPDATE_UI__".to_string());
250
251                        let llm_client_clone = llm_client.clone();
252                        let app_state_clone = app_state.clone();
253                        let protocol_clone = protocol.clone();
254
255                        // Spawn a task to handle this connection
256                        // Tracked, not detached: stop_server must abort this task too.
257                        let task_owner = app_state.clone();
258                        task_owner
259                            .spawn_server_task(server_id, async move {
260                                let status_tx = status_tx_for_task;
261                                // Held for the life of the connection, so the cap counts live
262                                // clients rather than accepts.
263                                let _permit = permit;
264
265                                // First-byte bound, before hyper sees the socket.
266                                //
267                                // hyper owns every read once `serve_connection` starts, and a
268                                // deadline on those reads would be wrong here rather than merely
269                                // awkward: hyper keeps polling the connection for more input
270                                // while a request is being answered, so such a deadline would
271                                // fire in the middle of a model round-trip. `peek` waits for data
272                                // without consuming it, so the request line is still there for
273                                // hyper afterwards, and it bounds exactly the case that needs
274                                // bounding - a peer that has connected and sent nothing at all.
275                                let spoke = matches!(
276                                    tokio::time::timeout(
277                                        FIRST_BYTE_READ_TIMEOUT,
278                                        stream.peek(&mut [0u8; 1]),
279                                    )
280                                    .await,
281                                    Ok(Ok(n)) if n > 0
282                                );
283
284                                let io = TokioIo::new(stream);
285
286                                // Clone for service_fn closure
287                                let llm_for_service = llm_client_clone.clone();
288                                let state_for_service = app_state_clone.clone();
289                                let status_for_service = status_tx.clone();
290                                let protocol_for_service = protocol_clone.clone();
291
292                                // Tracks whether this connection is answering anything. A
293                                // request waiting on the model, or parked for a human by a
294                                // `manual` rule, holds the count above zero, so the idle watchdog
295                                // below cannot close the connection the answer belongs to however
296                                // long it takes - only genuine silence counts.
297                                let activity = std::sync::Arc::new(
298                                    crate::server::accept_bounded::ConnectionActivity::new(),
299                                );
300                                let activity_for_service = std::sync::Arc::clone(&activity);
301
302                                // Create a service that handles SAML IDP requests with LLM
303                                let service = service_fn(move |req: Request<Incoming>| {
304                                    let llm_clone = llm_for_service.clone();
305                                    let state_clone = state_for_service.clone();
306                                    let status_clone = status_for_service.clone();
307                                    let protocol_clone = protocol_for_service.clone();
308                                    let activity = std::sync::Arc::clone(&activity_for_service);
309                                    async move {
310                                        let _busy = activity.busy();
311                                        handle_saml_idp_request(
312                                            req,
313                                            connection_id,
314                                            server_id,
315                                            remote_addr,
316                                            llm_clone,
317                                            state_clone,
318                                            status_clone,
319                                            protocol_clone,
320                                        )
321                                        .await
322                                    }
323                                });
324
325                                // Serve HTTP/1 on this connection, bounded at both ends.
326                                if !spoke {
327                                    Log::new(Some(&status_tx)).debug(format!(
328                                        "SAML IDP peer {} sent nothing for {}s; closing before \
329                                         any request",
330                                        remote_addr,
331                                        FIRST_BYTE_READ_TIMEOUT.as_secs()
332                                    ));
333                                } else {
334                                    // Keep the socket after hyper flushes the response. An oversized
335                                    // upload can still be sending when the 413 is ready.
336                                    let conn = http1::Builder::new()
337                                        .serve_connection(io, service)
338                                        .without_shutdown();
339                                    tokio::pin!(conn);
340                                    tokio::select! {
341                                        result = &mut conn => {
342                                            match result {
343                                                Ok(parts) => {
344                                                    let mut stream = parts.io.into_inner();
345                                                    // Send EOF after the response, then discard
346                                                    // the upload tail within shared bounds.
347                                                    let _ = stream.shutdown().await;
348                                                    crate::server::accept_bounded::drain_after_response(&mut stream).await;
349                                                }
350                                                Err(err) => error!(
351                                                    "Error serving SAML IDP connection \
352                                                     {}: {}",
353                                                    connection_id,
354                                                    err
355                                                ),
356                                            }
357                                        }
358                                        _ = crate::server::accept_bounded::watch_idle(
359                                            std::sync::Arc::clone(&activity),
360                                            IDLE_BETWEEN_REQUESTS_TIMEOUT,
361                                        ) => {
362                                            Log::new(Some(&status_tx)).debug(format!(
363                                                "SAML IDP connection {} idle for {}s; closing",
364                                                connection_id,
365                                                IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
366                                            ));
367                                        }
368                                    }
369                                }
370
371                                // Remove connection when done
372                                debug!("SAML IDP connection {} closed", connection_id);
373                                app_state_clone
374                                    .remove_connection_from_server(server_id, connection_id)
375                                    .await;
376                                let _ = status_tx.send("__UPDATE_UI__".to_string());
377                            })
378                            .await;
379                    }
380                    Err(e) => {
381                        Log::new(Some(&status_tx))
382                            .error(format!("Failed to accept SAML IDP connection: {}", e));
383                    }
384                }
385            }
386        });
387
388        // Register the accept loop so stop_server can abort it and release the port.
389        task_registrar
390            .register_server_task(server_id, accept_handle)
391            .await;
392
393        Ok(local_addr)
394    }
395}
396
397/// Handle a SAML IDP request with LLM decision making
398async fn handle_saml_idp_request(
399    req: Request<Incoming>,
400    connection_id: ConnectionId,
401    server_id: crate::state::ServerId,
402    remote_addr: SocketAddr,
403    llm_client: OllamaClient,
404    app_state: Arc<AppState>,
405    status_tx: mpsc::UnboundedSender<String>,
406    protocol: Arc<SamlIdpProtocol>,
407) -> Result<Response<Full<Bytes>>, Infallible> {
408    let method = req.method().clone();
409    let path = req.uri().path().to_string();
410    let query = req.uri().query().map(|q| q.to_string());
411
412    Log::new(Some(&status_tx)).debug(format!("SAML IDP {} {} from {}", method, path, remote_addr));
413
414    // Extract headers
415    let headers: Vec<(String, String)> = req
416        .headers()
417        .iter()
418        .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
419        .collect();
420
421    // Read request body
422    // Bounded, and refused rather than truncated: a cut-off SAMLResponse would reach the
423    // model as a well-formed request whose assertion happened to end early.
424    let body_bytes = match http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BYTES)
425        .collect()
426        .await
427    {
428        Ok(collected) => collected.to_bytes().to_vec(),
429        Err(e) => {
430            Log::new(Some(&status_tx)).warn(format!(
431                "SAML IDP {} {} decision=fail_closed_body_rejected (limit {} bytes): {}",
432                method, path, MAX_REQUEST_BYTES, e
433            ));
434            return Ok(build_safe_response(
435                413,
436                [(
437                    "content-type".to_string(),
438                    "text/plain; charset=utf-8".to_string(),
439                )],
440                "request body too large".to_string(),
441            ));
442        }
443    };
444
445    // TRACE level for full payloads
446    if !body_bytes.is_empty() {
447        trace!("SAML IDP request body: {} bytes", body_bytes.len());
448        if let Ok(body_str) = String::from_utf8(body_bytes.clone()) {
449            trace!("SAML IDP request body content: {}", body_str);
450        }
451    }
452
453    // Update connection stats
454    app_state
455        .update_connection_stats(
456            server_id,
457            connection_id,
458            Some(body_bytes.len() as u64),
459            None,
460            None,
461            None,
462        )
463        .await;
464
465    // Build event for LLM
466    let event = Event::new(
467        &SAML_IDP_REQUEST_EVENT,
468        serde_json::json!({
469            "method": method.to_string(),
470            "path": path,
471            "query": query,
472            "headers": headers,
473            "body": if body_bytes.is_empty() {
474                serde_json::Value::Null
475            } else if let Ok(body_str) = String::from_utf8(body_bytes.clone()) {
476                serde_json::Value::String(body_str)
477            } else {
478                // Never base64 in event data: a model cannot decode it, and this body is
479                // always text on a working SAML binding (the assertion's own base64 arrives
480                // inside a urlencoded form field). Say what happened instead.
481                serde_json::Value::String(format!(
482                    "<{} bytes of non-UTF-8 data; not a SAML binding this server understands>",
483                    body_bytes.len()
484                ))
485            },
486            "client_ip": remote_addr.ip().to_string(),
487        }),
488    );
489
490    // Call LLM for decision
491    debug!("Calling LLM for SAML IDP request decision");
492    let action_result = call_llm(
493        &llm_client,
494        &app_state,
495        server_id,
496        Some(connection_id),
497        &event,
498        protocol.as_ref(),
499    )
500    .await;
501
502    // Execute actions and build response
503    let response = match action_result {
504        Ok(result) => {
505            if result.protocol_results.is_empty() {
506                // The model answered, but with nothing this handler can put on the wire.
507                // Distinct in the log from an LLM *error* and from an explicit rejection.
508                warn!(
509                    "SAML IDP {} {} decision=fail_closed_no_action: model produced no actions",
510                    method, path
511                );
512                Log::new(Some(&status_tx)).warn(format!(
513                    "SAML IDP {} {}: model produced no actions",
514                    method, path
515                ));
516                fail_closed_response(crate::utils::WireFailure::Unavailable)
517            } else {
518                // Parse HTTP response from protocol results
519                use crate::llm::actions::protocol_trait::ActionResult;
520
521                let mut status_code = 200u16;
522                let mut response_headers = std::collections::HashMap::new();
523                let mut response_body = String::new();
524                // Did any action actually yield a response-shaped Output? Without this the
525                // loop below silently left status 200 with an empty body whenever the model's
526                // output was not JSON, or was JSON without status/headers/body — an empty
527                // 200 is not a sign-in any SP accepts, so it is a fail-open in disguise.
528                let mut produced_response = false;
529
530                for protocol_result in result.protocol_results {
531                    if let ActionResult::Output(output_data) = protocol_result {
532                        // Parse JSON response data
533                        if let Ok(json_value) =
534                            serde_json::from_slice::<serde_json::Value>(&output_data)
535                        {
536                            if json_value.get("status").is_some() {
537                                status_code = status_or(json_value.get("status"), 200);
538                                produced_response = true;
539                            }
540                            if let Some(headers_obj) =
541                                json_value.get("headers").and_then(|v| v.as_object())
542                            {
543                                for (k, v) in headers_obj {
544                                    if let Some(v_str) = v.as_str() {
545                                        response_headers.insert(k.clone(), v_str.to_string());
546                                    }
547                                }
548                            }
549                            if let Some(body) = json_value.get("body").and_then(|v| v.as_str()) {
550                                response_body = body.to_string();
551                                produced_response = true;
552                            }
553                            // Headers alone deliberately do NOT count. Every action this
554                            // protocol defines sets a status and a body; JSON carrying only
555                            // headers left the default 200 with an empty body standing, which
556                            // is the fail-open the `produced_response` flag exists to prevent.
557                        }
558                    }
559                }
560
561                if !produced_response {
562                    warn!(
563                        "SAML IDP {} {} decision=fail_closed_unusable_output: \
564                         actions ran but none yielded a response",
565                        method, path
566                    );
567                    Log::new(Some(&status_tx)).warn(format!(
568                        "SAML IDP {} {}: actions ran but none yielded a response",
569                        method, path
570                    ));
571                    fail_closed_response(crate::utils::WireFailure::Unavailable)
572                } else {
573                    // The model answered. A 4xx/5xx it chose itself is its own refusal
574                    // (`send_error_response`) and must stay distinguishable in the log from
575                    // the two fail-closed paths above.
576                    let decision = if status_code >= 400 {
577                        "model_reject"
578                    } else {
579                        "model_answer"
580                    };
581                    debug!(
582                        "SAML IDP {} {} decision={} status={}",
583                        method, path, decision, status_code
584                    );
585                    build_safe_response(status_code, response_headers, response_body)
586                }
587            }
588        }
589        Err(e) => {
590            // SAML rides on HTTP here, and the failure is ours rather than the peer's, so it
591            // is a 5xx: 503 + Retry-After while the backend is saturated so the peer backs
592            // off and retries, 500 otherwise. Critically it is not a SAML Response at all - a
593            // 2xx carrying an assertion is the only thing an SP will accept as a sign-in, and
594            // no branch on this path can produce one.
595            //
596            // Only the *category* reaches the socket; the error itself goes to the log and
597            // the status stream, where an operator looks.
598            let failure = crate::utils::WireFailure::classify(&e);
599            let category = if failure.is_overloaded() {
600                "overloaded"
601            } else {
602                "unavailable"
603            };
604            error!(
605                "SAML IDP {} {} decision=fail_closed_llm_error category={}: {}",
606                method, path, category, e
607            );
608            Log::new(Some(&status_tx)).error(format!(
609                "LLM error for SAML IDP {} {} (category={}): {}",
610                method, path, category, e
611            ));
612            fail_closed_response(failure)
613        }
614    };
615
616    // Update bytes sent
617    let response_size = response.body().size_hint().exact().unwrap_or(0);
618    app_state
619        .update_connection_stats(
620            server_id,
621            connection_id,
622            None,
623            Some(response_size),
624            None,
625            None,
626        )
627        .await;
628
629    debug!(
630        "SAML IDP response: {} ({} bytes)",
631        response.status(),
632        response_size
633    );
634
635    Ok(response)
636}