api-client/packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-relay/guest-js/index.ts
Shreyas ecf7d2507a
feat(relay): control redirect follow (#5508)
Add per-domain toggle to disable automatic HTTP redirect following in
 the Native and Agent interceptors. When disabled, requests return the
 redirect response (status code, headers, body) without following the
 Location header.

 Previously HTTP redirects were always followed (on browser, can't do
 much about that, see
 https://fetch.spec.whatwg.org/#atomic-http-redirect-handling) without
 option to inspect the redirect response itself. This prevented
 developers from accessing redirect metadata needed when testing OAuth
 flows (PKCE where intermediate responses contain authorization tokens),
 authentication endpoints that return codes in Location headers with 302
 status, and debugging API redirect chains. But on the desktop app,
 redirects were just never followed, creating the opposite effect.

 The browser's fetch API applies atomic HTTP redirect handling per spec,
 making it impossible to intercept redirects and inspect their responses.
 The Native and Agent interceptors use curl and native HTTP clients
 respectively, both supporting redirect control, making this feature
 viable for these specific interceptors. (Proxyscotch tbd).
2025-10-27 17:41:58 +05:30

289 lines
7.4 KiB
TypeScript

import { invoke } from '@tauri-apps/api/core'
export type Method =
| "GET" // Retrieve resource
| "POST" // Create resource
| "PUT" // Replace resource
| "DELETE" // Remove resource
| "PATCH" // Modify resource
| "HEAD" // GET without body
| "OPTIONS" // Get allowed methods
| "CONNECT" // Create tunnel
| "TRACE" // Loop-back test
export type Version = "HTTP/1.0" | "HTTP/1.1" | "HTTP/2.0" | "HTTP/3.0"
export type StatusCode =
| 100 // Continue
| 101 // Switching Protocols
| 102 // Processing
| 103 // Early Hints
| 200 // OK
| 201 // Created
| 202 // Accepted
| 203 // Non-Authoritative Info
| 204 // No Content
| 205 // Reset Content
| 206 // Partial Content
| 207 // Multi-Status
| 208 // Already Reported
| 226 // IM Used
| 300 // Multiple Choices
| 301 // Moved Permanently
| 302 // Found
| 303 // See Other
| 304 // Not Modified
| 305 // Use Proxy
| 306 // Switch Proxy
| 307 // Temporary Redirect
| 308 // Permanent Redirect
| 400 // Bad Request
| 401 // Unauthorized
| 402 // Payment Required
| 403 // Forbidden
| 404 // Not Found
| 405 // Method Not Allowed
| 406 // Not Acceptable
| 407 // Proxy Auth Required
| 408 // Request Timeout
| 409 // Conflict
| 410 // Gone
| 411 // Length Required
| 412 // Precondition Failed
| 413 // Payload Too Large
| 414 // URI Too Long
| 415 // Unsupported Media
| 416 // Range Not Satisfiable
| 417 // Expectation Failed
| 418 // I'm a teapot
| 421 // Misdirected Request
| 422 // Unprocessable Entity
| 423 // Locked
| 424 // Failed Dependency
| 425 // Too Early
| 426 // Upgrade Required
| 428 // Precondition Required
| 429 // Too Many Requests
| 431 // Headers Too Large
| 451 // Unavailable Legal
| 500 // Server Error
| 501 // Not Implemented
| 502 // Bad Gateway
| 503 // Service Unavailable
| 504 // Gateway Timeout
| 505 // HTTP Ver. Not Supported
| 506 // Variant Negotiates
| 507 // Insufficient Storage
| 508 // Loop Detected
| 510 // Not Extended
| 511 // Network Auth Required
export type FormDataValue =
| { kind: "text"; value: string }
| { kind: "file"; filename: string; contentType: string; data: Uint8Array }
export type FormData = [string, FormDataValue[]][]
export enum MediaType {
TEXT_PLAIN = "text/plain",
TEXT_HTML = "text/html",
TEXT_CSS = "text/css",
TEXT_CSV = "text/csv",
APPLICATION_JSON = "application/json",
APPLICATION_LD_JSON = "application/ld+json",
APPLICATION_XML = "application/xml",
TEXT_XML = "text/xml",
APPLICATION_FORM = "application/x-www-form-urlencoded",
APPLICATION_OCTET = "application/octet-stream",
MULTIPART_FORM = "multipart/form-data"
}
export type ContentType =
| { kind: "text"; content: string; mediaType: MediaType.TEXT_PLAIN | MediaType.TEXT_HTML | MediaType.TEXT_CSS | MediaType.TEXT_CSV }
| { kind: "json"; content: unknown; mediaType: MediaType.APPLICATION_JSON | MediaType.APPLICATION_LD_JSON }
| { kind: "xml"; content: string; mediaType: MediaType.APPLICATION_XML | MediaType.TEXT_XML }
| { kind: "form"; content: FormData; mediaType: MediaType.APPLICATION_FORM }
| { kind: "binary"; content: Uint8Array; mediaType: MediaType.APPLICATION_OCTET | string; filename?: string }
| { kind: "multipart"; content: FormData; mediaType: MediaType.MULTIPART_FORM }
| { kind: "urlencoded"; content: string; mediaType: MediaType.APPLICATION_FORM }
| { kind: "stream"; content: ReadableStream; mediaType: string }
export interface ResponseBody {
body: Uint8Array
mediaType: MediaType | string
}
export type AuthType =
| { kind: "none" }
| { kind: "basic"; username: string; password: string }
| { kind: "bearer"; token: string }
| {
kind: "digest"
username: string
password: string
realm?: string
nonce?: string
opaque?: string
algorithm?: "MD5" | "SHA-256" | "SHA-512"
qop?: "auth" | "auth-int"
nc?: string
cnonce?: string
}
| {
kind: "oauth2"
grantType:
| {
kind: "authorization_code"
authEndpoint: string
tokenEndpoint: string
clientId: string
clientSecret?: string
}
| {
kind: "client_credentials"
tokenEndpoint: string
clientId: string
clientSecret?: string
}
| {
kind: "password"
tokenEndpoint: string
username: string
password: string
}
| {
kind: "implicit"
authEndpoint: string
clientId: string
}
accessToken?: string
refreshToken?: string
}
| {
kind: "apikey"
key: string
value: string
in: "header" | "query"
}
| {
kind: "aws"
accessKey: string
secretKey: string
region: string
service: string
sessionToken?: string
in: "header" | "query"
}
export type CertificateType =
| {
kind: "pem"
cert: Uint8Array
key: Uint8Array
}
| {
kind: "pfx"
data: Uint8Array
password: string
}
export interface RequestOptions {
timeout?: number
followRedirects?: boolean
maxRedirects?: number
decompress?: boolean
cookies?: boolean
keepAlive?: boolean
}
export interface RequestMeta {
options?: RequestOptions
}
export interface Request {
id: number
url: string
method: Method
version: Version
headers?: Record<string, string>
params?: Record<string, string>
content?: ContentType
auth?: AuthType
security?: {
certificates?: {
client?: CertificateType
ca?: Array<Uint8Array>
}
verifyHost?: boolean
verifyPeer?: boolean
}
proxy?: {
url: string
auth?: {
username: string
password: string
}
}
meta?: RequestMeta
}
export interface Response {
id: number
status: StatusCode
statusText: string
version: Version
headers: Record<string, string>
cookies?: Array<{
name: string
value: string
domain?: string
path?: string
expires?: Date
secure?: boolean
httpOnly?: boolean
sameSite?: 'Strict' | 'Lax' | 'None'
}>
body: ResponseBody
meta: {
timing: {
start: number
end: number
}
size: {
headers: number
body: number
total: number
}
}
}
export type UnsupportedFeatureError = {
kind: "unsupported_feature"
feature: string
message: string
relay: string
}
export type RelayError =
| UnsupportedFeatureError
| { kind: "network"; message: string; cause?: unknown }
| { kind: "timeout"; message: string; phase?: "connect" | "tls" | "response" }
| { kind: "certificate"; message: string; cause?: unknown }
| { kind: "parse"; message: string; cause?: unknown }
| { kind: "abort"; message: string }
export type RequestResult =
| { kind: 'success'; response: Response }
| { kind: 'error'; error: RelayError }
export async function execute(request: Request): Promise<RequestResult> {
return await invoke<RequestResult>('plugin:relay|execute', { request })
}
export async function cancel(requestId: number): Promise<void> {
return await invoke<void>('plugin:relay|cancel', { requestId })
}