123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
- // SPDX-License-Identifier: Apache-2.0
- // SPDX-License-Identifier: MIT
- /**
- * Access the HTTP client written in Rust.
- *
- * This package is also accessible with `window.__TAURI__.http` when `tauri.conf.json > build > withGlobalTauri` is set to true.
- *
- * 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 { invokeTauriCommand } from './helpers/tauri'
- interface Duration {
- secs: number
- nanos: number
- }
- interface ClientOptions {
- maxRedirections?: number
- connectTimeout?: number | Duration
- }
- enum ResponseType {
- JSON = 1,
- Text = 2,
- Binary = 3
- }
- interface FilePart<T> {
- file: string | T
- mime?: string
- fileName?: string
- }
- type Part = string | Uint8Array | FilePart<Uint8Array>
- /** The body object to be used on POST and PUT requests. */
- 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-apps/api/http"
- * 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
- * }
- * });
- * ```
- *
- * @param data The body data.
- *
- * @return The body object ready to be used on the POST and PUT requests.
- */
- static form(data: Record<string, Part>): Body {
- const form: Record<string, string | number[] | FilePart<number[]>> = {}
- for (const key in data) {
- // eslint-disable-next-line security/detect-object-injection
- const v = data[key]
- let r
- if (typeof v === 'string') {
- r = v
- } else if (v instanceof Uint8Array || Array.isArray(v)) {
- r = Array.from(v)
- } 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 }
- }
- // eslint-disable-next-line security/detect-object-injection
- form[key] = r
- }
- return new Body('Form', form)
- }
- /**
- * Creates a new JSON body.
- * @example
- * ```typescript
- * import { Body } from "@tauri-apps/api/http"
- * Body.json({
- * registered: true,
- * name: 'tauri'
- * });
- * ```
- *
- * @param data The body JSON object.
- *
- * @return The body object ready to be used on the POST and PUT requests.
- */
- static json(data: Record<any, any>): Body {
- return new Body('Json', data)
- }
- /**
- * Creates a new UTF-8 string body.
- * @example
- * ```typescript
- * import { Body } from "@tauri-apps/api/http"
- * Body.text('The body content as a string');
- * ```
- *
- * @param data The body string.
- *
- * @return 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-apps/api/http"
- * Body.bytes(new Uint8Array([1, 2, 3]));
- * ```
- *
- * @param data The body byte array.
- *
- * @return The body object ready to be used on the POST and PUT requests.
- */
- static bytes(bytes: Iterable<number> | ArrayLike<number>): Body {
- // stringifying Uint8Array doesn't return an array of numbers, so we create one here
- return new Body('Bytes', Array.from(bytes))
- }
- }
- /** The request HTTP verb. */
- type HttpVerb =
- | 'GET'
- | 'POST'
- | 'PUT'
- | 'DELETE'
- | 'PATCH'
- | 'HEAD'
- | 'OPTIONS'
- | 'CONNECT'
- | 'TRACE'
- /** Options object sent to the backend. */
- interface HttpOptions {
- method: HttpVerb
- url: string
- headers?: Record<string, any>
- query?: Record<string, any>
- body?: Body
- timeout?: number | Duration
- responseType?: ResponseType
- }
- /** Request options. */
- type RequestOptions = Omit<HttpOptions, 'method' | 'url'>
- /** Options for the `fetch` API. */
- type FetchOptions = Omit<HttpOptions, 'url'>
- /** @ignore */
- interface IResponse<T> {
- url: string
- status: number
- headers: Record<string, string>
- rawHeaders: Record<string, string[]>
- data: T
- }
- /** Response object. */
- class Response<T> {
- /** 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<string, string>
- /** The response raw headers. */
- rawHeaders: Record<string, string[]>
- /** The response data. */
- data: T
- /** @ignore */
- constructor(response: IResponse<T>) {
- 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
- }
- }
- class Client {
- id: number
- /** @ignore */
- constructor(id: number) {
- this.id = id
- }
- /**
- * Drops the client instance.
- * @example
- * ```typescript
- * import { getClient } from '@tauri-apps/api/http';
- * const client = await getClient();
- * await client.drop();
- * ```
- *
- * @returns
- */
- async drop(): Promise<void> {
- return invokeTauriCommand({
- __tauriModule: 'Http',
- message: {
- cmd: 'dropClient',
- client: this.id
- }
- })
- }
- /**
- * Makes an HTTP request.
- * @example
- * ```typescript
- * import { getClient } from '@tauri-apps/api/http';
- * const client = await getClient();
- * const response = await client.request({
- * method: 'GET',
- * url: 'http://localhost:3003/users',
- * });
- * ```
- *
- * @param options The request options.
- * @returns A promise resolving to the response.
- */
- async request<T>(options: HttpOptions): Promise<Response<T>> {
- const jsonResponse =
- !options.responseType || options.responseType === ResponseType.JSON
- if (jsonResponse) {
- options.responseType = ResponseType.Text
- }
- return invokeTauriCommand<IResponse<T>>({
- __tauriModule: 'Http',
- message: {
- cmd: 'httpRequest',
- client: this.id,
- options
- }
- }).then((res) => {
- const response = new Response(res)
- if (jsonResponse) {
- /* eslint-disable */
- try {
- // @ts-expect-error
- response.data = JSON.parse(response.data as string)
- } catch (e) {
- if (response.ok && (response.data as unknown as string) === '') {
- // @ts-expect-error
- response.data = {}
- } 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-apps/api/http';
- * const client = await getClient();
- * const response = await client.get('http://localhost:3003/users', {
- * timeout: 30,
- * // the expected response type
- * responseType: ResponseType.JSON
- * });
- * ```
- *
- * @param url The request URL.
- * @param options The request options.
- * @returns A promise resolving to the response.
- */
- async get<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
- return this.request({
- method: 'GET',
- url,
- ...options
- })
- }
- /**
- * Makes a POST request.
- * @example
- * ```typescript
- * import { getClient, Body, ResponseType } from '@tauri-apps/api/http';
- * 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,
- * });
- * ```
- *
- * @param url The request URL.
- * @param body The body of the request.
- * @param options The request options.
- * @returns A promise resolving to the response.
- */
- async post<T>(
- url: string,
- body?: Body,
- options?: RequestOptions
- ): Promise<Response<T>> {
- return this.request({
- method: 'POST',
- url,
- body,
- ...options
- })
- }
- /**
- * Makes a PUT request.
- * @example
- * ```typescript
- * import { getClient, Body } from '@tauri-apps/api/http';
- * 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'
- * }
- * })
- * });
- * ```
- *
- * @param url The request URL.
- * @param body The body of the request.
- * @param options Request options.
- * @returns A promise resolving to the response.
- */
- async put<T>(
- url: string,
- body?: Body,
- options?: RequestOptions
- ): Promise<Response<T>> {
- return this.request({
- method: 'PUT',
- url,
- body,
- ...options
- })
- }
- /**
- * Makes a PATCH request.
- * @example
- * ```typescript
- * import { getClient, Body } from '@tauri-apps/api/http';
- * const client = await getClient();
- * const response = await client.patch('http://localhost:3003/users/1', {
- * body: Body.json({ email: 'contact@tauri.studio' })
- * });
- * ```
- *
- * @param url The request URL.
- * @param options The request options.
- * @returns A promise resolving to the response.
- */
- async patch<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
- return this.request({
- method: 'PATCH',
- url,
- ...options
- })
- }
- /**
- * Makes a DELETE request.
- * @example
- * ```typescript
- * import { getClient } from '@tauri-apps/api/http';
- * const client = await getClient();
- * const response = await client.delete('http://localhost:3003/users/1');
- * ```
- *
- * @param url The request URL.
- * @param options The request options.
- * @returns A promise resolving to the response.
- */
- async delete<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
- return this.request({
- method: 'DELETE',
- url,
- ...options
- })
- }
- }
- /**
- * Creates a new client using the specified options.
- * @example
- * ```typescript
- * import { getClient } from '@tauri-apps/api/http';
- * const client = await getClient();
- * ```
- *
- * @param options Client configuration.
- *
- * @return A promise resolving to the client instance.
- */
- async function getClient(options?: ClientOptions): Promise<Client> {
- return invokeTauriCommand<number>({
- __tauriModule: 'Http',
- message: {
- cmd: 'createClient',
- 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-apps/api/http';
- * const response = await fetch('http://localhost:3003/users/2', {
- * method: 'GET',
- * timeout: 30,
- * });
- * ```
- *
- * @param url The request URL.
- * @param options The fetch options.
- * @return The response object.
- */
- async function fetch<T>(
- url: string,
- options?: FetchOptions
- ): Promise<Response<T>> {
- 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, FilePart }
|