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:
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user