use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use bytes::{Bytes, BytesMut};
use claviron_http::policy::{Delivery, ResponsePolicy, X_ACCEL_BUFFERING};
use claviron_http::{RouteBody, RouteResponse, boxed_body, bytes_body};
use futures_util::stream;
use http::{HeaderMap, Response, StatusCode};
use http_body::Frame;
use http_body_util::StreamBody;
use tokio::sync::{mpsc, oneshot};
use tracing::debug;
use crate::engine::SAPI_SHUTTING_DOWN;
#[derive(Clone, Debug)]
pub(crate) struct CapturedResponse {
pub(crate) status: StatusCode,
pub(crate) headers: HeaderMap,
pub(crate) body: BytesMut,
}
impl Default for CapturedResponse {
fn default() -> Self {
Self {
status: StatusCode::OK,
headers: HeaderMap::new(),
body: BytesMut::new(),
}
}
}
impl CapturedResponse {
pub(in crate::engine) fn into_response(mut self) -> RouteResponse {
self.headers.remove(X_ACCEL_BUFFERING);
let mut response = Response::new(bytes_body(self.body.freeze()));
*response.status_mut() = self.status;
let dst = response.headers_mut();
for (name, value) in self.headers.iter() {
dst.append(name.clone(), value.clone());
}
response
}
pub(in crate::engine) fn mark_script_failure(&mut self) {
if self.status.as_u16() < 400 {
self.status = StatusCode::INTERNAL_SERVER_ERROR;
}
}
pub(in crate::engine) fn contains_php_fatal_output(&self) -> bool {
let body = self.body.as_ref();
body.windows(b"Fatal error".len())
.any(|window| window == b"Fatal error")
|| body
.windows(b"Parse error".len())
.any(|window| window == b"Parse error")
|| body
.windows(b"Compile Error".len())
.any(|window| window == b"Compile Error")
}
fn snapshot_head(&self) -> (StatusCode, HeaderMap) {
let mut headers = self.headers.clone();
headers.remove(X_ACCEL_BUFFERING);
(self.status, headers)
}
fn declared_length(&self) -> Option<u64> {
self.headers
.get(http::header::CONTENT_LENGTH)
.and_then(|value| value.to_str().ok())
.and_then(|value| value.trim().parse().ok())
}
}
pub(in crate::engine) const STREAM_BODY_CAPACITY: usize = 2;
pub(crate) enum BodySink {
Streaming(StreamingSink),
}
enum SinkState {
Undecided,
Holding { limit: Option<usize> },
Streaming,
}
pub(crate) struct StreamingSink {
policy: ResponsePolicy,
state: Mutex<SinkState>,
headers_tx: Mutex<Option<oneshot::Sender<(StatusCode, HeaderMap)>>>,
headers_committed: AtomicBool,
body_tx: mpsc::Sender<Bytes>,
write_deadline: Option<Duration>,
runtime: tokio::runtime::Handle,
shutdown: tokio_util::sync::CancellationToken,
}
impl StreamingSink {
pub(in crate::engine) fn new(
headers_tx: oneshot::Sender<(StatusCode, HeaderMap)>,
body_tx: mpsc::Sender<Bytes>,
write_deadline: Option<Duration>,
policy: ResponsePolicy,
) -> Self {
Self {
policy,
state: Mutex::new(SinkState::Undecided),
headers_tx: Mutex::new(Some(headers_tx)),
headers_committed: AtomicBool::new(false),
body_tx,
write_deadline,
runtime: tokio::runtime::Handle::current(),
shutdown: crate::engine::sapi_shutdown_signal(),
}
}
pub(in crate::engine) fn commit_headers(
&self,
captured: &Mutex<CapturedResponse>,
) {
if self.headers_committed.load(Ordering::Acquire) {
return;
}
let Ok(mut slot) = self.headers_tx.lock() else { return };
let Some(tx) = slot.take() else {
self.headers_committed.store(true, Ordering::Release);
return;
};
let head = match captured.lock() {
Ok(c) => c.snapshot_head(),
Err(_) => (StatusCode::OK, HeaderMap::new()),
};
let _ = tx.send(head);
self.headers_committed.store(true, Ordering::Release);
}
pub(in crate::engine) fn write(
&self,
output: &Mutex<CapturedResponse>,
data: &[u8],
) -> bool {
let Ok(mut state) = self.state.lock() else { return false };
if matches!(*state, SinkState::Undecided) {
*state = match output.lock() {
Ok(captured) => {
match self
.policy
.delivery(&captured.headers, captured.declared_length())
{
Delivery::Whole { up_to } => {
SinkState::Holding { limit: up_to }
}
Delivery::Pieces => SinkState::Streaming,
}
}
Err(_) => SinkState::Streaming,
};
}
if let SinkState::Holding { limit } = *state {
let Ok(mut captured) = output.lock() else { return false };
if limit
.is_none_or(|limit| captured.body.len() + data.len() <= limit)
{
captured.body.extend_from_slice(data);
return true;
}
let held = captured.body.split().freeze();
drop(captured);
*state = SinkState::Streaming;
drop(state);
self.commit_headers(output);
if !held.is_empty() && !self.push_body(held) {
return false;
}
return self.push_body(Bytes::copy_from_slice(data));
}
drop(state);
self.commit_headers(output);
self.push_body(Bytes::copy_from_slice(data))
}
pub(in crate::engine) fn flush(
&self,
output: &Mutex<CapturedResponse>,
) -> bool {
let Ok(mut state) = self.state.lock() else { return false };
if matches!(*state, SinkState::Streaming) {
return true;
}
*state = SinkState::Streaming;
drop(state);
let held = match output.lock() {
Ok(mut captured) => captured.body.split().freeze(),
Err(_) => return false,
};
self.commit_headers(output);
held.is_empty() || self.push_body(held)
}
pub(in crate::engine) fn finish(
&self,
output: &Mutex<CapturedResponse>,
) {
let streaming = self
.state
.lock()
.is_ok_and(|state| matches!(*state, SinkState::Streaming));
if streaming {
self.commit_headers(output);
}
}
pub(in crate::engine) fn push_body(
&self,
bytes: Bytes,
) -> bool {
if SAPI_SHUTTING_DOWN.load(Ordering::Acquire) {
return false;
}
let bytes = match self.body_tx.try_send(bytes) {
Ok(()) => return true,
Err(mpsc::error::TrySendError::Closed(_)) => return false,
Err(mpsc::error::TrySendError::Full(bytes)) => bytes,
};
let deadline = self.write_deadline;
self.runtime.block_on(async {
tokio::select! {
biased;
_ = self.shutdown.cancelled() => false,
sent = self.body_tx.send(bytes) => sent.is_ok(),
_ = async {
match deadline {
Some(deadline) => tokio::time::sleep(deadline).await,
None => std::future::pending().await
}
} => false,
}
})
}
}
struct StreamingBodyDropGuard {
finished: bool,
}
impl StreamingBodyDropGuard {
fn finish(mut self) {
self.finished = true;
}
}
impl Drop for StreamingBodyDropGuard {
fn drop(&mut self) {
if !self.finished {
debug!(
"streaming response body dropped before its end (client gone → body_rx drop)"
);
}
}
}
pub(in crate::engine) fn body_from_receiver(
rx: mpsc::Receiver<Bytes>
) -> RouteBody {
let guard = StreamingBodyDropGuard { finished: false };
let s = stream::unfold((rx, guard), |(mut rx, guard)| async move {
match rx.recv().await {
Some(chunk) => Some((
Ok::<_, std::convert::Infallible>(Frame::data(chunk)),
(rx, guard),
)),
None => {
guard.finish();
None
}
}
});
boxed_body(StreamBody::new(s))
}