request
The framework's unified encrypted request entry. Business code always uses request() (auto-imported); it encapsulates dynamic-key encryption, token / deviceId injection, login renewal, and SSR/CSR routing. createApiFetch is its underlying factory, instantiated by a framework plugin and provided as $request — business code rarely touches it.
How It Works
createApiFetchis instantiated inside a framework plugin (injectinggetToken/getDeviceId/onRelogin);request()is its global thin wrapper.- Every request generates its own requestId (UUID v4), used for: the request header aligned with the server, aes-dynamic key derivation (HMAC-SHA256), and debug-log pairing.
- The request body is wrapped as
{ data, token, deviceId }, gzipped + AES-CBC encrypted, and POSTed astext/plain; the response is decoded viadecodeResponse(decryption; the legacy backend returns some business failures as plaintext JSON, and such plaintext failure envelopes are accepted too) then unwrapped as{ status, data, error, errorCode }, returningdataon success; business failures (status: 'n') throwApiError(messagefrom the envelope'serror, witherrorCodeattached for structured branching). - SSR / CSR routing: SSR connects to the backend directly (
baseURL = apiBase + apiPrefix) and forwards the real IP (cf-connecting-ip / x-forwarded-for / x-real-ip) plus user-agent; CSR goes through the routeRules proxy (baseURL = apiPrefix). - auth-ready gating: on the client,
request()automaticallyawait useAuthReady(), guaranteeing no request fires before the device + token identity is established (prevents first-screen 401 storms). - Automatic token renewal: when the backend returns
errorCode === 2002(token expired),onReloginsilently re-authenticates and the request retries once; a second failure throwsApiError('登录失效,请刷新页面重试', 2002). - The i18n language header is read from the
i18n_redirectedcookie, falling back to'zh-cn'.
API
| Export | Signature | Notes |
|---|---|---|
request | <T = unknown>(url: string, fetchOptions?: Record<string, unknown>) => Promise<T> | Standard business entry; defaults to method: 'POST', with fetchOptions.body as the business params object |
createApiFetch | (options: ApiFetchOptions) => apiFetch | Low-level factory; ApiFetchOptions = { getToken, getDeviceId, onRelogin }, used inside framework plugins |
ApiError | class ApiError extends Error { errorCode?: number } | Error carrier for business failures; errorCode comes from the protocol envelope (e.g. 2002 for token expiry), and dev mock's errorCode option is attached the same way |
Global request config (timeout / shared headers) lives in the request block of swift.config.json (timeout defaults to 20s); per-request overrides go through fetchOptions.timeout / fetchOptions.headers.
Examples
// Standard POST (body = business params)
const list = await request<Article[]>('/article/list', { body: { page: 1 } })
// Methods other than POST
await request('/user/logout', { method: 'GET' })
// Failure handling: request throws an Error — try/catch and give user feedback
async function onSubmit() {
try {
await request('/order/submit', { body: form })
useToast().success('Submitted')
} catch (e) {
useToast().error(e instanceof Error ? e.message : 'Submission failed, please retry')
}
}
// Branch on errorCode (never match on message text — copy changes break everything)
try {
detail = await request('/movie/detail', { body: { id } })
} catch (e) {
if (e instanceof ApiError && e.errorCode === 4002) {
throw createError({ statusCode: 404 }) // invalid / delisted content → 404
}
throw e
}
Caveats
Business code always uses request(), never frameworkInternalRequest (REQ-001, blocker)
The framework also exports an ungated internal variant frameworkInternalRequest, reserved for the framework's own login flow (autoLogin etc.) to avoid a deadlock. Misusing it in business code sends requests before the identity is ready → no deviceId / token, and the backend treats them as anonymous.
Failures throw an ApiError — try/catch required (REQ-002, warning)
When the backend returns status: 'n' or the network fails, request() throws instead of returning { error }. Without a catch you get an unhandled promise rejection — follow-up logic (success toasts etc.) silently never runs and the user sees nothing happen. Business failures throw ApiError (an Error subclass — existing instanceof Error / e.message consumers are unaffected); branch on e.errorCode when you need to distinguish error types — never match on message text.
SSR calls require an absolute apiBase (REQ-003, blocker)
The SSR branch connects to the backend directly; runtimeConfig.public.apiBase must be an absolute URL (e.g. https://api.example.com). When empty, the framework fails fast with an explicit "apiBase not configured" error (instead of an obscure ERR_INVALID_URL). Sites without an external SSR backend should not call request() during SSR — move data fetching to onMounted (CSR):
// ❌ request() at setup top level of an ISR/SSR page with no apiBase → throws during SSR
const { data } = await useAsyncData(() => request('/article/list'))
// ✅ Frontend-only sites / private data: move to onMounted (CSR)
onMounted(async () => {
list.value = await request('/article/list')
})
Security Model
The apiKey used for request encryption also lives in runtimeConfig.public.* (readable from the client bundle); the dynamic-key protocol is obfuscation + anti-replay, not end-to-end encryption. See Client-Side Encryption Boundary.