diff --git a/Cargo.lock b/Cargo.lock index 41c6dbd4..8bbba0b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,9 +316,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1229,9 +1229,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", diff --git a/crates/iroh-http-adapter/src/lib.rs b/crates/iroh-http-adapter/src/lib.rs index 6eb5b329..6893ab2f 100644 --- a/crates/iroh-http-adapter/src/lib.rs +++ b/crates/iroh-http-adapter/src/lib.rs @@ -10,6 +10,7 @@ use iroh_http_core::{ respond, CoreError, ErrorCode, HandleStore, RequestPayload, ResponseHeadEntry, + DEFAULT_MAX_REQUEST_BODY_BYTES, DEFAULT_MAX_RESPONSE_BODY_BYTES, }; /// Maximum number of header rows accepted at an adapter boundary. @@ -20,8 +21,10 @@ pub const MAX_HEADER_NAME_LEN: usize = 256; pub const MAX_HEADER_VALUE_LEN: usize = 8_192; /// Maximum adapter-level timeout in milliseconds. pub const MAX_TIMEOUT_MS: u64 = 300_000; -/// Maximum adapter-level body cap in bytes. -pub const MAX_BODY_BYTES: usize = 16 * 1024 * 1024; +/// Maximum adapter-level request body cap in bytes. +pub const MAX_BODY_BYTES: usize = DEFAULT_MAX_REQUEST_BODY_BYTES; +/// Maximum adapter-level response body cap in bytes. +pub const MAX_RESPONSE_BODY_BYTES: usize = DEFAULT_MAX_RESPONSE_BODY_BYTES; /// Maximum total simultaneous connections a served endpoint will accept. pub const MAX_TOTAL_CONNECTIONS: usize = 100_000; /// Maximum header block size in bytes accepted for a served endpoint. @@ -418,7 +421,7 @@ pub fn coerce_fetch_options(raw: RawFetchOptions) -> Result`. @@ -25,12 +26,17 @@ export function makeReadable( ): ReadableStream { return new ReadableStream({ async pull(controller) { - const chunk = await adapter.nextChunk(handle); - if (chunk === null) { - controller.close(); + try { + const chunk = await adapter.nextChunk(handle); + if (chunk === null) { + controller.close(); + onClose?.(); + } else { + controller.enqueue(chunk); + } + } catch (error) { onClose?.(); - } else { - controller.enqueue(chunk); + throw classifyError(error); } }, cancel() { diff --git a/packages/iroh-http-tauri/Cargo.lock b/packages/iroh-http-tauri/Cargo.lock index 16fb5575..5881e183 100644 --- a/packages/iroh-http-tauri/Cargo.lock +++ b/packages/iroh-http-tauri/Cargo.lock @@ -490,9 +490,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1857,9 +1857,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", diff --git a/packages/iroh-http-tauri/guest-js/index.ts b/packages/iroh-http-tauri/guest-js/index.ts index 68955d04..cc983308 100644 --- a/packages/iroh-http-tauri/guest-js/index.ts +++ b/packages/iroh-http-tauri/guest-js/index.ts @@ -7,6 +7,7 @@ import { installForegroundHealthCheck } from "./lifecycle.js"; import { bigintToSafeNumber, classifyBindError, + classifyError, encodeBase64, IrohNode, type IrohNodeWithSecret, @@ -93,15 +94,23 @@ class TauriAdapter extends IrohAdapter { const v = new Uint8Array(buf); return v.length > 0 && v[0] !== 0 ? v.subarray(1) : null; }, - // Channel empty or lock contended — fall back to async. - () => - invoke(`${PLUGIN}|next_chunk`, { + (error) => { + const classified = classifyError(error); + if ( + classified.code !== "INTERNAL" || + !classified.message.startsWith("try_next_chunk:") + ) { + throw classified; + } + // Channel empty or lock contended — fall back to async. + return invoke(`${PLUGIN}|next_chunk`, { endpointHandle: this.#epHandle, handle: safeHandle, }).then((buf) => { const v = new Uint8Array(buf); return v.length > 0 && v[0] !== 0 ? v.subarray(1) : null; - }), + }); + }, ); } diff --git a/tests/suites/adapter-validation.mjs b/tests/suites/adapter-validation.mjs index c9a58c7f..8cb3afab 100644 --- a/tests/suites/adapter-validation.mjs +++ b/tests/suites/adapter-validation.mjs @@ -6,7 +6,7 @@ */ const MAX_TIMEOUT_MS = 300_000; -const MAX_BODY_BYTES = 16 * 1024 * 1024; +const MAX_RESPONSE_BODY_BYTES = 256 * 1024 * 1024; export function adapterValidationTests({ createNode, @@ -63,7 +63,6 @@ export function adapterValidationTests({ Number.POSITIVE_INFINITY, -1, 1.5, - MAX_BODY_BYTES + 1, ]; for (const value of timeoutValues) { @@ -76,11 +75,40 @@ export function adapterValidationTests({ await node.fetch(url, { maxResponseBodyBytes: value }); }, `maxResponseBodyBytes=${String(value)}`); } + + const overCapError = await assertThrows(async () => { + await node.fetch(url, { + maxResponseBodyBytes: MAX_RESPONSE_BODY_BYTES + 1, + }); + }, "maxResponseBodyBytes over cap"); + assertEqual(overCapError.code, "INVALID_ARGUMENT"); + assertEqual(overCapError.name, "TypeError"); } finally { await node.close(); } }); + test("adapter validation enforces the configured response body limit", async () => { + const node = await createNode({ disableNetworking: true }); + let handle; + try { + const { id } = await node.addr(); + handle = node.serve(() => new Response(new Uint8Array(1025))); + + const error = await assertThrows(async () => { + const res = await node.fetch(`httpi://${id}/response-limit`, { + maxResponseBodyBytes: 1024, + }); + await res.arrayBuffer(); + }, "response body above configured cap"); + + assertEqual(error.code, "BODY_TOO_LARGE"); + } finally { + await node.close(); + if (handle) await handle.finished.catch(() => {}); + } + }); + test("adapter validation rejects invalid fetch inputs", async () => { const node = await createNode({ disableNetworking: true }); try { @@ -169,19 +197,27 @@ export function adapterValidationTests({ }); }); - const res = await node.fetch(`httpi://${id}/validation`, { - headers: [["x-conformance", "ok"]], - requestTimeout: 30_000, - maxResponseBodyBytes: 1024, - }); + for ( + const maxResponseBodyBytes of [ + 16 * 1024 * 1024, + 64 * 1024 * 1024, + MAX_RESPONSE_BODY_BYTES, + ] + ) { + const res = await node.fetch(`httpi://${id}/validation`, { + headers: [["x-conformance", "ok"]], + requestTimeout: 30_000, + maxResponseBodyBytes, + }); - assertEqual(res.status, 200, "valid request should succeed"); - assertEqual(await res.text(), "ok", "valid header should pass through"); - assertEqual( - res.headers.get("x-conformance-response"), - "ok", - "valid response header should pass through", - ); + assertEqual(res.status, 200, "valid request should succeed"); + assertEqual(await res.text(), "ok", "valid header should pass through"); + assertEqual( + res.headers.get("x-conformance-response"), + "ok", + "valid response header should pass through", + ); + } } finally { await node.close(); if (handle) await handle.finished.catch(() => {});