Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
31 changes: 31 additions & 0 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,37 @@ interface BunFetchRequestInit extends RequestInit {
*/
decompress?: boolean;

/**
* Automatically compress the request body before sending and set the
* `Content-Encoding` request header accordingly.
*
* - `true` is equivalent to `"gzip"`.
* - A string selects the encoding with its default level.
* - An object selects the encoding and an explicit compression `level`.
*
* Only buffered bodies (string, `ArrayBuffer`/`TypedArray`, `Blob`) are
* compressed; `ReadableStream` bodies are sent as-is. If the request
* already has a `Content-Encoding` header, the body is left unchanged.
* This is a custom property that is not part of the Fetch API specification.
*
* @default false
* @example
* ```js
* await fetch("https://example.com/upload", {
* method: "POST",
* body: JSON.stringify(bigPayload),
* compress: "gzip",
* });
* ```
*/
compress?:
| boolean
| "gzip"
| "deflate"
| "br"
| "zstd"
| { encoding: "gzip" | "deflate" | "br" | "zstd"; level?: number };

/**
* The maximum number of redirects to follow when `redirect` is `"follow"`.
* If the response chain redirects more than this many times, the request
Expand Down
61 changes: 61 additions & 0 deletions src/runtime/webcore/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
#[path = "fetch/FetchTasklet.rs"]
pub mod fetch_tasklet;

#[path = "fetch/compress_body.rs"]
pub mod compress_body;

// ──────────────────────────────────────────────────────────────────────────
// fetch() implementation
// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -420,6 +423,7 @@
let mut disable_timeout = false;
let mut disable_keepalive = false;
let mut disable_decompression = false;
let mut compress: Option<compress_body::CompressOption> = None;
let mut max_redirects: Option<u8> = None;
let mut verbose: http::HTTPVerboseLevel = if vm
.log_ref()
Expand Down Expand Up @@ -664,6 +668,34 @@
return Ok(JSValue::ZERO);
}

// "compress: boolean | string | { encoding, level? }"
'extract_compress: {
let objects_to_try = [
options_object.unwrap_or(JSValue::ZERO),
request_init_object.unwrap_or(JSValue::ZERO),
];

for obj in objects_to_try {
if !obj.is_empty() {
if let Some(compress_value) = obj.get(global_this, "compress")? {
if !compress_value.is_undefined() {
compress =
compress_body::CompressOption::from_js(global_this, compress_value)?;
break 'extract_compress;
}
}

if global_this.has_exception() {
return Ok(JSValue::ZERO);
}
}
}
}

if global_this.has_exception() {
return Ok(JSValue::ZERO);
}

// "maxRedirects: number"
'extract_max_redirects: {
let objects_to_try = [
Expand Down Expand Up @@ -1731,6 +1763,35 @@
}
}

// Automatic request-body compression. Only buffered bodies (Blob bytes,
// ArrayBuffer/TypedArray, string) are handled; ReadableStream and sendfile
// are skipped. S3 destinations replace the header set with a signed one,
// so compression is skipped there too.
if let Some(compress_opt) = compress
&& let HTTPRequestBody::AnyBlob(_) = &body
&& !url.is_s3()
{

Check failure on line 1773 in src/runtime/webcore/fetch.rs

View check run for this annotation

Claude / Claude Code Review

Bun.file() body silently skips compression when sendfile path is taken

When `body: Bun.file(path)` takes the sendfile fast path (plain `http://`, no proxy, file ≥32 KiB, non-Windows), the body becomes `HTTPRequestBody::Sendfile` and this `AnyBlob`-only match silently skips the explicitly-requested compression — yet the *same* call over `https://`, with a proxy, with a <32 KiB file, or on Windows reads into an `AnyBlob` and *does* compress. Since the user-facing docs say `Blob` bodies are compressed and `Bun.file()` returns a `Blob`, consider adding `&& compress.is_
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
let already_has_encoding = headers
.as_ref()
.and_then(|h| h.get_content_encoding())
.is_some();
if !already_has_encoding {
let input = body.slice();
if !input.is_empty() {
let compressed =
compress_body::compress_request_body(global_this, input, &compress_opt)?;
let mut old = core::mem::replace(
&mut body,
HTTPRequestBody::AnyBlob(blob::Any::from_owned_slice(compressed)),
);
old.detach();
headers
.get_or_insert_default()
.append(b"Content-Encoding", compress_opt.encoding.header_value());
}
}
}

if url.is_s3() {
// get ENV config — `Transpiler::env_mut` is the safe accessor for the
// process-singleton dotenv loader (set during init).
Expand Down
Loading
Loading