use core::future::poll_fn;
use core::marker::PhantomData;
use core::pin::Pin;
use core::task::{Context, Poll};
use bytes::Bytes;
use foundation_netio::shared::client::body_reader::AsyncSendSafeBody;
use foundation_netio::shared::http::{SendSafeBody, SimpleHeader};
use foundation_netio::PreparedRequestBuilder;
use futures_core::Stream as FuturesStream;
use serde::de::DeserializeOwned;
use super::api_types::ApiError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BodyLimits {
pub max_error_body: usize,
pub max_json_line: usize,
}
impl Default for BodyLimits {
fn default() -> Self {
Self {
max_error_body: 64 * 1024,
max_json_line: 1024 * 1024,
}
}
}
impl BodyLimits {
#[must_use]
pub fn with_max_error_body(mut self, bytes: usize) -> Self {
self.max_error_body = bytes;
self
}
#[must_use]
pub fn with_max_json_line(mut self, bytes: usize) -> Self {
self.max_json_line = bytes;
self
}
}
pub async fn read_bounded(body: SendSafeBody, limit: usize) -> String {
let mut body = AsyncSendSafeBody::from(body);
let mut buf = Vec::new();
while buf.len() < limit {
match poll_fn(|cx| Pin::new(&mut body).poll_next(cx)).await {
Some(Ok(chunk)) => {
let room = limit - buf.len();
buf.extend_from_slice(&chunk[..chunk.len().min(room)]);
}
Some(Err(e)) => {
tracing::debug!("error response body ended early: {e}");
break;
}
None => break,
}
}
String::from_utf8_lossy(&buf).into_owned()
}
pub async fn read_error_body(body: SendSafeBody, limit: usize) -> Option<String> {
let text = read_bounded(body, limit).await;
(!text.is_empty()).then_some(text)
}
#[must_use]
pub fn with_raw_body(
mut builder: PreparedRequestBuilder,
media_type: &str,
body: SendSafeBody,
) -> PreparedRequestBuilder {
let length = match &body {
SendSafeBody::None => Some(0),
SendSafeBody::Text(text) => Some(text.len()),
SendSafeBody::Bytes(bytes) => Some(bytes.len()),
SendSafeBody::Stream(_)
| SendSafeBody::ChunkedStream(_)
| SendSafeBody::LineFeedStream(_)
| SendSafeBody::SseStream(_) => None,
};
builder = builder.header(SimpleHeader::CONTENT_TYPE, media_type);
if let Some(length) = length {
builder = builder.header(SimpleHeader::CONTENT_LENGTH, length.to_string());
}
builder.body(body)
}
fn body_read_failed(e: impl core::fmt::Display) -> ApiError {
ApiError::RequestSendFailed(format!("reading the response body: {e}"))
}
pub struct ByteStream {
body: Option<AsyncSendSafeBody>,
}
impl ByteStream {
#[must_use]
pub fn new(body: SendSafeBody) -> Self {
Self {
body: Some(AsyncSendSafeBody::from(body)),
}
}
pub async fn next(&mut self) -> Option<Result<Bytes, ApiError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
pub async fn drain(mut self) -> Result<u64, ApiError> {
let mut total = 0u64;
while let Some(chunk) = self.next().await {
total += chunk?.len() as u64;
}
Ok(total)
}
}
impl core::fmt::Debug for ByteStream {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ByteStream")
.field("ended", &self.body.is_none())
.finish_non_exhaustive()
}
}
impl FuturesStream for ByteStream {
type Item = Result<Bytes, ApiError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
let Some(body) = this.body.as_mut() else {
return Poll::Ready(None);
};
match Pin::new(body).poll_next(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(Bytes::from(chunk)))),
Poll::Ready(Some(Err(e))) => {
this.body = None;
Poll::Ready(Some(Err(body_read_failed(e))))
}
Poll::Ready(None) => {
this.body = None;
Poll::Ready(None)
}
}
}
}
pub struct JsonLinesStream<T> {
bytes: ByteStream,
buf: Vec<u8>,
start: usize,
scanned: usize,
max_line: usize,
done: bool,
_item: PhantomData<fn() -> T>,
}
impl<T: DeserializeOwned> JsonLinesStream<T> {
#[must_use]
pub fn new(body: SendSafeBody, max_line: usize) -> Self {
Self {
bytes: ByteStream::new(body),
buf: Vec::new(),
start: 0,
scanned: 0,
max_line,
done: false,
_item: PhantomData,
}
}
pub async fn next(&mut self) -> Option<Result<T, ApiError>> {
poll_fn(|cx| Pin::new(&mut *self).poll_next(cx)).await
}
fn take_line(&mut self) -> Option<Vec<u8>> {
let newline = self.buf[self.scanned..].iter().position(|b| *b == b'\n')?;
let end = self.scanned + newline;
let line = self.buf[self.start..end].to_vec();
self.start = end + 1;
self.scanned = self.start;
if self.start > self.buf.len() / 2 {
self.buf.drain(..self.start);
self.start = 0;
self.scanned = 0;
}
Some(line)
}
fn line_too_long(&mut self) -> ApiError {
self.done = true;
ApiError::ParseFailed(format!(
"response line longer than {} bytes",
self.max_line
))
}
fn parse(&mut self, line: &[u8]) -> Result<T, ApiError> {
if line.len() > self.max_line {
return Err(self.line_too_long());
}
serde_json::from_slice(line).map_err(|e| {
self.done = true;
ApiError::ParseFailed(format!(
"response line {:?}: {e}",
String::from_utf8_lossy(&line[..line.len().min(256)])
))
})
}
}
impl<T> core::fmt::Debug for JsonLinesStream<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("JsonLinesStream")
.field("item", &core::any::type_name::<T>())
.field("buffered", &(self.buf.len() - self.start))
.field("max_line", &self.max_line)
.field("done", &self.done)
.finish_non_exhaustive()
}
}
impl<T: DeserializeOwned> FuturesStream for JsonLinesStream<T> {
type Item = Result<T, ApiError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if this.done {
return Poll::Ready(None);
}
if let Some(line) = this.take_line() {
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
return Poll::Ready(Some(this.parse(&line)));
}
this.scanned = this.buf.len();
if this.buf.len() - this.start > this.max_line {
return Poll::Ready(Some(Err(this.line_too_long())));
}
match Pin::new(&mut this.bytes).poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(Ok(chunk))) => this.buf.extend_from_slice(&chunk),
Poll::Ready(Some(Err(e))) => {
this.done = true;
return Poll::Ready(Some(Err(e)));
}
Poll::Ready(None) => {
this.done = true;
let rest = this.buf.split_off(this.start);
this.buf.clear();
this.start = 0;
this.scanned = 0;
if rest.iter().all(u8::is_ascii_whitespace) {
return Poll::Ready(None);
}
return Poll::Ready(Some(this.parse(&rest)));
}
}
}
}
}