import type { HttpClient } from './index.js'; import type { HttpClientConfig } from './options.js'; import { type RequestResponse, request, RequestResponseError } from './request.js'; /** Restful 响应 */ export interface RestfulResponse extends RequestResponse { /** 数据 */ readonly data: T; } /** * 发送 restful 请求 * * 可预期的异常包括 `RequestError` 和 `RequestResponseError` */ export type Restful = ( url: string | undefined, payload?: unknown, init?: RequestInit, ) => Promise>; /** 规格化 URL */ function normalizeUrl(url: string | undefined, config: HttpClientConfig): string { if (!url) return config.apiUrl; if (url.startsWith('http://') || url.startsWith('https://')) return url; if (url.startsWith('/')) { return `${config.apiUrl}${url}`; } throw new Error(`Url must starts with '/', 'http://' or 'https://'`); } /** 解析 request body */ function parseRequestBody(payload: NonNullable): [string | null, BodyInit] { if (typeof payload == 'string') { // Will be sent as text/plain return [null, payload]; } if (typeof payload == 'object') { if (payload instanceof URLSearchParams) { // Will be sent as application/x-www-form-urlencoded return [null, payload]; } else if (typeof FormData == 'function' && payload instanceof FormData) { // Will be sent as multipart/form-data return [null, payload]; } else if ( ('byteLength' in payload && typeof payload.byteLength == 'number') || (typeof Blob != 'undefined' && payload instanceof Blob) ) { return ['application/octet-stream', payload as BufferSource | Blob]; } } return ['application/json;charset=utf-8', JSON.stringify(payload)]; } /** * 发送 restful 请求 * * 可预期的异常包括 `RequestError` 和 `RequestResponseError` */ export function createRestful(client: HttpClient): Restful { return async (url: string | undefined, payload?: unknown, init?: RequestInit) => { const { config } = client; const { fetcher: { Headers }, } = config; url = normalizeUrl(url, config); const headers = new Headers(init?.headers); const req: RequestInit = { ...init, headers }; if (payload == null) { req.method ??= 'GET'; } else if (req.body != null) { throw new Error(`Cannot set 'init.body' when 'payload' is provided`); } else { req.method ??= 'POST'; const [contentType, body] = parseRequestBody(payload); req.body = body; if (!headers.has('Content-Type') && contentType) { headers.set('Content-Type', contentType); } } req.method = req.method.toUpperCase(); if (req.method === 'GET' && req.body) { throw new Error(`GET request cannot have body`); } if (config.apiTunnel && url.startsWith(config.apiUrl + '/')) { const { search, pathname, origin } = new URL(url, config.apiUrl); headers.set('X-Http-Method', req.method); headers.set('X-Http-Path', `${origin}${pathname}`.slice(config.apiUrl.length)); req.method = 'POST'; url = `${config.apiUrl}/connect${search}`; } const res = await request(config, url, req); if (res.status >= 300) throw new RequestResponseError(res); return res as RestfulResponse; }; }