fix(readwise): use Tauri HTTP transport in desktop app (#4413)

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) <noreply@anthropic.com>
This commit is contained in:
wfjack
2026-06-02 21:59:17 +08:00
committed by GitHub
parent 274afb0677
commit 5ff18b8f32
2 changed files with 34 additions and 1 deletions
@@ -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> = {}): 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<string, string>)['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();
});
});
@@ -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<HighlightColor, string> = {
red: 'pink',
@@ -34,7 +36,8 @@ export class ReadwiseClient {
options: { method?: 'GET' | 'POST'; body?: string } = {},
): Promise<Response> {
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}`,