http.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. /**
  5. * Access the HTTP client written in Rust.
  6. *
  7. * This package is also accessible with `window.__TAURI__.http` when `tauri.conf.json > build > withGlobalTauri` is set to true.
  8. *
  9. * The APIs must be allowlisted on `tauri.conf.json`:
  10. * ```json
  11. * {
  12. * "tauri": {
  13. * "allowlist": {
  14. * "http": {
  15. * "all": true, // enable all http APIs
  16. * "request": true // enable HTTP request API
  17. * }
  18. * }
  19. * }
  20. * }
  21. * ```
  22. * It is recommended to allowlist only the APIs you use for optimal bundle size and security.
  23. *
  24. * ## Security
  25. *
  26. * This API has a scope configuration that forces you to restrict the URLs and paths that can be accessed using glob patterns.
  27. *
  28. * For instance, this scope configuration only allows making HTTP requests to the GitHub API for the `tauri-apps` organization:
  29. * ```json
  30. * {
  31. * "tauri": {
  32. * "allowlist": {
  33. * "http": {
  34. * "scope": ["https://api.github.com/repos/tauri-apps/*"]
  35. * }
  36. * }
  37. * }
  38. * }
  39. * ```
  40. * Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
  41. *
  42. * @module
  43. */
  44. import { invokeTauriCommand } from './helpers/tauri'
  45. interface Duration {
  46. secs: number
  47. nanos: number
  48. }
  49. interface ClientOptions {
  50. maxRedirections?: number
  51. connectTimeout?: number | Duration
  52. }
  53. enum ResponseType {
  54. JSON = 1,
  55. Text = 2,
  56. Binary = 3
  57. }
  58. interface FilePart<T> {
  59. file: string | T
  60. mime?: string
  61. fileName?: string
  62. }
  63. type Part = string | Uint8Array | FilePart<Uint8Array>
  64. /** The body object to be used on POST and PUT requests. */
  65. class Body {
  66. type: string
  67. payload: unknown
  68. /** @ignore */
  69. private constructor(type: string, payload: unknown) {
  70. this.type = type
  71. this.payload = payload
  72. }
  73. /**
  74. * Creates a new form data body. The form data is an object where each key is the entry name,
  75. * and the value is either a string or a file object.
  76. *
  77. * By default it sets the `application/x-www-form-urlencoded` Content-Type header,
  78. * but you can set it to `multipart/form-data` if the Cargo feature `http-multipart` is enabled.
  79. *
  80. * Note that a file path must be allowed in the `fs` allowlist scope.
  81. * @example
  82. * ```typescript
  83. * import { Body } from "@tauri-apps/api/http"
  84. * Body.form({
  85. * key: 'value',
  86. * image: {
  87. * file: '/path/to/file', // either a path or an array buffer of the file contents
  88. * mime: 'image/jpeg', // optional
  89. * fileName: 'image.jpg' // optional
  90. * }
  91. * });
  92. * ```
  93. *
  94. * @param data The body data.
  95. *
  96. * @return The body object ready to be used on the POST and PUT requests.
  97. */
  98. static form(data: Record<string, Part>): Body {
  99. const form: Record<string, string | number[] | FilePart<number[]>> = {}
  100. for (const key in data) {
  101. // eslint-disable-next-line security/detect-object-injection
  102. const v = data[key]
  103. let r
  104. if (typeof v === 'string') {
  105. r = v
  106. } else if (v instanceof Uint8Array || Array.isArray(v)) {
  107. r = Array.from(v)
  108. } else if (typeof v.file === 'string') {
  109. r = { file: v.file, mime: v.mime, fileName: v.fileName }
  110. } else {
  111. r = { file: Array.from(v.file), mime: v.mime, fileName: v.fileName }
  112. }
  113. // eslint-disable-next-line security/detect-object-injection
  114. form[key] = r
  115. }
  116. return new Body('Form', form)
  117. }
  118. /**
  119. * Creates a new JSON body.
  120. * @example
  121. * ```typescript
  122. * import { Body } from "@tauri-apps/api/http"
  123. * Body.json({
  124. * registered: true,
  125. * name: 'tauri'
  126. * });
  127. * ```
  128. *
  129. * @param data The body JSON object.
  130. *
  131. * @return The body object ready to be used on the POST and PUT requests.
  132. */
  133. static json(data: Record<any, any>): Body {
  134. return new Body('Json', data)
  135. }
  136. /**
  137. * Creates a new UTF-8 string body.
  138. * @example
  139. * ```typescript
  140. * import { Body } from "@tauri-apps/api/http"
  141. * Body.text('The body content as a string');
  142. * ```
  143. *
  144. * @param data The body string.
  145. *
  146. * @return The body object ready to be used on the POST and PUT requests.
  147. */
  148. static text(value: string): Body {
  149. return new Body('Text', value)
  150. }
  151. /**
  152. * Creates a new byte array body.
  153. * @example
  154. * ```typescript
  155. * import { Body } from "@tauri-apps/api/http"
  156. * Body.bytes(new Uint8Array([1, 2, 3]));
  157. * ```
  158. *
  159. * @param data The body byte array.
  160. *
  161. * @return The body object ready to be used on the POST and PUT requests.
  162. */
  163. static bytes(bytes: Iterable<number> | ArrayLike<number>): Body {
  164. // stringifying Uint8Array doesn't return an array of numbers, so we create one here
  165. return new Body('Bytes', Array.from(bytes))
  166. }
  167. }
  168. /** The request HTTP verb. */
  169. type HttpVerb =
  170. | 'GET'
  171. | 'POST'
  172. | 'PUT'
  173. | 'DELETE'
  174. | 'PATCH'
  175. | 'HEAD'
  176. | 'OPTIONS'
  177. | 'CONNECT'
  178. | 'TRACE'
  179. /** Options object sent to the backend. */
  180. interface HttpOptions {
  181. method: HttpVerb
  182. url: string
  183. headers?: Record<string, any>
  184. query?: Record<string, any>
  185. body?: Body
  186. timeout?: number | Duration
  187. responseType?: ResponseType
  188. }
  189. /** Request options. */
  190. type RequestOptions = Omit<HttpOptions, 'method' | 'url'>
  191. /** Options for the `fetch` API. */
  192. type FetchOptions = Omit<HttpOptions, 'url'>
  193. /** @ignore */
  194. interface IResponse<T> {
  195. url: string
  196. status: number
  197. headers: Record<string, string>
  198. rawHeaders: Record<string, string[]>
  199. data: T
  200. }
  201. /** Response object. */
  202. class Response<T> {
  203. /** The request URL. */
  204. url: string
  205. /** The response status code. */
  206. status: number
  207. /** A boolean indicating whether the response was successful (status in the range 200–299) or not. */
  208. ok: boolean
  209. /** The response headers. */
  210. headers: Record<string, string>
  211. /** The response raw headers. */
  212. rawHeaders: Record<string, string[]>
  213. /** The response data. */
  214. data: T
  215. /** @ignore */
  216. constructor(response: IResponse<T>) {
  217. this.url = response.url
  218. this.status = response.status
  219. this.ok = this.status >= 200 && this.status < 300
  220. this.headers = response.headers
  221. this.rawHeaders = response.rawHeaders
  222. this.data = response.data
  223. }
  224. }
  225. class Client {
  226. id: number
  227. /** @ignore */
  228. constructor(id: number) {
  229. this.id = id
  230. }
  231. /**
  232. * Drops the client instance.
  233. * @example
  234. * ```typescript
  235. * import { getClient } from '@tauri-apps/api/http';
  236. * const client = await getClient();
  237. * await client.drop();
  238. * ```
  239. *
  240. * @returns
  241. */
  242. async drop(): Promise<void> {
  243. return invokeTauriCommand({
  244. __tauriModule: 'Http',
  245. message: {
  246. cmd: 'dropClient',
  247. client: this.id
  248. }
  249. })
  250. }
  251. /**
  252. * Makes an HTTP request.
  253. * @example
  254. * ```typescript
  255. * import { getClient } from '@tauri-apps/api/http';
  256. * const client = await getClient();
  257. * const response = await client.request({
  258. * method: 'GET',
  259. * url: 'http://localhost:3003/users',
  260. * });
  261. * ```
  262. *
  263. * @param options The request options.
  264. * @returns A promise resolving to the response.
  265. */
  266. async request<T>(options: HttpOptions): Promise<Response<T>> {
  267. const jsonResponse =
  268. !options.responseType || options.responseType === ResponseType.JSON
  269. if (jsonResponse) {
  270. options.responseType = ResponseType.Text
  271. }
  272. return invokeTauriCommand<IResponse<T>>({
  273. __tauriModule: 'Http',
  274. message: {
  275. cmd: 'httpRequest',
  276. client: this.id,
  277. options
  278. }
  279. }).then((res) => {
  280. const response = new Response(res)
  281. if (jsonResponse) {
  282. /* eslint-disable */
  283. try {
  284. // @ts-expect-error
  285. response.data = JSON.parse(response.data as string)
  286. } catch (e) {
  287. if (response.ok && (response.data as unknown as string) === '') {
  288. // @ts-expect-error
  289. response.data = {}
  290. } else if (response.ok) {
  291. throw Error(
  292. `Failed to parse response \`${response.data}\` as JSON: ${e};
  293. try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`
  294. )
  295. }
  296. }
  297. /* eslint-enable */
  298. return response
  299. }
  300. return response
  301. })
  302. }
  303. /**
  304. * Makes a GET request.
  305. * @example
  306. * ```typescript
  307. * import { getClient, ResponseType } from '@tauri-apps/api/http';
  308. * const client = await getClient();
  309. * const response = await client.get('http://localhost:3003/users', {
  310. * timeout: 30,
  311. * // the expected response type
  312. * responseType: ResponseType.JSON
  313. * });
  314. * ```
  315. *
  316. * @param url The request URL.
  317. * @param options The request options.
  318. * @returns A promise resolving to the response.
  319. */
  320. async get<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
  321. return this.request({
  322. method: 'GET',
  323. url,
  324. ...options
  325. })
  326. }
  327. /**
  328. * Makes a POST request.
  329. * @example
  330. * ```typescript
  331. * import { getClient, Body, ResponseType } from '@tauri-apps/api/http';
  332. * const client = await getClient();
  333. * const response = await client.post('http://localhost:3003/users', {
  334. * body: Body.json({
  335. * name: 'tauri',
  336. * password: 'awesome'
  337. * }),
  338. * // in this case the server returns a simple string
  339. * responseType: ResponseType.Text,
  340. * });
  341. * ```
  342. *
  343. * @param url The request URL.
  344. * @param body The body of the request.
  345. * @param options The request options.
  346. * @returns A promise resolving to the response.
  347. */
  348. async post<T>(
  349. url: string,
  350. body?: Body,
  351. options?: RequestOptions
  352. ): Promise<Response<T>> {
  353. return this.request({
  354. method: 'POST',
  355. url,
  356. body,
  357. ...options
  358. })
  359. }
  360. /**
  361. * Makes a PUT request.
  362. * @example
  363. * ```typescript
  364. * import { getClient, Body } from '@tauri-apps/api/http';
  365. * const client = await getClient();
  366. * const response = await client.put('http://localhost:3003/users/1', {
  367. * body: Body.form({
  368. * file: {
  369. * file: '/home/tauri/avatar.png',
  370. * mime: 'image/png',
  371. * fileName: 'avatar.png'
  372. * }
  373. * })
  374. * });
  375. * ```
  376. *
  377. * @param url The request URL.
  378. * @param body The body of the request.
  379. * @param options Request options.
  380. * @returns A promise resolving to the response.
  381. */
  382. async put<T>(
  383. url: string,
  384. body?: Body,
  385. options?: RequestOptions
  386. ): Promise<Response<T>> {
  387. return this.request({
  388. method: 'PUT',
  389. url,
  390. body,
  391. ...options
  392. })
  393. }
  394. /**
  395. * Makes a PATCH request.
  396. * @example
  397. * ```typescript
  398. * import { getClient, Body } from '@tauri-apps/api/http';
  399. * const client = await getClient();
  400. * const response = await client.patch('http://localhost:3003/users/1', {
  401. * body: Body.json({ email: 'contact@tauri.studio' })
  402. * });
  403. * ```
  404. *
  405. * @param url The request URL.
  406. * @param options The request options.
  407. * @returns A promise resolving to the response.
  408. */
  409. async patch<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
  410. return this.request({
  411. method: 'PATCH',
  412. url,
  413. ...options
  414. })
  415. }
  416. /**
  417. * Makes a DELETE request.
  418. * @example
  419. * ```typescript
  420. * import { getClient } from '@tauri-apps/api/http';
  421. * const client = await getClient();
  422. * const response = await client.delete('http://localhost:3003/users/1');
  423. * ```
  424. *
  425. * @param url The request URL.
  426. * @param options The request options.
  427. * @returns A promise resolving to the response.
  428. */
  429. async delete<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
  430. return this.request({
  431. method: 'DELETE',
  432. url,
  433. ...options
  434. })
  435. }
  436. }
  437. /**
  438. * Creates a new client using the specified options.
  439. * @example
  440. * ```typescript
  441. * import { getClient } from '@tauri-apps/api/http';
  442. * const client = await getClient();
  443. * ```
  444. *
  445. * @param options Client configuration.
  446. *
  447. * @return A promise resolving to the client instance.
  448. */
  449. async function getClient(options?: ClientOptions): Promise<Client> {
  450. return invokeTauriCommand<number>({
  451. __tauriModule: 'Http',
  452. message: {
  453. cmd: 'createClient',
  454. options
  455. }
  456. }).then((id) => new Client(id))
  457. }
  458. /** @internal */
  459. let defaultClient: Client | null = null
  460. /**
  461. * Perform an HTTP request using the default client.
  462. * @example
  463. * ```typescript
  464. * import { fetch } from '@tauri-apps/api/http';
  465. * const response = await fetch('http://localhost:3003/users/2', {
  466. * method: 'GET',
  467. * timeout: 30,
  468. * });
  469. * ```
  470. *
  471. * @param url The request URL.
  472. * @param options The fetch options.
  473. * @return The response object.
  474. */
  475. async function fetch<T>(
  476. url: string,
  477. options?: FetchOptions
  478. ): Promise<Response<T>> {
  479. if (defaultClient === null) {
  480. defaultClient = await getClient()
  481. }
  482. return defaultClient.request({
  483. url,
  484. method: options?.method ?? 'GET',
  485. ...options
  486. })
  487. }
  488. export type {
  489. Duration,
  490. ClientOptions,
  491. Part,
  492. HttpVerb,
  493. HttpOptions,
  494. RequestOptions,
  495. FetchOptions
  496. }
  497. export { getClient, fetch, Body, Client, Response, ResponseType, FilePart }