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

  1. createApiFetch is instantiated inside a framework plugin (injecting getToken / getDeviceId / onRelogin); request() is its global thin wrapper.
  2. 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.
  3. The request body is wrapped as { data, token, deviceId }, gzipped + AES-CBC encrypted, and POSTed as text/plain; the response is decoded via decodeResponse (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 }, returning data on success; business failures (status: 'n') throw ApiError (message from the envelope's error, with errorCode attached for structured branching).
  4. 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).
  5. auth-ready gating: on the client, request() automatically await useAuthReady(), guaranteeing no request fires before the device + token identity is established (prevents first-screen 401 storms).
  6. Automatic token renewal: when the backend returns errorCode === 2002 (token expired), onRelogin silently re-authenticates and the request retries once; a second failure throws ApiError('登录失效,请刷新页面重试', 2002).
  7. The i18n language header is read from the i18n_redirected cookie, falling back to 'zh-cn'.

API

ExportSignatureNotes
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) => apiFetchLow-level factory; ApiFetchOptions = { getToken, getDeviceId, onRelogin }, used inside framework plugins
ApiErrorclass 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.