Merge remote-tracking branch 'origin/master' into release-ui-polish

This commit is contained in:
Danila Poyarkov 2026-09-13 16:42:59 +03:00
commit b16b2011c9
3 changed files with 93 additions and 3 deletions

View file

@ -56,6 +56,7 @@
### Fixed
- Avoid recursive desktop HTTP proxy requests when font downloads intercept Tauri IPC traffic.
- Keep FIT image fills proportional, centered, and fully visible without stretching or cropped edges.
- 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)

View file

@ -72,10 +72,20 @@ export function withAbortSignal<T>(promise: Promise<T>, 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) => tauriFetch(input, init, options.maxResponseBytes, options.timeoutMs)
return (input, init) =>
executeTauriFetch(
options.nativeFetch ?? nativeFetch,
input,
init,
options.maxResponseBytes,
options.timeoutMs
)
}
export async function tauriFetch(
@ -84,15 +94,44 @@ export async function tauriFetch(
maxResponseBytes?: number,
timeoutMs?: number
): Promise<Response> {
return executeTauriFetch(nativeFetch, input, init, maxResponseBytes, timeoutMs)
}
async function executeTauriFetch(
fetcher: FetchFunction,
input: RequestInfo | URL,
init?: RequestInit,
maxResponseBytes?: number,
timeoutMs?: number
): Promise<Response> {
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 fetcher(input, init)
}
const request = new Request(input, init)
request.signal.throwIfAborted()
const { invoke } = await import('@tauri-apps/api/core')
request.signal.throwIfAborted()
// 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()
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())],
headers,
body: bodyData ? Array.from(bodyData) : undefined,
max_response_bytes: maxResponseBytes,
follow_redirects: request.redirect === 'follow',
timeout_ms: timeoutMs

View file

@ -44,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<RequestInfo | URL> = []
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) => {
@ -72,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) => {