Skip to content

HTTP Requests in Tauri

Hi, I'm Terence. This article looks at how to make HTTP requests in a Tauri application.

Introduction

Earlier articles in this series focused more on application structure and architecture. Here we move one level closer to actual application behavior by integrating HTTP support, which allows a desktop application to communicate with remote servers.

Install dependencies

Adding HTTP support in Tauri is actually simple.

You can use standard request libraries such as Axios, but adapter behavior can become tricky after packaging. For reliability, I recommend using Tauri's own HTTP API because it fits the runtime model directly.

One trade-off is that requests made through Tauri's Rust-backed HTTP layer are not visible in the browser-style WebView network console. Even so, it is the officially recommended approach and avoids a lot of subtle runtime issues.

bash
npm install -D @tauri-apps/api

Update configuration

After installing the dependency, you still need to allow outbound requests explicitly. In practice, you are telling Tauri which HTTP and HTTPS targets are allowed.

Example src-tauri/tauri.conf.json:

json
{
  "tauri": {
    "allowlist": {
      "http": {
        "all": true,
        "request": true,
        "scope": [
          "http://localhost/*",
          "http://jsonplaceholder.typicode.com/*"
        ]
      }
    }
  }
}

Build a simple request wrapper

The implementation idea is very similar to a normal frontend HTTP client: create a small wrapper class, then expose helpers on top of it.

src/http/client.ts

typescript
import { fetch, FetchOptions, Response } from "@tauri-apps/api/http";

interface RequestInterceptor {
  onRequest(config: FetchOptions): FetchOptions | Promise<FetchOptions>;
}

interface ResponseInterceptor {
  onResponse<T>(response: Response<T>): Response<T> | Promise<Response<T>>;
  onError(error: Error): void | Promise<void>;
}

class HttpClient {
  private baseURL: string = "";
  private requestInterceptors: RequestInterceptor[] = [];
  private responseInterceptors: ResponseInterceptor[] = [];

  private constructor() {}

  private static instance: HttpClient;

  static getInstance(): HttpClient {
    if (!HttpClient.instance) {
      HttpClient.instance = new HttpClient();
    }
    return HttpClient.instance;
  }

  setBaseURL(baseURL: string): void {
    this.baseURL = baseURL;
  }

  addRequestInterceptor(interceptor: RequestInterceptor): void {
    this.requestInterceptors.push(interceptor);
  }

  addResponseInterceptor(interceptor: ResponseInterceptor): void {
    this.responseInterceptors.push(interceptor);
  }

  private async executeRequestInterceptors(config: FetchOptions): Promise<FetchOptions> {
    for (const interceptor of this.requestInterceptors) {
      config = await interceptor.onRequest(config);
    }
    return config;
  }

  private async executeResponseInterceptors<T>(response: Response<T>): Promise<Response<T>> {
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor.onResponse(response);
    }
    return response;
  }

  private async executeErrorInterceptors(error: Error): Promise<void> {
    for (const interceptor of this.responseInterceptors) {
      await interceptor.onError(error);
    }
  }

  async request<T>(url: string, options: FetchOptions): Promise<T> {
    const config = await this.executeRequestInterceptors(options);

    try {
      const response = await fetch<T>(this.baseURL + url, config);
      const processedResponse = await this.executeResponseInterceptors(response);
      return processedResponse.data;
    } catch (error) {
      await this.executeErrorInterceptors(error as Error);
      throw error;
    }
  }
}

const httpClient = HttpClient.getInstance();
export default httpClient;

Then expose a small public API:

src/http/index.ts

typescript
import httpClient from "./client";
import { FetchOptions, Response, ResponseType, Body } from "@tauri-apps/api/http";

httpClient.setBaseURL("http://jsonplaceholder.typicode.com");

httpClient.addRequestInterceptor({
  onRequest(config: FetchOptions): FetchOptions {
    config.headers = {
      ...config.headers,
    };
    return config;
  },
});

httpClient.addResponseInterceptor({
  onResponse<T>(response: Response<T>): Response<T> {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response;
  },
  onError(error: Error): void {
    console.error("HTTP request error:", error);
    throw error;
  },
});

const get = async <T>(url: string) => {
  return httpClient.request<T>(url, {
    method: "GET",
    responseType: ResponseType.JSON,
  });
};

const post = async <T>(url: string, data: Record<string, unknown>) => {
  return httpClient.request<T>(url, {
    method: "POST",
    body: Body.json(data),
    responseType: ResponseType.JSON,
  });
};

export default {
  get,
  post,
};

The core ideas are the same as in ordinary frontend request encapsulation:

  • set a base URL
  • use interceptors
  • expose simple get / post helpers

Use it in a page

tsx
import { useState } from "react";
import { Button, Card } from "@douyinfe/semi-ui";
import http from "@/http";

const Http = () => {
  const [getResponse, setGetResponse] = useState<string>("");
  const [postResponse, setPostResponse] = useState<string>("");

  const getFetchTest = () => {
    http.get("/posts/2").then((res) => {
      setGetResponse(JSON.stringify(res));
    });
  };

  const postFetchTest = () => {
    http
      .post("/posts", {
        id: 1,
        title: "foo",
        body: "bar",
      })
      .then((res) => {
        setPostResponse(JSON.stringify(res));
      });
  };

  return (
    <div>
      <Button onClick={getFetchTest}>Run GET request test</Button>
      <Card title="GET response data" style={{ marginTop: 10 }}>
        {getResponse}
      </Card>
      <div style={{ marginTop: 20 }}>
        <Button onClick={postFetchTest}>Run POST request test</Button>
        <Card title="POST response data" style={{ marginTop: 10 }}>
          {postResponse}
        </Card>
      </div>
    </div>
  );
};

export default Http;

At this point, HTTP support in Tauri is effectively in place.

Illustration of an XTools HTTP demo screen in English

Source code

https://github.com/Xutaotaotao/XTools/tree/feature-http

Closing

This article only implements the basic HTTP layer in Tauri, but once that piece is in place, the desktop application becomes far more capable. A desktop client with clean remote communication can do a lot more than a local-only tool.

If you are exploring Tauri and run into questions around HTTP, this is a good place to start.

Last updated: