Working with Responses
Process HTTP responses returned by the Tauri HTTP plugin - parse JSON bodies, read headers, check status codes, and handle errors properly in a React frontend.
When you call fetch() from @tauri-apps/plugin-http, the returned Promise resolves to a Response object. This object is designed to mirror the standard Web Fetch API Response as closely as possible. Understanding how to inspect and consume that response is the key to building any data-driven Tauri app.
The response tells you three things:
- Metadata: the HTTP status code, status text, and a
Headersobject. - Body content: the raw payload that the server sent, which you can read in several formats.
- Success or failure: whether the request itself completed (even if the server returned an error status).
Not the browser’s Response:
Although the API looks identical, the response is constructed inside Tauri’s Rust backend. It does not go through the browser’s HTTP stack, so CORS headers are irrelevant and there is no response.type property like cors or opaque.
Reading the Response Body
The response body arrives as raw bytes. You choose which format you want to decode those bytes into by calling one of the body‑reading methods. Every method returns a Promise because the body might not have fully arrived yet when the Response object is created.
JSON
Most REST APIs send data in JSON format. Use response.json() to parse the body directly into a JavaScript object or array.
// src/App.tsx
import { fetch } from '@tauri-apps/plugin-http';
import { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
}
export default function App() {
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadUser() {
try {
const response = await fetch('https://api.example.com/users/2', {
method: 'GET',
});
const data: User = await response.json();
setUser(data);
} catch (err) {
setError(String(err));
}
}
loadUser();
}, []);
if (error) return <div>Error: {error}</div>;
if (!user) return <div>Loading…</div>;
return <div>Welcome, {user.name}</div>;
}
The call to response.json() will throw if the body is not valid JSON. That means you need to handle the error, especially when an API might return an HTML error page or plain text with a non‑200 status.
A successful parse is not a successful request:
response.json() can succeed even when the HTTP status is 404 or 500 — the server might still return well‑formed JSON describing the error. Always check response.ok or response.status before trusting the data.
Plain Text
For endpoints that return text, logs, or HTML, use response.text().
const response = await fetch('https://example.com/api/logs');
const rawLogs = await response.text();
console.log(rawLogs);
Binary Data
When you download images, PDFs, or other binary content, response.blob() or response.arrayBuffer() are the right tools.
response.blob()returns aBlobthat you can use to create object URLs or pass to other Web APIs.response.arrayBuffer()gives you a low‑levelArrayBuffer, useful when you need to manipulate bytes in Rust via a Tauri command.
// Download an image and display it
const imgResponse = await fetch('https://example.com/photo.png');
const imgBlob = await imgResponse.blob();
const objectUrl = URL.createObjectURL(imgBlob);
// use objectUrl in an <img> tag
Read the body only once:
A Response body can be consumed once. Calling json() and then text() on the same response will throw an Illegal invocation error. If you need the data in two formats, read it once (e.g., as text()) and then parse the string yourself.
Form Data
If an API returns multipart/form‑data, you can attempt to parse it with response.formData(). This is rare for REST APIs but can appear when handling file upload responses. The method returns a FormData object you can iterate over.
Working with Headers
The response.headers property is a standard Headers object. You can read headers, check for their existence, and iterate over them.
const response = await fetch('https://api.example.com/data');
console.log(response.headers.get('content-type')); // "application/json"
console.log(response.headers.has('x-ratelimit-remaining')); // true or false
response.headers.forEach((value, name) => {
console.log(`${name}: ${value}`);
});
Headers are case‑insensitive. Common use‑cases:
- Pagination: many APIs expose
Linkheaders for next/previous pages. - Rate limiting: headers like
X-RateLimit-Remainingtell you how many requests you have left. - Caching:
ETagandLast-Modifiedhelp you avoid re‑downloading unchanged data.
Headers arrive before the body:
You can inspect response.headers immediately after fetch() resolves, without waiting for the body to be consumed. That’s why it’s safe to use headers to decide how to read the body.
Inspecting Status Codes
Every response carries an HTTP status code accessible through three properties:
response.status— the numeric code (200, 404, 500, …)response.statusText— the canonical reason phrase (e.g.,"OK","Not Found")response.ok— a boolean that istruewhen the status is between 200 and 299 A common pattern is to guard against unsuccessful requests right after fetching:
const response = await fetch('https://api.example.com/item/42');
if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
}
const data = await response.json();
This ensures that you never try to parse a body that represents an error page as if it were successful data.
Network errors vs. server errors:
response.ok being false does not cause fetch() to throw. Only network failures, permission denials, or invalid URLs throw an exception. A 500 response is a resolved promise with ok: false. You must explicitly check for it.
Error Handling
Errors when working with responses fall into three categories.
Network and Infrastructure Errors
These happen when the request cannot reach the server at all — the device is offline, the DNS lookup fails, or the TLS handshake is rejected. The fetch() promise rejects with an error message describing the failure.
try {
const response = await fetch('https://unreachable.example.com/data');
} catch (err) {
console.error('Network error:', err);
// err is a string, e.g. "error sending request for url (https://...)"
}
Permission Denials
If the target URL is not allowed by the http:default scope you configured in capabilities, the promise rejects with a permission error before any network request is made. This is a security feature; the error message will clearly state that the URL is not in the allow‑list.
Silent failures are possible during development:
If you see errors like url not allowed on the configured scope, double‑check your src-tauri/capabilities/default.json. The allow patterns must match the exact URL, including the scheme and path prefix if specified.
Body‑Parsing Errors
A server may respond with a status 200 but send malformed JSON. When you call response.json(), the method attempts to parse the bytes. If the bytes are not valid JSON, the promise rejects with a SyntaxError. Always wrap json() in a try/catch or chain a .catch().
async function safeFetchJson(url: string) {
const response = await fetch(url);
if (!response.ok) {
// Try to extract an error message from the body
let errorMessage = `Request failed with status ${response.status}`;
try {
const errorBody = await response.json();
if (errorBody.message) {
errorMessage = errorBody.message;
}
} catch {
// ignore — the body wasn’t JSON
}
throw new Error(errorMessage);
}
return response.json();
}
This pattern handles both the case where the body is valid JSON (maybe a machine‑readable error) and the case where it isn’t (e.g., an HTML error page). The outer try/catch in the caller still catches any remaining parsing failures.
Aborted Requests
If you cancel a request using AbortController, the fetch() promise rejects with an AbortError. You can distinguish this from other errors by checking the error message, which will contain "aborted".
Practical Component with All States
Below is a complete React component that fetches a list of users and handles loading, success, empty data, network error, HTTP error, and JSON parse error. It uses fetch from @tauri-apps/plugin-http and manages state explicitly.
// src/UsersList.tsx
import { fetch } from '@tauri-apps/plugin-http';
import { useEffect, useState } from 'react';
interface User {
id: number;
name: string;
}
type LoadState =
| { type: 'loading' }
| { type: 'success'; users: User[] }
| { type: 'error'; message: string };
export default function UsersList() {
const [state, setState] = useState<LoadState>({ type: 'loading' });
useEffect(() => {
let cancelled = false;
async function loadUsers() {
try {
const response = await fetch('https://api.example.com/users', {
method: 'GET',
connectTimeout: 15_000,
});
if (!response.ok) {
// Try to get a JSON error body
let detail = `${response.status} ${response.statusText}`;
try {
const errBody = await response.json();
if (errBody.error) detail = errBody.error;
} catch { /* not JSON, keep raw status */ }
throw new Error(detail);
}
const users: User[] = await response.json();
if (!cancelled) setState({ type: 'success', users });
} catch (err) {
if (!cancelled) setState({ type: 'error', message: String(err) });
}
}
loadUsers();
return () => { cancelled = true; };
}, []);
if (state.type === 'loading') return <div>Loading users…</div>;
if (state.type === 'error') return <div>Error: {state.message}</div>;
if (state.users.length === 0) return <div>No users found.</div>;
return (
<ul>
{state.users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
The component uses a cancelled flag to avoid setting state after unmount. connectTimeout is passed as part of ClientOptions to avoid hanging indefinitely if the server is unresponsive. The error handling attempts to extract a meaningful message from the response body, making the UI more helpful.
Summary
Processing HTTP responses in Tauri v2 centers on the Response object returned by fetch(). The body must be read with exactly one method (json, text, blob, arrayBuffer, or formData), and headers are available immediately. Status codes are inspected through status, statusText, and the convenience ok boolean.
The most frequent mistake is assuming a 200 response always contains valid JSON. Combine a status check with a guarded parse, and you’ll have reliable data extraction. When things go wrong, separate the error into its source — network, permission, HTTP status, or malformed body — to give your users a clear message.
If you have not yet configured the URL scope, the plugin will reject any request before it leaves the app. Head back to the HTTP Plugin setup to set allowed origins. Once that’s solid, the patterns here let you integrate any REST or HTTP‑based API into your React‑powered Tauri application.