Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fancy multipart form bodies #375

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ default = ["http2", "static-curl", "text-decoding"]
cookies = ["httpdate"]
http2 = ["curl/http2"]
json = ["serde", "serde_json"]
multipart = ["fastrand"]
nightly = []
psl = ["httpdate", "parking_lot", "publicsuffix"]
spnego = ["curl-sys/spnego"]
Expand Down Expand Up @@ -53,6 +54,10 @@ waker-fn = "1"
version = "0.8"
optional = true

[dependencies.fastrand]
version = "1"
optional = true

[dependencies.httpdate]
version = "1"
optional = true
Expand Down
12 changes: 12 additions & 0 deletions examples/multipart_form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use isahc::{forms::FormDataBuilder, prelude::*, Body};

fn main() -> Result<(), isahc::Error> {
let form = FormDataBuilder::<Body>::new().field("foo", "bar").build();

let mut response = isahc::post("https://httpbin.org/post", form)?;

println!("{:?}", response);
print!("{}", response.text()?);

Ok(())
}
88 changes: 60 additions & 28 deletions src/body/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Provides types for working with request and response bodies.

use futures_lite::io::{AsyncRead, BlockOn};
use http::HeaderValue;
use std::{
borrow::Cow,
fmt,
Expand Down Expand Up @@ -28,10 +29,13 @@ pub use sync::Body;
/// implements.
///
/// For synchronous requests, use [`Body`] instead.
pub struct AsyncBody(Inner);
pub struct AsyncBody {
content_type: Option<HeaderValue>,
repr: Repr,
}

/// All possible body implementations.
enum Inner {
enum Repr {
/// An empty body.
Empty,

Expand All @@ -48,7 +52,10 @@ impl AsyncBody {
/// An empty body represents the *absence* of a body, which is semantically
/// different than the presence of a body of zero length.
pub const fn empty() -> Self {
Self(Inner::Empty)
Self {
content_type: None,
repr: Repr::Empty,
}
}

/// Create a new body from a potentially static byte buffer.
Expand Down Expand Up @@ -76,7 +83,10 @@ impl AsyncBody {
B: AsRef<[u8]> + 'static,
{
castaway::match_type!(bytes, {
Cursor<Cow<'static, [u8]>> as bytes => Self(Inner::Buffer(bytes)),
Cursor<Cow<'static, [u8]>> as bytes => Self {
content_type: None,
repr: Repr::Buffer(bytes),
},
&'static [u8] as bytes => Self::from_static_impl(bytes),
&'static str as bytes => Self::from_static_impl(bytes.as_bytes()),
Vec<u8> as bytes => Self::from(bytes),
Expand All @@ -87,7 +97,10 @@ impl AsyncBody {

#[inline]
fn from_static_impl(bytes: &'static [u8]) -> Self {
Self(Inner::Buffer(Cursor::new(Cow::Borrowed(bytes))))
Self {
content_type: None,
repr: Repr::Buffer(Cursor::new(Cow::Borrowed(bytes))),
}
}

/// Create a streaming body that reads from the given reader.
Expand All @@ -100,7 +113,10 @@ impl AsyncBody {
where
R: AsyncRead + Send + Sync + 'static,
{
Self(Inner::Reader(Box::pin(read), None))
Self {
content_type: None,
repr: Repr::Reader(Box::pin(read), None),
}
}

/// Create a streaming body with a known length.
Expand All @@ -116,7 +132,15 @@ impl AsyncBody {
where
R: AsyncRead + Send + Sync + 'static,
{
Self(Inner::Reader(Box::pin(read), Some(length)))
Self {
content_type: None,
repr: Repr::Reader(Box::pin(read), Some(length)),
}
}

pub(crate) fn with_content_type(mut self, content_type: Option<HeaderValue>) -> Self {
self.content_type = content_type;
self
}

/// Report if this body is empty.
Expand All @@ -126,8 +150,8 @@ impl AsyncBody {
/// difference between the absence of a body and the presence of a
/// zero-length body. This method will only return `true` for the former.
pub fn is_empty(&self) -> bool {
match self.0 {
Inner::Empty => true,
match self.repr {
Repr::Empty => true,
_ => false,
}
}
Expand All @@ -146,23 +170,28 @@ impl AsyncBody {
/// bytes, even if a value is returned it should not be relied on as always
/// being accurate, and should be treated as a "hint".
pub fn len(&self) -> Option<u64> {
match &self.0 {
Inner::Empty => Some(0),
Inner::Buffer(bytes) => Some(bytes.get_ref().len() as u64),
Inner::Reader(_, len) => *len,
match &self.repr {
Repr::Empty => Some(0),
Repr::Buffer(bytes) => Some(bytes.get_ref().len() as u64),
Repr::Reader(_, len) => *len,
}
}

/// Get the content type of this body, if any.
pub(crate) fn content_type(&self) -> Option<&HeaderValue> {
self.content_type.as_ref()
}

/// If this body is repeatable, reset the body stream back to the start of
/// the content. Returns `false` if the body cannot be reset.
pub fn reset(&mut self) -> bool {
match &mut self.0 {
Inner::Empty => true,
Inner::Buffer(cursor) => {
match &mut self.repr {
Repr::Empty => true,
Repr::Buffer(cursor) => {
cursor.set_position(0);
true
}
Inner::Reader(_, _) => false,
Repr::Reader(_, _) => false,
}
}

Expand All @@ -174,14 +203,14 @@ impl AsyncBody {
/// generally if the underlying reader only supports blocking under a
/// specific runtime.
pub(crate) fn into_sync(self) -> sync::Body {
match self.0 {
Inner::Empty => sync::Body::empty(),
Inner::Buffer(cursor) => sync::Body::from_bytes_static(cursor.into_inner()),
Inner::Reader(reader, Some(len)) => {
match self.repr {
Repr::Empty => sync::Body::empty(),
Repr::Buffer(cursor) => sync::Body::from_bytes_static(cursor.into_inner()),
Repr::Reader(reader, Some(len)) => {
sync::Body::from_reader_sized(BlockOn::new(reader), len)
}
Inner::Reader(reader, None) => sync::Body::from_reader(BlockOn::new(reader)),
}
Repr::Reader(reader, None) => sync::Body::from_reader(BlockOn::new(reader)),
}.with_content_type(self.content_type)
}
}

Expand All @@ -191,10 +220,10 @@ impl AsyncRead for AsyncBody {
cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
match &mut self.0 {
Inner::Empty => Poll::Ready(Ok(0)),
Inner::Buffer(cursor) => Poll::Ready(cursor.read(buf)),
Inner::Reader(read, _) => AsyncRead::poll_read(read.as_mut(), cx, buf),
match &mut self.repr {
Repr::Empty => Poll::Ready(Ok(0)),
Repr::Buffer(cursor) => Poll::Ready(cursor.read(buf)),
Repr::Reader(read, _) => AsyncRead::poll_read(read.as_mut(), cx, buf),
}
}
}
Expand All @@ -213,7 +242,10 @@ impl From<()> for AsyncBody {

impl From<Vec<u8>> for AsyncBody {
fn from(body: Vec<u8>) -> Self {
Self(Inner::Buffer(Cursor::new(Cow::Owned(body))))
Self {
content_type: None,
repr: Repr::Buffer(Cursor::new(Cow::Owned(body))),
}
}
}

Expand Down
82 changes: 57 additions & 25 deletions src/body/sync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::AsyncBody;
use futures_lite::{future::yield_now, io::AsyncWriteExt};
use http::HeaderValue;
use sluice::pipe::{pipe, PipeWriter};
use std::{
borrow::Cow,
Expand All @@ -17,9 +18,12 @@ use std::{
/// implements [`Read`], which [`Body`] itself also implements.
///
/// For asynchronous requests, use [`AsyncBody`] instead.
pub struct Body(Inner);
pub struct Body {
content_type: Option<HeaderValue>,
repr: Repr,
}

enum Inner {
enum Repr {
Empty,
Buffer(Cursor<Cow<'static, [u8]>>),
Reader(Box<dyn Read + Send + Sync>, Option<u64>),
Expand All @@ -31,7 +35,10 @@ impl Body {
/// An empty body represents the *absence* of a body, which is semantically
/// different than the presence of a body of zero length.
pub const fn empty() -> Self {
Self(Inner::Empty)
Self {
content_type: None,
repr: Repr::Empty,
}
}

/// Create a new body from a potentially static byte buffer.
Expand Down Expand Up @@ -59,7 +66,10 @@ impl Body {
B: AsRef<[u8]> + 'static,
{
castaway::match_type!(bytes, {
Cursor<Cow<'static, [u8]>> as bytes => Self(Inner::Buffer(bytes)),
Cursor<Cow<'static, [u8]>> as bytes => Self {
content_type: None,
repr: Repr::Buffer(bytes),
},
Vec<u8> as bytes => Self::from(bytes),
String as bytes => Self::from(bytes.into_bytes()),
bytes => Self::from(bytes.as_ref().to_vec()),
Expand All @@ -76,7 +86,10 @@ impl Body {
where
R: Read + Send + Sync + 'static,
{
Self(Inner::Reader(Box::new(reader), None))
Self {
content_type: None,
repr: Repr::Reader(Box::new(reader), None),
}
}

/// Create a streaming body with a known length.
Expand All @@ -92,7 +105,16 @@ impl Body {
where
R: Read + Send + Sync + 'static,
{
Self(Inner::Reader(Box::new(reader), Some(length)))

Self {
content_type: None,
repr: Repr::Reader(Box::new(reader), Some(length)),
}
}

pub(crate) fn with_content_type(mut self, content_type: Option<HeaderValue>) -> Self {
self.content_type = content_type;
self
}

/// Report if this body is empty.
Expand All @@ -102,8 +124,8 @@ impl Body {
/// difference between the absence of a body and the presence of a
/// zero-length body. This method will only return `true` for the former.
pub fn is_empty(&self) -> bool {
match self.0 {
Inner::Empty => true,
match self.repr {
Repr::Empty => true,
_ => false,
}
}
Expand All @@ -122,19 +144,24 @@ impl Body {
/// bytes, even if a value is returned it should not be relied on as always
/// being accurate, and should be treated as a "hint".
pub fn len(&self) -> Option<u64> {
match &self.0 {
Inner::Empty => Some(0),
Inner::Buffer(bytes) => Some(bytes.get_ref().len() as u64),
Inner::Reader(_, len) => *len,
match &self.repr {
Repr::Empty => Some(0),
Repr::Buffer(bytes) => Some(bytes.get_ref().len() as u64),
Repr::Reader(_, len) => *len,
}
}

/// Get the content type of this body, if any.
pub(crate) fn content_type(&self) -> Option<&HeaderValue> {
self.content_type.as_ref()
}

/// If this body is repeatable, reset the body stream back to the start of
/// the content. Returns `false` if the body cannot be reset.
pub fn reset(&mut self) -> bool {
match &mut self.0 {
Inner::Empty => true,
Inner::Buffer(cursor) => {
match &mut self.repr {
Repr::Empty => true,
Repr::Buffer(cursor) => {
cursor.set_position(0);
true
}
Expand All @@ -154,10 +181,10 @@ impl Body {
/// copy the bytes from the reader to the writing half of the pipe in a
/// blocking fashion.
pub(crate) fn into_async(self) -> (AsyncBody, Option<Writer>) {
match self.0 {
Inner::Empty => (AsyncBody::empty(), None),
Inner::Buffer(cursor) => (AsyncBody::from_bytes_static(cursor.into_inner()), None),
Inner::Reader(reader, len) => {
let (body, writer) = match self.repr {
Repr::Empty => (AsyncBody::empty(), None),
Repr::Buffer(cursor) => (AsyncBody::from_bytes_static(cursor.into_inner()), None),
Repr::Reader(reader, len) => {
let (pipe_reader, writer) = pipe();

(
Expand All @@ -172,16 +199,18 @@ impl Body {
}),
)
}
}
};

(body.with_content_type(self.content_type), writer)
}
}

impl Read for Body {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
match &mut self.0 {
Inner::Empty => Ok(0),
Inner::Buffer(cursor) => cursor.read(buf),
Inner::Reader(reader, _) => reader.read(buf),
match &mut self.repr {
Repr::Empty => Ok(0),
Repr::Buffer(cursor) => cursor.read(buf),
Repr::Reader(reader, _) => reader.read(buf),
}
}
}
Expand All @@ -200,7 +229,10 @@ impl From<()> for Body {

impl From<Vec<u8>> for Body {
fn from(body: Vec<u8>) -> Self {
Self(Inner::Buffer(Cursor::new(Cow::Owned(body))))
Self {
content_type: None,
repr: Repr::Buffer(Cursor::new(Cow::Owned(body))),
}
}
}

Expand Down
Loading