Skip to content
Open
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
22 changes: 22 additions & 0 deletions docs/runtime/networking/fetch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,28 @@ const response = await fetch("http://example.com", {
controller.abort();
```

### Subresource integrity

To make `fetch` check the response body against a digest, use the `integrity` option:

```ts
const response = await fetch("data:text/plain,hello", {
integrity: "sha256-LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=",
});

console.log(await response.text()); // "hello"
```

The value is a list of `sha256-`, `sha384-` or `sha512-` digests in base64 or base64url, separated by whitespace. When the list names more than one algorithm, only the digests of the strongest algorithm count, and the body must match one of them. A value that names none of these algorithms checks no digest.

With a non-empty `integrity`, `fetch` reads the whole body into memory before the promise settles:

- If the body does not match, the promise rejects with a `TypeError` and no `Response` is created. The error has the code `IntegrityCheckFailed`, and its message has the digest of the body that arrived.
- A response without a body (a `HEAD` request, or status `204`, `205` or `304`) also rejects.
- The digest is calculated from the bytes that the `Response` gives you. These are the decoded bytes, or the raw bytes with `decompress: false`.

The option also applies to `data:` URLs, to `blob:` URLs of a `Blob` in memory, and to a `Request` that was created with `integrity`. `fetch` rejects a request that it cannot check: a `file:` URL, a `blob:` URL of a `Bun.file()` or of an S3 file, a request that upgrades the connection, and an S3 upload with a stream body.

### Unix domain sockets

To fetch a URL using a Unix domain socket, use the `unix: string` option:
Expand Down
4 changes: 2 additions & 2 deletions docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ The following list covers the globals implemented by Node.js and Bun's compatibi

### [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch)

🟢 Fully implemented. The `integrity` option is ignored.
🟢 Fully implemented. The `cache`, `credentials`, `duplex`, `mode`, `priority`, `referrer`, `referrerPolicy` and `window` options are accepted but ignored.

### [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File)

Expand Down Expand Up @@ -421,7 +421,7 @@ The following list covers the globals implemented by Node.js and Bun's compatibi

### [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)

🟡 Missing `keepalive` and `duplex`. The `credentials`, `integrity`, `referrer` and `referrerPolicy` options are accepted but ignored.
🟡 Missing `keepalive` and `duplex`. The `credentials`, `referrer` and `referrerPolicy` options are accepted but ignored.

### [`setImmediate()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate)

Expand Down
4 changes: 4 additions & 0 deletions src/jsc/CommonStrings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub enum CommonStringsForRust {
FetchOptionTls = 34,
FetchOptionUnix = 35,
FetchOptionVerbose = 36,
FetchOptionCache = 37,
FetchOptionIntegrity = 38,
FetchOptionMode = 39,
FetchOptionRedirect = 40,
}

unsafe extern "C" {
Expand Down
12 changes: 12 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1316,6 +1316,18 @@ impl JSValue {
_ => Ok(None),
}
}
/// [`get_optional_enum`](Self::get_optional_enum) for a key that is one of `BunCommonStrings.h`'s.
pub fn get_optional_enum_common_string<E: FromJsEnum>(
self,
global: &JSGlobalObject,
key: crate::CommonString,
property_name: &'static str,
) -> JsResult<Option<E>> {
match self.get_common_string(global, key)? {
Some(v) if !v.is_null() => Ok(Some(v.to_enum::<E>(global, property_name)?)),
_ => Ok(None),
}
}
/// `get(prop)`, filter
/// undefined/null → `None`, otherwise `toEnum` (via `to_enum_from_map`).
pub fn get_optional_enum_from_map<
Expand Down
12 changes: 12 additions & 0 deletions src/jsc/bindings/BunCommonStrings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ enum class CommonStringsForRust : uint8_t {
fetchOptionTls = 34,
fetchOptionUnix = 35,
fetchOptionVerbose = 36,
fetchOptionCache = 37,
fetchOptionIntegrity = 38,
fetchOptionMode = 39,
fetchOptionRedirect = 40,
};

static JSC::JSValue toJS(Zig::GlobalObject* globalObject, CommonStringsForRust commonString)
Expand Down Expand Up @@ -302,6 +306,14 @@ static JSC::JSValue toJS(Zig::GlobalObject* globalObject, CommonStringsForRust c
return commonStrings.fetchOptionUnixString();
case CommonStringsForRust::fetchOptionVerbose:
return commonStrings.fetchOptionVerboseString();
case CommonStringsForRust::fetchOptionCache:
return commonStrings.fetchOptionCacheString();
case CommonStringsForRust::fetchOptionIntegrity:
return commonStrings.fetchOptionIntegrityString();
case CommonStringsForRust::fetchOptionMode:
return commonStrings.fetchOptionModeString();
case CommonStringsForRust::fetchOptionRedirect:
return commonStrings.fetchOptionRedirectString();
default: {
ASSERT_NOT_REACHED();
return jsUndefined();
Expand Down
4 changes: 4 additions & 0 deletions src/jsc/bindings/BunCommonStrings.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@
macro(envHttpProxy, "http_proxy") \
macro(envNO_PROXY, "NO_PROXY") \
macro(envNoProxy, "no_proxy") \
macro(fetchOptionCache, "cache") \
macro(fetchOptionCompress, "compress") \
macro(fetchOptionDecompress, "decompress") \
macro(fetchOptionIntegrity, "integrity") \
macro(fetchOptionKeepalive, "keepalive") \
macro(fetchOptionMaxRedirects, "maxRedirects") \
macro(fetchOptionMode, "mode") \
macro(fetchOptionProtocol, "protocol") \
macro(fetchOptionProxy, "proxy") \
macro(fetchOptionRedirect, "redirect") \
macro(fetchOptionS3, "s3") \
macro(fetchOptionSession, "session") \
macro(fetchOptionTimeout, "timeout") \
Expand Down
89 changes: 83 additions & 6 deletions src/runtime/webcore/Request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ pub(crate) struct Request {
pub(crate) weak_ptr_data: WeakPtrData,
// We must report a consistent value for this
reported_estimated_size: Cell<usize>,
/// The `integrity` member of the init, when it is not the empty string. Boxed: few requests have one.
integrity: Option<Box<BunString>>,
}

// 112 bytes is the mimalloc size class that a Request without `integrity` (104 bytes) was already in.
const _: () = assert!(core::mem::size_of::<Request>() <= 112);

// A `#[repr(C)]` 4-byte struct for direct
// field access — `Request` is only ever passed to C++ by **pointer** with size
// reported via the codegen'd `Request__ZigStructSize`, so the absolute size is
Expand Down Expand Up @@ -370,6 +375,7 @@ impl Request {
core::mem::size_of::<Request>()
+ self.request_context.memory_cost()
+ self.url.get().byte_slice().len()
+ self.size_of_integrity()
+ self.body_value().memory_cost()
}

Expand Down Expand Up @@ -431,6 +437,7 @@ impl Request {
request_context: AnyRequestContext::NULL,
weak_ptr_data: WeakPtrData::EMPTY,
reported_estimated_size: Cell::new(0),
integrity: None,
}
}

Expand Down Expand Up @@ -458,6 +465,7 @@ impl Request {
self.reported_estimated_size.set(
self.body_value().estimated_size()
+ self.size_of_url()
+ self.size_of_integrity()
+ core::mem::size_of::<Request>(),
);
}
Expand Down Expand Up @@ -675,8 +683,11 @@ impl Request {
JSValue::js_empty_string(global_this)
}

pub(crate) fn get_integrity(_this: &Self, global_this: &JSGlobalObject) -> JSValue {
JSValue::js_empty_string(global_this)
pub(crate) fn get_integrity(&self, global_this: &JSGlobalObject) -> JsResult<JSValue> {
match self.integrity() {
Some(integrity) => integrity.to_js(global_this),
None => Ok(JSValue::js_empty_string(global_this)),
}
}

/// The `AbortSignal` this request was constructed with or lazily created
Expand Down Expand Up @@ -713,6 +724,7 @@ impl Request {
self.headers.set(None);

self.url.set(BunString::EMPTY);
self.integrity = None;

// AbortSignalRef::Drop unrefs the C++ handle.
self.signal.set(None);
Expand Down Expand Up @@ -768,6 +780,21 @@ impl Request {
self.url.get().to_js(global_object)
}

/// The `integrity` that `fetch()` checks the response body against. `None`: no check.
pub(crate) fn integrity(&self) -> Option<&BunString> {
self.integrity.as_deref()
}

fn set_integrity(&mut self, integrity: BunString) {
self.integrity = (!integrity.is_empty()).then(|| Box::new(integrity));
}

fn size_of_integrity(&self) -> usize {
self.integrity.as_ref().map_or(0, |integrity| {
core::mem::size_of::<BunString>() + integrity.byte_slice().len()
})
}

pub(crate) fn size_of_url(&self) -> usize {
let url = self.url.get();
if url.length() > 0 {
Expand Down Expand Up @@ -962,7 +989,7 @@ enum Fields {
// Credentials,
Redirect,
Cache,
// Integrity,
Integrity,
// Keepalive,
Signal,
// Proxy,
Expand Down Expand Up @@ -998,6 +1025,7 @@ impl Request {
request_context: AnyRequestContext::NULL,
weak_ptr_data: WeakPtrData::EMPTY,
reported_estimated_size: Cell::new(0),
integrity: None,
};
// A scopeguard cannot capture `&mut req` while the
// fn body also uses it. Cleanup is invoked at each early-return site via `bail!`.
Expand Down Expand Up @@ -1124,6 +1152,11 @@ impl Request {
fields.insert(Fields::Mode);
}

if !fields.contains(Fields::Integrity) {
req.integrity.clone_from(&request.integrity);
fields.insert(Fields::Integrity);
}

if !fields.contains(Fields::Headers) {
match request.clone_headers(cx.global()) {
Ok(Some(headers)) => {
Expand Down Expand Up @@ -1200,6 +1233,34 @@ impl Request {
}
}

// "integrity": a DOMString, so every value but undefined is a string.
if !fields.contains(Fields::Integrity) {
// A Request that is the input gives the value that it stores. A property of the object, its own or a subclass getter, does not replace it.
let input_request = if !is_first_argument_a_url && value == url_or_object {
value.as_class_ref::<Request>()
} else {
None
};
if let Some(request) = input_request {
req.integrity.clone_from(&request.integrity);
fields.insert(Fields::Integrity);
} else {
match value
.get_common_string(cx.global(), bun_jsc::CommonString::FetchOptionIntegrity)
{
Ok(Some(integrity)) => {
match BunString::from_js(integrity, cx.global()) {
Ok(integrity) => req.set_integrity(integrity),
Err(e) => bail!(Err(e)),
}
fields.insert(Fields::Integrity);
}
Ok(None) => {}
Err(e) => bail!(Err(e)),
}
}
}

if !fields.contains(Fields::Body) {
match value.fast_get(cx.global(), bun_jsc::BuiltinName::Body) {
Ok(Some(body_)) => {
Expand Down Expand Up @@ -1341,7 +1402,11 @@ impl Request {

// Extract redirect option
if !fields.contains(Fields::Redirect) {
match value.get_optional_enum::<FetchRedirect>(cx.global(), "redirect") {
match value.get_optional_enum_common_string::<FetchRedirect>(
cx.global(),
bun_jsc::CommonString::FetchOptionRedirect,
"redirect",
) {
Ok(Some(redirect_value)) => {
req.flags.redirect = redirect_value;
fields.insert(Fields::Redirect);
Expand All @@ -1353,7 +1418,11 @@ impl Request {

// Extract cache option
if !fields.contains(Fields::Cache) {
match value.get_optional_enum::<FetchCacheMode>(cx.global(), "cache") {
match value.get_optional_enum_common_string::<FetchCacheMode>(
cx.global(),
bun_jsc::CommonString::FetchOptionCache,
"cache",
) {
Ok(Some(cache_value)) => {
req.flags.cache = cache_value;
fields.insert(Fields::Cache);
Expand All @@ -1365,7 +1434,11 @@ impl Request {

// Extract mode option
if !fields.contains(Fields::Mode) {
match value.get_optional_enum::<FetchRequestMode>(cx.global(), "mode") {
match value.get_optional_enum_common_string::<FetchRequestMode>(
cx.global(),
bun_jsc::CommonString::FetchOptionMode,
"mode",
) {
Ok(Some(mode_value)) => {
req.flags.mode = mode_value;
fields.insert(Fields::Mode);
Expand Down Expand Up @@ -1484,6 +1557,7 @@ impl Request {
} else {
self.url.get().clone()
};
let integrity = self.integrity.clone();

// `ptr::write` is a raw bit-overwrite — no destructors run on the old
// `*req`, so the Drop impl on `JsRef` doesn't fire on the caller's
Expand All @@ -1509,6 +1583,7 @@ impl Request {
request_context: AnyRequestContext::NULL,
weak_ptr_data: WeakPtrData::EMPTY,
reported_estimated_size: Cell::new(0),
integrity,
},
);
}
Expand Down Expand Up @@ -1541,6 +1616,7 @@ impl Request {
request_context: AnyRequestContext::NULL,
weak_ptr_data: WeakPtrData::EMPTY,
reported_estimated_size: Cell::new(0),
integrity: None,
});
// Box<Request> drops on the error path automatically
self.clone_into(&mut req, cx, false)?;
Expand Down Expand Up @@ -1570,6 +1646,7 @@ impl Request {
request_context,
weak_ptr_data: WeakPtrData::EMPTY,
reported_estimated_size: Cell::new(0),
integrity: None,
}
}
}
Loading
Loading