Fetch API
This page is the API reference for the built-in app.fetch HTTP client. See the Built-in HTTP Client Guide for full usage and examples.
Here, app means a real application instance from plugin setup, a service, or req.app. The current defineRoutes factory parameter is a bound function that does not retain fetch shortcuts, create, or proxy. In a handler use req.app.fetch; directly calling factory app.fetch(url, init) still works. See the basic guide example.
app.fetch(input, init?)
Send an HTTP request. Signatures are compatible with native fetch, with additional support for timeouts, retries and requestId propagation.
Parameters
Return value: Promise<Response>, a standard Fetch Response. HTTP 4xx/5xx returns a Response for the caller to check via ok/status. Network failures, internal timeout, and cancellation reject. The caller consumes the body.
Outbound lifecycles include fetch:before/after/error and proxy proxy:before/after/error. Before runs once per operation; after receives the final Response, including HTTP 5xx. Error does not cover every pre-parse, before-hook, or body-consumption failure. An ordinary before-hook error rejects directly; proxy before-hook errors enter local proxy error handling. See Hooks Guide for payload, attempt, and error boundaries.
Shortcut method
app.fetch.get(url, init?)
Send a GET request.
app.fetch.post(url, body?, init?)
Send POST. Non-null/undefined body is JSON-stringified; application/json is added only when no Content-Type was explicitly supplied. Null/undefined creates no body or default Content-Type but retains explicit headers. Use the generic call for FormData, binary, or streams.
app.fetch.put(url, body?, init?)
Send PUT. Non-null body JSON serialization and explicit Content-Type preservation match post.
app.fetch.patch(url, body?, init?)
Send PATCH. Non-null body JSON serialization and explicit Content-Type preservation match post.
app.fetch.delete(url, init?)
Send a DELETE request.
app.fetch.create(options)
Create a preconfigured subclient with baseURL, default headers, timeout, retry count, and delay. Unspecified timeout/retry/retryDelay inherit from the factory that created it.
The subclient also has all shortcuts and create(), but no proxy. Proxy exists only on root app.fetch.proxy.
String input is appended to the baseURL with trailing slash removed; a leading / does not discard /api/v1. Even an absolute URL string is appended. Use the root client to bypass baseURL, or pass a URL/Request object to the subclient. Per-call headers override subclient headers, and both override headers propagated automatically from AsyncLocalStorage.
Nested create() currently reuses the parent factory that created the subclient; it does not accumulate each subclient's baseURL/headers/timeout. Pass retained settings explicitly.
app.fetch.proxy
app.fetch.proxy is used to proxy the current request to the upstream service in the routing handler, and transparently transmit the upstream response directly to the client. It does not wrap 2xx / 3xx / 4xx / 5xx upstream responses as { code, data, requestId }; only proxy-local errors such as local parameter errors, target non-existence, upstream network errors, or timeouts return vext-style error responses.
Named Target Proxy
Named targets come from config.fetch.proxy[]:
name will be mapped to app.fetch.proxy.<name>, the reserved name then cannot be used.
Put this registration fragment inside defineRoutes((app) => { ... }). In its handler, use the real instance at req.app.fetch.proxy.<name>(req, res, options):
Direct URL proxy
Without a named target, call req.app.fetch.proxy(req, res, { url }) directly:
header priority
Proxy request headers are merged in the following order, with the latter overriding the former:
Authorization will not passthrough from the current request by default. Passthrough of the original Authorization is only allowed if the target configuration or this call sets allowAuthorizationForward: true and forwardHeaders explicitly contains authorization.
The effective whitelist is the union of target and call lists; an empty call list does not clear target entries. Dynamic injection supports synchronous/async functions with { req, target, options }. Null/undefined injected values are ignored rather than deleting existing headers.
Proxy does not use ordinary fetch AsyncLocalStorage propagation. Forward the incoming requestId header explicitly, or inject { "x-request-id": req.requestId }. Either target or call-level Authorization permission can enable forwarding; call-level false does not revoke target-level true.
retry contract
Agent retry configuration priority:
retry counts extra attempts, so total attempts equal retry + 1. Only idempotent GET/HEAD/OPTIONS/PUT/DELETE methods are auto-retried, never POST/PATCH by default. Retry conditions are upstream 5xx or network failures such as DNS/connection errors. 2xx/3xx/4xx, timeout, and client cancellation do not retry. Proxy returns a local 504 only before response headers begin; timeout during body forwarding interrupts transfer and cannot be replaced with a complete JSON error. The request body must also be replayable; a stream is not made replayable just because the method is idempotent.
Parameter and Response Boundaries
- A named target requires a nonempty path; direct proxy requires an absolute URL. Method defaults to req.method. GET/HEAD ignore body; other methods without an explicit body read the raw request body subject to maxBodySize.
- Query merges existing URL query, req.query, then options.query; null/undefined option values remove matching keys.
- Manual redirect preserves upstream 3xx. Forwarding filters hop-by-hop, Content-Length, and Content-Encoding headers while retaining multiple Set-Cookie headers. Response handling applies no-body semantics for HEAD, 204, and 304.
- Local proxy errors have
{ code, message, requestId }JSON. Parameter errors are usually 400, missing targets 500, network failures 502, and timeout before headers 504. Once streaming starts, handle failure as part of stream lifecycle.
Type definition
VextFetchInit
Inherited from standard RequestInit, extending the following fields:
VextFetchClientOptions
Configuration options for the create() factory method.
VextFetch
Type definition for root app.fetch. It is not only a callable function, but also has shortcut methods, create() and proxy mounted.
VextFetchProxyOptions
Named target mode uses path; direct URL mode uses url. See Parameter and Response Boundaries for merging and body rules.
VextFetchProxyTargetConfig
VextFetchProxyHeaders / HeaderContext
VextFetchProxy / Handler
Global configuration
Configure global defaults through fetch in src/config/default.ts:
The framework captures listed incoming headers in request metadata middleware and writes requestContext.store.propagatedHeaders. Ordinary app.fetch reads and injects them on outbound calls. Capture depends on request context, not on the requestId switch; explicit outbound headers win. Proxy has its own whitelist.
No need to pass it manually on every call.
- Global configuration
config.fetch.propagateHeaders: declare which headers need to be captured and transparently transmitted - Headers not declared in the global configuration: manually set in
init.headers - See Request Context for the trace relationship.
Priority
Behavior description
Timeout must be a finite number in (0, 2147483647], retry a nonnegative integer, and retryDelay or each function result a finite number in [0, 2147483647]. A delay callback's attempt starts at 1.
Timeout
Ordinary app.fetch clears its timeout when response headers arrive, allowing a slow body to finish. The caller's init.signal or Request.signal continues to cancel body consumption. Each retry has a new attempt timer. The app.fetch.proxy timeout also covers forwarding the upstream body, and a disconnected client cancels the upstream request.
- Implemented using
AbortController+setTimeout - Internal ordinary-fetch timeout rejects with an Error named
TimeoutError, message[app.fetch] GET https://... timed out after 10000ms. - If
init.signalis passed in at the same time, it will be merged with the timeout signal - any trigger will abort the request
Retry
- Only idempotent methods with replayable bodies retry: GET/HEAD/OPTIONS/PUT/DELETE.
- POST / PATCH does not retry (to avoid repeated execution of side effects)
- Trigger condition: HTTP 5xx response or network error
- Not triggered by internal timeout, caller cancellation, or 4xx; a Request body without a replayable init replacement and streaming bodies do not retry.
- Exhausted 5xx returns a Response; exhausted network errors reject. Cancellation interrupts retry waits and preserves reason.
- Ordinary fetch does not automatically read
req.signal; a handler must pass{ signal: req.signal }explicitly. retryDelaysupports exponential backoff in functional form:(attempt) => Math.min(1000 * 2 ** attempt, 10000)
requestId propagation
- Automatically read the
requestIdof the current request fromrequestContext(AsyncLocalStorage) - Adds it only when present in the store and not already set on outbound headers. Header name follows
config.requestId.header, defaultx-request-id. propagateRequestId: falsedisables only automatic requestId insertion, not an explicit header or other propagated headers.
Structured log
Each actual attempt can log outbound activity; prevalidation or before-hook failures do not guarantee a log. Logger threshold still filters levels:
Log fields: type: "outbound" / method / url / status / duration / requestId
Proxy handling uses type: "proxy".
Replacement implementation
The current version does not expose the app.setFetch() public API, so direct replacement of the built-in implementation is not supported here.
If you need a different HTTP client strategy, it is recommended to keep app.fetch as the default implementation of the framework, and then additionally mount the custom client through a plug-in:
If app.fetch is completely bypassed, requestId propagation, timeouts, retries, and structured logging capabilities all need to be completed by yourself.
Type import
Next step
- Read the Built-in HTTP Client Guide for complete usage and best practices
- View the mounting location of
app.fetchin Application Instance - Understand the global configuration related to
fetchin Configuration Items