use bun_core::MutableString;
use bun_http::header_builder::HeaderBuilder;
use bun_http::{AsyncHTTP, FetchRedirect, Method};
use bun_url::URL;
use bytes::Bytes;
use compact_str::CompactString;
use smallvec::SmallVec;
pub struct HttpResponse {
pub status_code: u32,
pub status_text: CompactString,
pub headers: SmallVec<[(CompactString, CompactString); 8]>,
pub body: Bytes,
}
pub fn http_request(
method: Method,
url: &str,
headers: &[(String, String)],
body: Option<&[u8]>,
) -> ::std::result::Result<HttpResponse, String> {
let url_bytes = url.as_bytes();
let parsed_url = URL::parse(url_bytes);
let mut hb = HeaderBuilder::default();
for (name, value) in headers {
hb.count(name.as_bytes(), value.as_bytes());
}
if let ::std::result::Result::Err(e) = hb.allocate() {
return ::std::result::Result::Err(format!("Header allocation failed: {:?}", e));
}
for (name, value) in headers {
hb.append(name.as_bytes(), value.as_bytes());
}
let entry_list = hb.entries;
let headers_buf: &[u8] = unsafe {
if let Some(ptr) = hb.content.ptr {
::std::slice::from_raw_parts(ptr.as_ptr(), hb.content.len)
} else {
&[]
}
};
let response_buffer = Box::into_raw(Box::new(MutableString::default()));
let body_slice: &[u8] = body.unwrap_or_default();
let mut async_http = AsyncHTTP::init_sync(
method,
parsed_url,
entry_list,
headers_buf,
response_buffer,
body_slice,
None, None, FetchRedirect::Follow,
);
let result = async_http.send_sync().map_err(|e| format!("{:?}", e))?;
let body_vec = unsafe { std::mem::take(&mut (*response_buffer).list) };
unsafe {
drop(Box::from_raw(response_buffer));
}
let status_code = result.status_code;
let status_text = CompactString::new(::std::str::from_utf8(result.status).unwrap_or(""));
let headers: SmallVec<[(CompactString, CompactString); 8]> = result
.headers
.list
.iter()
.map(|h| {
let name = CompactString::new(::std::str::from_utf8(h.name()).unwrap_or(""));
let value = CompactString::new(::std::str::from_utf8(h.value()).unwrap_or(""));
(name, value)
})
.collect();
::std::result::Result::Ok(HttpResponse {
status_code,
status_text,
headers,
body: Bytes::from(body_vec),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_method_roundtrip() {
assert_eq!(Method::GET.as_str(), "GET");
assert_eq!(Method::POST.as_str(), "POST");
assert_eq!(Method::PUT.as_str(), "PUT");
assert_eq!(Method::DELETE.as_str(), "DELETE");
assert_eq!(Method::PATCH.as_str(), "PATCH");
assert_eq!(Method::HEAD.as_str(), "HEAD");
}
#[test]
fn test_http_response_construction() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![("Content-Type".into(), "text/html".into())],
body: Bytes::from_static(b"hello"),
};
assert_eq!(resp.status_code, 200);
assert_eq!(resp.status_text, "OK");
assert_eq!(resp.headers.len(), 1);
assert_eq!(&resp.body[..], b"hello");
}
#[test]
fn test_http_response_empty_body() {
let resp = HttpResponse {
status_code: 204,
status_text: CompactString::new("No Content"),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert_eq!(resp.status_code, 204);
assert!(resp.body.is_empty());
assert!(resp.headers.is_empty());
}
#[test]
fn test_http_response_multiple_headers() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![
("content-type".into(), "application/json".into()),
("x-request-id".into(), "abc-123".into()),
("cache-control".into(), "no-cache".into()),
],
body: Bytes::from_static(b"{}"),
};
assert_eq!(resp.headers.len(), 3);
assert_eq!(resp.headers[0].0, "content-type");
assert_eq!(resp.headers[1].1, "abc-123");
}
#[test]
fn test_http_response_error_status() {
let resp = HttpResponse {
status_code: 500,
status_text: CompactString::new("Internal Server Error"),
headers: SmallVec::new(),
body: Bytes::from_static(b"error"),
};
assert_eq!(resp.status_code, 500);
assert_eq!(resp.status_text, "Internal Server Error");
}
#[test]
fn test_http_response_redirect_status() {
let resp = HttpResponse {
status_code: 301,
status_text: CompactString::new("Moved Permanently"),
headers: smallvec::smallvec![("location".into(), "https://example.com".into())],
body: Bytes::new(),
};
assert_eq!(resp.status_code, 301);
assert_eq!(resp.headers[0].0, "location");
}
#[test]
fn test_method_all_variants() {
assert_eq!(Method::GET.as_str(), "GET");
assert_eq!(Method::POST.as_str(), "POST");
assert_eq!(Method::PUT.as_str(), "PUT");
assert_eq!(Method::DELETE.as_str(), "DELETE");
assert_eq!(Method::PATCH.as_str(), "PATCH");
assert_eq!(Method::HEAD.as_str(), "HEAD");
assert_eq!(Method::OPTIONS.as_str(), "OPTIONS");
}
#[test]
fn test_method_connect_trace() {
assert_eq!(Method::CONNECT.as_str(), "CONNECT");
assert_eq!(Method::TRACE.as_str(), "TRACE");
}
#[test]
fn test_http_response_status_codes_range() {
for code in [
200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 500, 502, 503,
] {
let resp = HttpResponse {
status_code: code,
status_text: CompactString::new(""),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert_eq!(resp.status_code, code);
}
}
#[test]
fn test_http_response_body_binary() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: SmallVec::new(),
body: Bytes::from(vec![0x89, 0x50, 0x4E, 0x47]),
};
assert_eq!(&resp.body[..4], &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn test_http_response_header_value_with_semicolon() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![(
"content-type".into(),
"text/html; charset=utf-8".into()
)],
body: Bytes::new(),
};
assert!(resp.headers[0].1.contains("charset=utf-8"));
}
#[test]
fn test_http_response_large_body() {
let large_body: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: SmallVec::new(),
body: Bytes::from(large_body.clone()),
};
assert_eq!(resp.body.len(), 10_000);
assert_eq!(resp.body[0], 0);
assert_eq!(resp.body[255], 255);
}
#[test]
fn test_http_response_header_order_preserved() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![
("x-first".into(), "1".into()),
("x-second".into(), "2".into()),
("x-third".into(), "3".into()),
],
body: Bytes::new(),
};
assert_eq!(resp.headers[0].0, "x-first");
assert_eq!(resp.headers[1].0, "x-second");
assert_eq!(resp.headers[2].0, "x-third");
}
#[test]
fn test_http_response_status_4xx() {
for code in [400, 401, 403, 404, 405, 408, 429] {
let resp = HttpResponse {
status_code: code,
status_text: CompactString::new(""),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert!(resp.status_code >= 400 && resp.status_code < 500);
}
}
#[test]
fn test_http_response_status_5xx() {
for code in [500, 502, 503, 504] {
let resp = HttpResponse {
status_code: code,
status_text: CompactString::new(""),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert!(resp.status_code >= 500 && resp.status_code < 600);
}
}
#[test]
fn test_method_debug_format() {
let _ = format!("{:?}", Method::GET);
let _ = format!("{:?}", Method::POST);
}
#[test]
fn test_http_response_unicode_body() {
let unicode_body = "你好世界".as_bytes().to_vec();
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![(
"content-type".into(),
"text/plain; charset=utf-8".into()
)],
body: Bytes::from(unicode_body.clone()),
};
assert_eq!(&resp.body[..], &unicode_body[..]);
}
#[test]
fn test_http_response_empty_status_text() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new(""),
headers: SmallVec::new(),
body: Bytes::new(),
};
assert!(resp.status_text.is_empty());
}
#[test]
fn test_http_response_header_duplicate_names() {
let resp = HttpResponse {
status_code: 200,
status_text: CompactString::new("OK"),
headers: smallvec::smallvec![
("set-cookie".into(), "a=1".into()),
("set-cookie".into(), "b=2".into()),
],
body: Bytes::new(),
};
assert_eq!(resp.headers.len(), 2);
assert_eq!(resp.headers[0].0, "set-cookie");
assert_eq!(resp.headers[1].0, "set-cookie");
assert_ne!(resp.headers[0].1, resp.headers[1].1);
}
#[test]
fn test_short_status_text_no_heap_alloc() {
let short = CompactString::new("OK");
assert_eq!(short.len(), 2);
assert_eq!(&*short, "OK");
let longer = CompactString::new("Internal Server Error");
assert_eq!(&*longer, "Internal Server Error");
}
#[test]
fn test_small_headers_stack_allocated() {
let headers: SmallVec<[(CompactString, CompactString); 8]> = smallvec::smallvec![
("content-type".into(), "text/html".into()),
("content-length".into(), "42".into()),
("server".into(), "bao".into()),
];
assert_eq!(headers.len(), 3);
assert!(headers.len() <= 8);
}
#[test]
fn test_body_take_no_clone() {
let original = vec![0xDE, 0xAD, 0xBE, 0xEF];
let ptr = original.as_ptr();
let b = Bytes::from(original);
assert_eq!(
b.as_ptr(),
ptr,
"Bytes::from(Vec) must reuse the same allocation (zero-copy)"
);
assert_eq!(&b[..], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
}