From 6ffde827d5e38ba360b5340f9dbc6ca71cdc9c7d Mon Sep 17 00:00:00 2001 From: them7d Date: Sat, 12 Sep 2026 17:09:16 +0300 Subject: [PATCH 1/6] fix: load fonts with request in desktop app cause OOMs (replaced with fetch) --- src/app/tauri/http.ts | 20 +++++++++++++++----- tests/engine/tauri/http.test.ts | 7 ------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/tauri/http.ts b/src/app/tauri/http.ts index 29fefb760..eb31cfab2 100644 --- a/src/app/tauri/http.ts +++ b/src/app/tauri/http.ts @@ -1,4 +1,3 @@ -import type { FetchFunction } from '@/app/http/types' export interface ProxyHttpHeader { name: string @@ -74,9 +73,6 @@ export interface TauriFetchOptions { maxResponseBytes?: number } -export function createTauriFetch(options: TauriFetchOptions = {}): FetchFunction { - return (input, init) => tauriFetch(input, init, options.maxResponseBytes, options.timeoutMs) -} export async function tauriFetch( input: RequestInfo | URL, @@ -84,15 +80,29 @@ export async function tauriFetch( maxResponseBytes?: number, timeoutMs?: number ): Promise { + const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url); + + if (url.startsWith('http://ipc.localhost') || url.startsWith('https://ipc.localhost') || url.startsWith('ipc://')) { + return fetch(input as RequestInfo, init); + } + const request = new Request(input, init) request.signal.throwIfAborted() const { invoke } = await import('@tauri-apps/api/core') request.signal.throwIfAborted() + + let bodyData: Uint8Array | undefined = undefined + if (request.body != null) { + const buffer = await request.arrayBuffer() + request.signal.throwIfAborted() + bodyData = new Uint8Array(buffer) + } + const payload: ProxyHttpRequest = { url: request.url, method: request.method, headers: headersToProxyHeaders(request.headers), - body: request.body == null ? undefined : [...new Uint8Array(await request.arrayBuffer())], + body: bodyData ? Array.from(bodyData) : undefined, max_response_bytes: maxResponseBytes, follow_redirects: request.redirect === 'follow', timeout_ms: timeoutMs diff --git a/tests/engine/tauri/http.test.ts b/tests/engine/tauri/http.test.ts index 453d9035b..19551b68f 100644 --- a/tests/engine/tauri/http.test.ts +++ b/tests/engine/tauri/http.test.ts @@ -1,7 +1,6 @@ import { afterEach, describe, expect, test } from 'bun:test' import { - createTauriFetch, tauriFetch, type ProxyHttpRequest, type ProxyHttpResponse @@ -56,12 +55,6 @@ describe('tauriFetch', () => { } }) - const response = await createTauriFetch({ timeoutMs: 15_000 })('https://example.test/check', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: '{"ok":true}' - }) - if (!captured) throw new Error('Expected proxy_http_request to be invoked') expect(response.status).toBe(201) expect(response.headers.get('x-open-pencil')).toBe('ok') From b2dd02669adbfabbe4bd7eb310b9e9ad5a7d0503 Mon Sep 17 00:00:00 2001 From: Mohammad Zaid <64780229+them7d@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:25:13 +0300 Subject: [PATCH 2/6] Fix font loading to prevent out of memory errors Replaced font loading method in desktop app to prevent OOMs. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be2a0b649..f0394c832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ ### Fixed +- load fonts with request in desktop app cause OOMs (replaced with fetch) + - Preserve edited instance text, including cleared labels, when saving and reopening `.fig` files. - Honor `.pen` frame layout defaults and sizing and padding shorthands so imported auto-layout frames keep their computed dimensions and child positions. (#564) - Avoid macOS Keychain prompts during credential status checks and pause repeated credential access after failures until explicitly retried from Settings. From 695c113a32482d5a067dfa35be715ad63ffeecd9 Mon Sep 17 00:00:00 2001 From: Mohammad Zaid <64780229+them7d@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:59:04 +0300 Subject: [PATCH 3/6] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0394c832..c807fa5a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,7 +46,7 @@ ### Fixed -- load fonts with request in desktop app cause OOMs (replaced with fetch) +- The desktop app now loads fonts with `fetch` instead of `request`, preventing out-of-memory errors. - Preserve edited instance text, including cleared labels, when saving and reopening `.fig` files. - Honor `.pen` frame layout defaults and sizing and padding shorthands so imported auto-layout frames keep their computed dimensions and child positions. (#564) From 7b3e09dae758754ae212bcaa81c8adbe208632f9 Mon Sep 17 00:00:00 2001 From: Mohammad Zaid <64780229+them7d@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:38:57 +0300 Subject: [PATCH 4/6] fix(tauri-fetch): use robust URL parsing for IPC detection Replace string prefix matching with URL parsing to reliably detect ipc:// and ipc.localhost requests. Also capture the native fetch via globalThis binding to avoid recursive calls if fetch is patched/overridden elsewhere. --- src/app/tauri/http.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/tauri/http.ts b/src/app/tauri/http.ts index eb31cfab2..129d8b9c6 100644 --- a/src/app/tauri/http.ts +++ b/src/app/tauri/http.ts @@ -73,6 +73,7 @@ export interface TauriFetchOptions { maxResponseBytes?: number } +const nativeFetch: typeof fetch = globalThis.fetch.bind(globalThis) export async function tauriFetch( input: RequestInfo | URL, @@ -80,10 +81,13 @@ export async function tauriFetch( maxResponseBytes?: number, timeoutMs?: number ): Promise { - const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url); - - if (url.startsWith('http://ipc.localhost') || url.startsWith('https://ipc.localhost') || url.startsWith('ipc://')) { - return fetch(input as RequestInfo, init); + const parsedURL = new URL(typeof input === 'object' ? (input as Request).url : input) + const isIpcURL = + parsedURL.protocol === 'ipc:' || + ((parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:') && + parsedURL.hostname === 'ipc.localhost') + if (isIpcURL) { + return nativeFetch(input as RequestInfo, init) } const request = new Request(input, init) From d3097067f34387503f4ae7bcecd2fd390e9cc32f Mon Sep 17 00:00:00 2001 From: Mohammad Zaid <64780229+them7d@users.noreply.github.com> Date: Sun, 13 Sep 2026 15:47:40 +0300 Subject: [PATCH 5/6] fix(tauri-fetch): correctly extract URL from Request input Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/app/tauri/http.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/tauri/http.ts b/src/app/tauri/http.ts index 129d8b9c6..5f59729f3 100644 --- a/src/app/tauri/http.ts +++ b/src/app/tauri/http.ts @@ -81,7 +81,7 @@ export async function tauriFetch( maxResponseBytes?: number, timeoutMs?: number ): Promise { - const parsedURL = new URL(typeof input === 'object' ? (input as Request).url : input) + const parsedURL = new URL(input instanceof Request ? input.url : input) const isIpcURL = parsedURL.protocol === 'ipc:' || ((parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:') && From e342cfca30da9de62ed768a9db38d0209dfcac4a Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 13 Sep 2026 15:48:40 +0300 Subject: [PATCH 6/6] fix(tauri): preserve HTTP contracts when bypassing IPC Keep IPC on captured native fetch during font proxying. Preserve URL inputs, configured timeouts, and generated multipart headers; cover native routing and reject lookalike IPC hostnames. --- src/app/tauri/http.ts | 33 ++++++++++++++++--- tests/engine/tauri/http.test.ts | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/app/tauri/http.ts b/src/app/tauri/http.ts index 129d8b9c6..f86015aaa 100644 --- a/src/app/tauri/http.ts +++ b/src/app/tauri/http.ts @@ -1,3 +1,4 @@ +import type { FetchFunction } from '@/app/http/types' export interface ProxyHttpHeader { name: string @@ -71,23 +72,45 @@ export function withAbortSignal(promise: Promise, signal: AbortSignal): Pr export interface TauriFetchOptions { timeoutMs?: number maxResponseBytes?: number + nativeFetch?: FetchFunction } const nativeFetch: typeof fetch = globalThis.fetch.bind(globalThis) +export function createTauriFetch(options: TauriFetchOptions = {}): FetchFunction { + return (input, init) => + executeTauriFetch( + options.nativeFetch ?? nativeFetch, + input, + init, + options.maxResponseBytes, + options.timeoutMs + ) +} + export async function tauriFetch( input: RequestInfo | URL, init?: RequestInit, maxResponseBytes?: number, timeoutMs?: number ): Promise { - const parsedURL = new URL(typeof input === 'object' ? (input as Request).url : input) + return executeTauriFetch(nativeFetch, input, init, maxResponseBytes, timeoutMs) +} + +async function executeTauriFetch( + fetcher: FetchFunction, + input: RequestInfo | URL, + init?: RequestInit, + maxResponseBytes?: number, + timeoutMs?: number +): Promise { + const parsedURL = new URL(input instanceof Request ? input.url : input.toString()) const isIpcURL = parsedURL.protocol === 'ipc:' || ((parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:') && parsedURL.hostname === 'ipc.localhost') if (isIpcURL) { - return nativeFetch(input as RequestInfo, init) + return fetcher(input, init) } const request = new Request(input, init) @@ -95,7 +118,9 @@ export async function tauriFetch( const { invoke } = await import('@tauri-apps/api/core') request.signal.throwIfAborted() - let bodyData: Uint8Array | undefined = undefined + // Capture generated multipart headers before consuming the Request body. + const headers = headersToProxyHeaders(request.headers) + let bodyData: Uint8Array | undefined if (request.body != null) { const buffer = await request.arrayBuffer() request.signal.throwIfAborted() @@ -105,7 +130,7 @@ export async function tauriFetch( const payload: ProxyHttpRequest = { url: request.url, method: request.method, - headers: headersToProxyHeaders(request.headers), + headers, body: bodyData ? Array.from(bodyData) : undefined, max_response_bytes: maxResponseBytes, follow_redirects: request.redirect === 'follow', diff --git a/tests/engine/tauri/http.test.ts b/tests/engine/tauri/http.test.ts index 19551b68f..c68232fee 100644 --- a/tests/engine/tauri/http.test.ts +++ b/tests/engine/tauri/http.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, test } from 'bun:test' import { + createTauriFetch, tauriFetch, type ProxyHttpRequest, type ProxyHttpResponse @@ -43,6 +44,44 @@ afterEach(async () => { }) describe('tauriFetch', () => { + test('routes IPC through the captured native fetch without invoking the HTTP proxy', async () => { + await mockTauriIPC(() => { + throw new Error('IPC recursively entered the HTTP proxy') + }) + const inputs = [ + 'ipc://localhost/plugin%3Ahttp%7Cfetch', + 'http://ipc.localhost/plugin%3Ahttp%7Cfetch', + new URL('https://ipc.localhost/plugin%3Ahttp%7Cfetch'), + new Request('http://ipc.localhost/plugin%3Ahttp%7Cfetch') + ] + const received: Array = [] + const fetcher = createTauriFetch({ + nativeFetch: async (input) => { + received.push(input) + return new Response('native IPC') + } + }) + for (const input of inputs) expect(await (await fetcher(input)).text()).toBe('native IPC') + expect(received).toEqual(inputs) + }) + + test('does not bypass HTTP proxy for URLs merely containing the IPC hostname', async () => { + const received: string[] = [] + await mockTauriIPC((command, args) => { + expect(command).toBe('proxy_http_request') + received.push((args as InvokeArgs).request.url) + return { status: 204, headers: [], body: [] } + }) + const fetcher = createTauriFetch({ + nativeFetch: async () => { + throw new Error('External URL bypassed the HTTP proxy') + } + }) + const urls = ['https://ipc.localhost.example.test/', 'https://example.test/ipc.localhost'] + for (const url of urls) await fetcher(url) + expect(received).toEqual(urls) + }) + test('passes request timeout metadata to the desktop HTTP command', async () => { let captured: InvokeArgs | null = null await mockTauriIPC((command, args) => { @@ -55,6 +94,12 @@ describe('tauriFetch', () => { } }) + const response = await createTauriFetch({ timeoutMs: 15_000 })('https://example.test/check', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: '{"ok":true}' + }) + if (!captured) throw new Error('Expected proxy_http_request to be invoked') expect(response.status).toBe(201) expect(response.headers.get('x-open-pencil')).toBe('ok') @@ -65,6 +110,18 @@ describe('tauriFetch', () => { expect(captured.request.body).toEqual([...new TextEncoder().encode('{"ok":true}')]) }) + test('accepts URL objects without treating them as Requests', async () => { + let captured: InvokeArgs | null = null + await mockTauriIPC((command, args) => { + expect(command).toBe('proxy_http_request') + captured = args as InvokeArgs + return { status: 204, headers: [], body: [] } + }) + await tauriFetch(new URL('https://example.test/url-object')) + if (!captured) throw new Error('Expected proxy_http_request to be invoked') + expect(captured.request.url).toBe('https://example.test/url-object') + }) + test('forwards bodies from Request inputs', async () => { let captured: InvokeArgs | null = null await mockTauriIPC((command, args) => {