From 5ff18b8f3210359853ccf08c3f0f5d6d0631681e Mon Sep 17 00:00:00 2001 From: wfjack Date: Tue, 2 Jun 2026 21:59:17 +0800 Subject: [PATCH] fix(readwise): use Tauri HTTP transport in desktop app (#4413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch ReadwiseClient to @tauri-apps/plugin-http when running in Tauri desktop mode, matching the pattern already used by WebDAV, Hardcover, AI providers, translators, OPDS, and KOReader sync. In a Tauri webview context the standard window.fetch is still subject to CORS preflight rules and—on Android—the platform's cleartext-traffic policy. @tauri-apps/plugin-http sends the request from the Rust side via reqwest, bypassing the renderer entirely (no Origin header, no preflight, no cleartext block). ReadwiseClient was one of the few remaining services that had not yet adopted this transport, so users on the desktop build could hit CORS or network errors when validating tokens or pushing highlights. Co-authored-by: Claude Opus 4.6 (1M context) --- .../services/readwise/ReadwiseClient.test.ts | 30 +++++++++++++++++++ .../src/services/readwise/ReadwiseClient.ts | 5 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/readest-app/src/__tests__/services/readwise/ReadwiseClient.test.ts b/apps/readest-app/src/__tests__/services/readwise/ReadwiseClient.test.ts index 639c7fd0..801d5f88 100644 --- a/apps/readest-app/src/__tests__/services/readwise/ReadwiseClient.test.ts +++ b/apps/readest-app/src/__tests__/services/readwise/ReadwiseClient.test.ts @@ -1,9 +1,19 @@ import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; import { ReadwiseClient } from '@/services/readwise/ReadwiseClient'; import { READWISE_API_BASE_URL } from '@/services/constants'; +import { isTauriAppPlatform } from '@/services/environment'; import type { ReadwiseSettings } from '@/types/settings'; import type { Book, BookNote } from '@/types/book'; +vi.mock('@tauri-apps/plugin-http', () => ({ + fetch: vi.fn(), +})); + +vi.mock('@/services/environment', () => ({ + isTauriAppPlatform: vi.fn(() => false), +})); + const makeSettings = (overrides: Partial = {}): ReadwiseSettings => ({ enabled: true, accessToken: 'test-token', @@ -37,6 +47,10 @@ describe('ReadwiseClient base URL', () => { beforeEach(() => { fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); vi.stubGlobal('fetch', fetchMock); + vi.mocked(tauriFetch) + .mockReset() + .mockResolvedValue(new Response(null, { status: 204 })); + vi.mocked(isTauriAppPlatform).mockReturnValue(false); }); afterEach(() => { @@ -79,4 +93,20 @@ describe('ReadwiseClient base URL', () => { expect(init.method).toBe('POST'); expect((init.headers as Record)['Authorization']).toBe('Token test-token'); }); + + test('validateToken uses the Tauri HTTP transport in the desktop app', async () => { + vi.mocked(isTauriAppPlatform).mockReturnValue(true); + const client = new ReadwiseClient(makeSettings({ baseUrl: 'https://example.com/api/v2' })); + + await client.validateToken(); + + expect(tauriFetch).toHaveBeenCalledWith('https://example.com/api/v2/auth/', { + method: 'GET', + headers: { + Authorization: 'Token test-token', + }, + body: undefined, + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); }); diff --git a/apps/readest-app/src/services/readwise/ReadwiseClient.ts b/apps/readest-app/src/services/readwise/ReadwiseClient.ts index c6e4202e..4a8e8a8f 100644 --- a/apps/readest-app/src/services/readwise/ReadwiseClient.ts +++ b/apps/readest-app/src/services/readwise/ReadwiseClient.ts @@ -1,7 +1,9 @@ import { Book, BookNote, HighlightColor } from '@/types/book'; import { ReadwiseSettings } from '@/types/settings'; import { READWISE_API_BASE_URL } from '@/services/constants'; +import { isTauriAppPlatform } from '@/services/environment'; import { buildAnnotationWebUrl } from '@/utils/deeplink'; +import { fetch as tauriFetch } from '@tauri-apps/plugin-http'; const READEST_TO_READWISE_COLOR: Record = { red: 'pink', @@ -34,7 +36,8 @@ export class ReadwiseClient { options: { method?: 'GET' | 'POST'; body?: string } = {}, ): Promise { const { method = 'GET', body } = options; - return fetch(`${this.baseUrl}${endpoint}`, { + const fetchFn = isTauriAppPlatform() ? tauriFetch : globalThis.fetch; + return fetchFn(`${this.baseUrl}${endpoint}`, { method, headers: { Authorization: `Token ${this.config.accessToken}`,