Built-in HTTP client (app.fetch)
VextJS has a built-in enhanced HTTP client app.fetch, which is based on the Node.js 20+ native fetch package and provides capabilities such as requestId automatic propagation, timeout control, automatic retry, structured log, create() factory and config driver agent. There is no need to install any third-party HTTP libraries to make inter-service calls.
Function overview
Basic usage
The signature of app.fetch is fully compatible with native fetch and can be replaced seamlessly:
app.fetch will automatically inject the requestId of the current request into the x-request-id header of the outbound request. If downstream services also use VextJS, they will automatically receive and continue this tracking ID to implement distributed link tracking.
Fetch Hooks
app.fetch and app.fetch.proxy will trigger outbound life cycle hooks, which are suitable for uniform header injection, recording third-party call time or reporting failures:
fetch:before and proxy:before are propagable hooks. Throwing an error will prevent this outbound request; fetch:after/error and proxy:after/error are safe hooks, which only record failures and do not change the main process.
Shortcut method
In addition to calling app.fetch(url, init) directly, shortcuts to commonly used HTTP methods are also provided:
GET
POST
The second parameter of the post / put / patch method is the request body object, which will automatically JSON.stringify and set Content-Type: application/json:
PUT
PATCH
DELETE
List of method signatures
Configuration
Global configuration (config.fetch)
Configure global defaults through the fetch field in vext.config.ts:
After configuration, the requestId middleware will read the header value specified in the list from the inbound request header when each request comes in.
Write to requestContext.store.propagatedHeaders. app.fetch automatically reads and injects from the store during outbound requests.
No need to manually pass these headers on every app.fetch call - the framework does the entire chain automatically.
Single request configuration (VextFetchInit)
Global configuration can be overridden per request via the init parameter:
VextFetchInit complete field
VextFetchInit inherits from the standard RequestInit and extends the following fields:| Field | Type | Default Value | Description |
| -------------------- | ------------------------------------------------ | -------------------------------- | -------------------------------------------------------------------------------- |
| timeout | number | Global config.fetch.timeout | Request timeout (milliseconds) |
| retry | number | global config.fetch.retry | Number of retries (idempotent methods only) |
| retryDelay | number \| (attempt: number) => number | Global config.fetch.retryDelay | Retry interval, supports exponential backoff in functional form |
| propagateRequestId | boolean | true | Whether to automatically inject the x-request-id header (propagatedHeaders will still be transparently transmitted when disabled) |
| propagateHeaders | string[] | — | Additional headers that need to be transparently transmitted in this request (must be declared in config.fetch.propagateHeaders to have a value) |
Single request init.timeout > options.timeout of create() > Global config.fetch.timeout
create() factory
When you need to call the same downstream service frequently, use create() to create a preconfigured subclient to avoid repeatedly passing in baseURL and public headers:
Use in a route or service:
VextFetchClientOptions
Subclients also support calling create() again to create more fine-grained clients:
app.fetch.proxy request proxy
app.fetch.proxy is suitable for gateway, BFF or "forward the current request to an internal service" scenario. Its positioning is different from app.fetch.create(): create() returns a standard Response for the business code to process by itself; proxy receives the current req/res and writes the upstream response directly back to the client.
The upstream response will be transparently transmitted directly: 2xx / 3xx / 4xx / 5xx will not be packaged into { code, data, requestId }. Only proxy-local errors will be responded to with vext-style errors, such as missing path/url, target does not exist, Authorization passthrough is prohibited, upstream network error 502, or timeout 504.
Header merging and Authorization
The request header priority is:
forwardHeaders reads from the current req.headers whitelist. The original Authorization is not passed through by default; it will only be passed through if the target configuration or single call explicitly sets allowAuthorizationForward: true and the whitelist contains authorization.
Direct URL pattern
If you are only temporarily proxying to a full URL, you do not need to configure the target:
proxy retry rules
The agent's retry represents "extra attempts", and the total number of attempts is retry + 1. The priority is options.retry > target.retry > config.fetch.retry > 0, the same is true for retryDelay. Only idempotent methods such as GET / HEAD / OPTIONS / PUT / DELETE will automatically retry in the event of upstream 5xx or network errors; POST / PATCH does not retry by default. There is no retry after timeout, and a local 504 is returned directly.
requestId automatically propagates
This is one of the core capabilities of app.fetch. When an HTTP request comes into VextJS, the requestId middleware generates a unique ID for it and writes it to the requestContext (based on AsyncLocalStorage). When you use app.fetch to call a downstream service, the framework automatically:
- Read the current
requestIdfromrequestContext.getStore() - Inject the
x-request-idheader into the outbound request
Disable requestId propagation
Some external APIs do not support custom headers and propagation can be disabled:
Custom header transparent transmission (propagateHeaders)
In addition to requestId, app.fetch also supports automatic transparent transmission of other inbound request headers to downstream - typical uses are distributed link tracing headers (traceparent) and multi-tenant identification (x-tenant-id).
Configuration method
Declare the header names that need to be transparently transmitted in config.fetch.propagateHeaders:
Working principle
The framework automatically completes the transparent transmission link when processing each inbound request, without any manual operation:
Manual transparent transmission (temporary solution)
If a header is not declared in the global propagateHeaders, but this request requires transparent transmission, set it manually in init.headers:
- requestId (vext built-in): automatically generated for log correlation and internal inter-service tracking
- traceId (APM system): generated by OpenTelemetry / Jaeger, etc., transparently transmitted through
propagateHeaders
For details, see [Request Context → Relationship with Distributed Tracing](/guide/request-context#Relationship with Distributed Tracing traceId) for complete instructions.
Timeout control
app.fetch uses AbortController to implement timeout control. Throw an Error with explicit information after the timeout:
If the request is passed in signal at the same time (such as manual cancellation by the user), app.fetch will merge the two signals - either trigger will abort the request.
Automatic retry
Retry only takes effect for idempotent methods (GET / HEAD / OPTIONS / PUT / DELETE), POST / PATCH will not be retried (to avoid repeated execution of side effects).
List of idempotent methods
The following methods are considered idempotent and allow automatic retries:
Trigger conditions
Retry decision process
Behavior when the final retry fails
This is the most important detail - the final behavior of 5xx and network errors is different:
Retry log
Each retry will record a debug level log, including the current number of retries and the maximum number of retries:
The retry log is not recorded for the first request, and is only output when attempt >= 1. In the production environment, logger.level: 'info' will not output the retry log (the debug level is silenced).
Exponential backoff
retryDelay supports functional form to implement exponential backoff strategy:
The default retryDelay is fixed 1000ms (1 second).
Structured log
Structured logs are automatically recorded for each outbound request, containing the following fields:
Log levels automatically adjust based on response status:
Example of log output:
Replace fetch implementation
The current version does not expose the app.setFetch() public API, so it does not support directly replacing the framework's built-in app.fetch in the plug-in.
If you need to use axios or other HTTP clients, it is recommended to mount the independent client through app.extend() in the plugin instead of overriding the built-in implementation:
If you bypass the built-in app.fetch, requestId propagation, timeouts, retries and structured logging will all need to be implemented yourself. In most scenarios, it is recommended to use the built-in app.fetch directly, or mount a dedicated client based on app.fetch.create().
Complete example: calls between microservices
Here is a complete example of an order service calling a user service and an inventory service:
Throughout the call chain, requestId is automatically propagated from inbound requests to all outbound requests, enabling complete distributed tracing.
Next step
- Learn how the requestId and request context middleware generates and manages requestIds
- See plugins how to mount a custom client through
app.extend() - Explore global configuration items related to
fetchin Configuration - Learn how to mock
app.fetchfor unit testing in Testing