// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT /** * Access the HTTP client written in Rust. * * The APIs must be allowlisted on `tauri.conf.json`: * ```json * { * "tauri": { * "allowlist": { * "http": { * "all": true, // enable all http APIs * "request": true // enable HTTP request API * } * } * } * } * ``` * It is recommended to allowlist only the APIs you use for optimal bundle size and security. * * ## Security * * This API has a scope configuration that forces you to restrict the URLs and paths that can be accessed using glob patterns. * * For instance, this scope configuration only allows making HTTP requests to the GitHub API for the `tauri-apps` organization: * ```json * { * "tauri": { * "allowlist": { * "http": { * "scope": ["https://api.github.com/repos/tauri-apps/*"] * } * } * } * } * ``` * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access. * * @module */ import { invoke } from '@tauri-apps/api/tauri' /** * @since 1.0.0 */ interface Duration { secs: number nanos: number } /** * @since 1.0.0 */ interface ClientOptions { /** * Defines the maximum number of redirects the client should follow. * If set to 0, no redirects will be followed. */ maxRedirections?: number connectTimeout?: number | Duration } /** * @since 1.0.0 */ enum ResponseType { JSON = 1, Text = 2, Binary = 3 } /** * @since 1.0.0 */ interface FilePart { file: string | T mime?: string fileName?: string } type Part = string | Uint8Array | FilePart /** * The body object to be used on POST and PUT requests. * * @since 1.0.0 */ class Body { type: string payload: unknown /** @ignore */ private constructor(type: string, payload: unknown) { this.type = type this.payload = payload } /** * Creates a new form data body. The form data is an object where each key is the entry name, * and the value is either a string or a file object. * * By default it sets the `application/x-www-form-urlencoded` Content-Type header, * but you can set it to `multipart/form-data` if the Cargo feature `http-multipart` is enabled. * * Note that a file path must be allowed in the `fs` allowlist scope. * @example * ```typescript * import { Body } from "tauri-plugin-http-api" * const body = Body.form({ * key: 'value', * image: { * file: '/path/to/file', // either a path or an array buffer of the file contents * mime: 'image/jpeg', // optional * fileName: 'image.jpg' // optional * } * }); * * // alternatively, use a FormData: * const form = new FormData(); * form.append('key', 'value'); * form.append('image', file, 'image.png'); * const formBody = Body.form(form); * ``` * * @param data The body data. * * @returns The body object ready to be used on the POST and PUT requests. */ static form(data: Record | FormData): Body { const form: Record> = {} const append = ( key: string, v: string | Uint8Array | FilePart | File ): void => { if (v !== null) { let r if (typeof v === 'string') { r = v } else if (v instanceof Uint8Array || Array.isArray(v)) { r = Array.from(v) } else if (v instanceof File) { r = { file: v.name, mime: v.type, fileName: v.name } } else if (typeof v.file === 'string') { r = { file: v.file, mime: v.mime, fileName: v.fileName } } else { r = { file: Array.from(v.file), mime: v.mime, fileName: v.fileName } } form[String(key)] = r } } if (data instanceof FormData) { for (const [key, value] of data) { append(key, value) } } else { for (const [key, value] of Object.entries(data)) { append(key, value) } } return new Body('Form', form) } /** * Creates a new JSON body. * @example * ```typescript * import { Body } from "tauri-plugin-http-api" * Body.json({ * registered: true, * name: 'tauri' * }); * ``` * * @param data The body JSON object. * * @returns The body object ready to be used on the POST and PUT requests. */ static json(data: Record): Body { return new Body('Json', data) } /** * Creates a new UTF-8 string body. * @example * ```typescript * import { Body } from "tauri-plugin-http-api" * Body.text('The body content as a string'); * ``` * * @param value The body string. * * @returns The body object ready to be used on the POST and PUT requests. */ static text(value: string): Body { return new Body('Text', value) } /** * Creates a new byte array body. * @example * ```typescript * import { Body } from "tauri-plugin-http-api" * Body.bytes(new Uint8Array([1, 2, 3])); * ``` * * @param bytes The body byte array. * * @returns The body object ready to be used on the POST and PUT requests. */ static bytes( bytes: Iterable | ArrayLike | ArrayBuffer ): Body { // stringifying Uint8Array doesn't return an array of numbers, so we create one here return new Body( 'Bytes', Array.from(bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes) ) } } /** The request HTTP verb. */ type HttpVerb = | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'CONNECT' | 'TRACE' /** * Options object sent to the backend. * * @since 1.0.0 */ interface HttpOptions { method: HttpVerb url: string headers?: Record query?: Record body?: Body timeout?: number | Duration responseType?: ResponseType } /** Request options. */ type RequestOptions = Omit /** Options for the `fetch` API. */ type FetchOptions = Omit /** @ignore */ interface IResponse { url: string status: number headers: Record rawHeaders: Record data: T } /** * Response object. * * @since 1.0.0 * */ class Response { /** The request URL. */ url: string /** The response status code. */ status: number /** A boolean indicating whether the response was successful (status in the range 200–299) or not. */ ok: boolean /** The response headers. */ headers: Record /** The response raw headers. */ rawHeaders: Record /** The response data. */ data: T /** @ignore */ constructor(response: IResponse) { this.url = response.url this.status = response.status this.ok = this.status >= 200 && this.status < 300 this.headers = response.headers this.rawHeaders = response.rawHeaders this.data = response.data } } /** * @since 1.0.0 */ class Client { id: number /** @ignore */ constructor(id: number) { this.id = id } /** * Drops the client instance. * @example * ```typescript * import { getClient } from 'tauri-plugin-http-api'; * const client = await getClient(); * await client.drop(); * ``` */ async drop(): Promise { return invoke('plugin:http|drop_client', { client: this.id }) } /** * Makes an HTTP request. * @example * ```typescript * import { getClient } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.request({ * method: 'GET', * url: 'http://localhost:3003/users', * }); * ``` */ async request(options: HttpOptions): Promise> { const jsonResponse = !options.responseType || options.responseType === ResponseType.JSON if (jsonResponse) { options.responseType = ResponseType.Text } return invoke>('plugin:http|request', { client: this.id, options }).then((res) => { const response = new Response(res) if (jsonResponse) { /* eslint-disable */ try { response.data = JSON.parse(response.data as string) } catch (e) { if (response.ok && (response.data as unknown as string) === '') { response.data = {} as T } else if (response.ok) { throw Error( `Failed to parse response \`${response.data}\` as JSON: ${e}; try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.` ) } } /* eslint-enable */ return response } return response }) } /** * Makes a GET request. * @example * ```typescript * import { getClient, ResponseType } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.get('http://localhost:3003/users', { * timeout: 30, * // the expected response type * responseType: ResponseType.JSON * }); * ``` */ async get(url: string, options?: RequestOptions): Promise> { return this.request({ method: 'GET', url, ...options }) } /** * Makes a POST request. * @example * ```typescript * import { getClient, Body, ResponseType } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.post('http://localhost:3003/users', { * body: Body.json({ * name: 'tauri', * password: 'awesome' * }), * // in this case the server returns a simple string * responseType: ResponseType.Text, * }); * ``` */ async post( url: string, body?: Body, options?: RequestOptions ): Promise> { return this.request({ method: 'POST', url, body, ...options }) } /** * Makes a PUT request. * @example * ```typescript * import { getClient, Body } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.put('http://localhost:3003/users/1', { * body: Body.form({ * file: { * file: '/home/tauri/avatar.png', * mime: 'image/png', * fileName: 'avatar.png' * } * }) * }); * ``` */ async put( url: string, body?: Body, options?: RequestOptions ): Promise> { return this.request({ method: 'PUT', url, body, ...options }) } /** * Makes a PATCH request. * @example * ```typescript * import { getClient, Body } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.patch('http://localhost:3003/users/1', { * body: Body.json({ email: 'contact@tauri.app' }) * }); * ``` */ async patch(url: string, options?: RequestOptions): Promise> { return this.request({ method: 'PATCH', url, ...options }) } /** * Makes a DELETE request. * @example * ```typescript * import { getClient } from 'tauri-plugin-http-api'; * const client = await getClient(); * const response = await client.delete('http://localhost:3003/users/1'); * ``` */ async delete(url: string, options?: RequestOptions): Promise> { return this.request({ method: 'DELETE', url, ...options }) } } /** * Creates a new client using the specified options. * @example * ```typescript * import { getClient } from 'tauri-plugin-http-api'; * const client = await getClient(); * ``` * * @param options Client configuration. * * @returns A promise resolving to the client instance. * * @since 1.0.0 */ async function getClient(options?: ClientOptions): Promise { return invoke('plugin:http|create_client', { options }).then((id) => new Client(id)) } /** @internal */ let defaultClient: Client | null = null /** * Perform an HTTP request using the default client. * @example * ```typescript * import { fetch } from 'tauri-plugin-http-api'; * const response = await fetch('http://localhost:3003/users/2', { * method: 'GET', * timeout: 30, * }); * ``` */ async function fetch( url: string, options?: FetchOptions ): Promise> { if (defaultClient === null) { defaultClient = await getClient() } return defaultClient.request({ url, method: options?.method ?? 'GET', ...options }) } export type { Duration, ClientOptions, Part, HttpVerb, HttpOptions, RequestOptions, FetchOptions } export { getClient, fetch, Body, Client, Response, ResponseType, type FilePart }