@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
/ * *
* Access the HTTP client written in Rust .
* Make HTTP requests with the Rust backend .
*
* # # Security
*
@ -31,518 +31,94 @@ declare global {
}
/ * *
* Options to configure the Rust client used to make fetch requests
*
* @since 2.0 . 0
* /
interface Duration {
secs : number ;
nanos : number ;
}
/ * *
* @since 2.0 . 0
* /
interface ClientOptions {
export 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 ;
/** Timeout in milliseconds */
connectTimeout? : number ;
}
/ * *
* @since 2.0 . 0
* /
enum ResponseType {
JSON = 1 ,
Text = 2 ,
Binary = 3 ,
}
/ * *
* @since 2.0 . 0
* /
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 .
* Fetch a resource from the network . It returns a ` Promise ` that resolves to the
* ` Response ` to that ` Request ` , whether it is successful or not .
*
* @since 2.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 ` multipart ` is enabled .
*
* Note that a file path must be allowed in the ` fs ` scope .
* @example
* ` ` ` typescript
* import { Body } from "@tauri-apps/plugin-http"
* 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 .
*
* @since 2.0 . 0
* /
static form ( data : Record < string , Part > | FormData ) : Body {
const form : Record < string , string | number [ ] | FilePart < number [ ] > > = { } ;
const append = (
key : string ,
v : string | Uint8Array | FilePart < Uint8Array > | 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-apps/plugin-http"
* 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 .
*
* @since 2.0 . 0
* /
static json < K extends string | number | symbol , V > ( data : Record < K , V > ) : Body {
return new Body ( "Json" , data ) ;
}
/ * *
* Creates a new UTF - 8 string body .
* @example
* ` ` ` typescript
* import { Body } from "@tauri-apps/plugin-http"
* 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 .
*
* @since 2.0 . 0
* /
static text ( value : string ) : Body {
return new Body ( "Text" , value ) ;
}
/ * *
* Creates a new byte array body .
* @example
* ` ` ` typescript
* import { Body } from "@tauri-apps/plugin-http"
* 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 .
*
* @since 2.0 . 0
* /
static bytes (
bytes : Iterable < number > | ArrayLike < number > | 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 .
* @example
* ` ` ` typescript
* const response = await fetch ( "http://my.json.host/data.json" ) ;
* console . log ( response . status ) ; // e.g. 200
* console . log ( response . statusText ) ; // e.g. "OK"
* const jsonData = await response . json ( ) ;
* ` ` `
*
* @since 2.0 . 0
* /
interface HttpOptions {
method : HttpVerb ;
url : string ;
headers? : Record < string , unknown > ;
query? : Record < string , unknown > ;
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 .
*
* @since 2.0 . 0
* * /
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 ;
}
}
/ * *
* @since 2.0 . 0
* /
class Client {
id : number ;
/** @ignore */
constructor ( id : number ) {
this . id = id ;
}
/ * *
* Drops the client instance .
* @example
* ` ` ` typescript
* import { getClient } from '@tauri-apps/plugin-http' ;
* const client = await getClient ( ) ;
* await client . drop ( ) ;
* ` ` `
* /
async drop ( ) : Promise < void > {
return window . __TAURI_INVOKE__ ( "plugin:http|drop_client" , {
client : this.id ,
} ) ;
}
/ * *
* Makes an HTTP request .
* @example
* ` ` ` typescript
* import { getClient } from '@tauri-apps/plugin-http' ;
* const client = await getClient ( ) ;
* const response = await client . request ( {
* method : 'GET' ,
* url : 'http://localhost:3003/users' ,
* } ) ;
* ` ` `
* /
async request < T > ( options : HttpOptions ) : Promise < Response < T > > {
const jsonResponse =
! options . responseType || options . responseType === ResponseType . JSON ;
if ( jsonResponse ) {
options . responseType = ResponseType . Text ;
}
return window
. __TAURI_INVOKE__ < IResponse < T > > ( "plugin:http|request" , {
clientId : 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-apps/plugin-http' ;
* const client = await getClient ( ) ;
* const response = await client . get ( 'http://localhost:3003/users' , {
* timeout : 30 ,
* // the expected response type
* responseType : ResponseType.JSON
* } ) ;
* ` ` `
* /
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/plugin-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 ,
* } ) ;
* ` ` `
* /
async post < T > (
url : string ,
body? : Body ,
options? : RequestOptions ,
) : Promise < Response < T > > {
return this . request ( {
method : "POST" ,
url ,
body ,
. . . options ,
} ) ;
}
export async function fetch (
input : URL | Request | string ,
init? : RequestInit & ClientOptions ,
) : Promise < Response > {
const maxRedirections = init ? . maxRedirections ;
const connectTimeout = init ? . maxRedirections ;
// Remove these fields before creating the request
if ( init ) {
delete init . maxRedirections ;
delete init . connectTimeout ;
}
const req = new Request ( input , init ) ;
const buffer = await req . arrayBuffer ( ) ;
const reqData = buffer . byteLength ? Array . from ( new Uint8Array ( buffer ) ) : null ;
const rid = await window . __TAURI_INVOKE__ < number > ( "plugin:http|fetch" , {
cmd : "fetch" ,
method : req.method ,
url : req.url ,
headers : Array.from ( req . headers . entries ( ) ) ,
data : reqData ,
maxRedirections ,
connectTimeout ,
} ) ;
/ * *
* Makes a PUT request .
* @example
* ` ` ` typescript
* import { getClient , Body } from '@tauri-apps/plugin-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'
* }
* } )
* } ) ;
* ` ` `
* /
async put < T > (
url : string ,
body? : Body ,
options? : RequestOptions ,
) : Promise < Response < T > > {
return this . request ( {
method : "PUT" ,
url ,
body ,
. . . options ,
req . signal . addEventListener ( "abort" , ( ) = > {
window . __TAURI_INVOKE__ ( "plugin:http|fetch_cancel" , {
rid ,
} ) ;
}
} ) ;
/ * *
* Makes a PATCH request .
* @example
* ` ` ` typescript
* import { getClient , Body } from '@tauri-apps/plugin-http' ;
* const client = await getClient ( ) ;
* const response = await client . patch ( 'http://localhost:3003/users/1' , {
* body : Body.json ( { email : 'contact@tauri.app' } )
* } ) ;
* ` ` `
* /
async patch < T > ( url : string , options? : RequestOptions ) : Promise < Response < T > > {
return this . request ( {
method : "PATCH" ,
url ,
. . . options ,
} ) ;
interface FetchSendResponse {
status : number ;
statusText : string ;
headers : [ [ string , string ] ] ;
url : string ;
}
/ * *
* Makes a DELETE request .
* @example
* ` ` ` typescript
* import { getClient } from '@tauri-apps/plugin-http' ;
* const client = await getClient ( ) ;
* const response = await client . delete ( 'http://localhost:3003/users/1' ) ;
* ` ` `
* /
async delete < T > ( url : string , options? : RequestOptions ) : Promise < Response < T > > {
return this . request ( {
method : "DELETE" ,
url ,
. . . options ,
const { status , statusText , url , headers } =
await window . __TAURI_INVOKE__ < FetchSendResponse > ( "plugin:http|fetch_send" , {
rid ,
} ) ;
}
}
/ * *
* Creates a new client using the specified options .
* @example
* ` ` ` typescript
* import { getClient } from '@tauri-apps/plugin-http' ;
* const client = await getClient ( ) ;
* ` ` `
*
* @param options Client configuration .
*
* @returns A promise resolving to the client instance .
*
* @since 2.0 . 0
* /
async function getClient ( options? : ClientOptions ) : Promise < Client > {
return window
. __TAURI_INVOKE__ < number > ( "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-apps/plugin-http' ;
* const response = await fetch ( 'http://localhost:3003/users/2' , {
* method : 'GET' ,
* timeout : 30 ,
* } ) ;
* ` ` `
* /
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 ,
const body = await window . __TAURI_INVOKE__ < number [ ] > (
"plugin:http|fetch_read_body" ,
{
rid ,
} ,
) ;
const res = new Response ( Uint8Array . from ( body ) , {
headers ,
status ,
statusText ,
} ) ;
}
export type {
Duration ,
ClientOptions ,
Part ,
HttpVerb ,
HttpOptions ,
RequestOptions ,
FetchOptions ,
} ;
// url is read only but seems like we can do this
Object . defineProperty ( res , "url" , { value : url } ) ;
export {
getClient ,
fetch ,
Body ,
Client ,
Response ,
ResponseType ,
type FilePart ,
} ;
return res ;
}