HarmonyOS Network Library: Chainable API, Interceptors & Multi-format Requests
This article details a HarmonyOS network request library encapsulation featuring global configuration, chainable API, request/response interceptors, and support for JSON, form, binary, and multipart file uploads, with complete implementation code, usage examples, and pitfalls like ArkTS type constraints and large-file memory limits.
Why Wrap the Native HTTP Module?
Using HarmonyOS's official http module directly requires repetitive boilerplate for every request: calling createHttp(), setting headers, parsing the response, and invoking destroy(). This article presents a custom network library that eliminates that boilerplate by providing global configuration, a chainable API, request/response interceptors, and built-in handling for JSON, form data, binary payloads, and multipart file uploads. The complete source code is available at
https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git.
Quick Demo: Login Request
The library modularizes network requests. For a login endpoint:
Define request and response interfaces:
// Request body interface
interface LoginRequest {
username: string;
password: string;
}
// Generic response wrapper
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface AuthTokenModel {
accessToken: string;
refreshToken: string;
userId: string;
}Construct the request payload:
const request: LoginRequest = {
username: this.username.trim(),
password: encryptedPassword
};Execute the request with the chainable API:
const resp = await httpClient.post(APIConstants.API_LOGIN)
.json(request)
.execute<ApiResponse<AuthTokenModel>>();
// Use the response
if (resp.status === 200 && resp.data.code === 200) {
this.authToken.accessToken = resp.data.data.accessToken;
this.authToken.refreshToken = resp.data.data.refreshToken;
this.authToken.userId = resp.data.data.userId;
promptAction.showToast({ message: 'Login successful' });
} else {
promptAction.showToast({ message: resp.data.message || 'Login failed' });
}Key points: httpClient.post(url) takes a relative path (e.g., 'api/v1/auth/login'); the baseUrl is configured globally in HttpConfig. .json(parameter) automatically serializes the object and sets Content-Type: application/json.
Generics ( <ApiResponse<AuthTokenModel>>) provide type-safe response data.
No manual createHttp(), response parsing, or destroy() — the library handles lifecycle internally.
Global Configuration
Configure the library once at app startup (e.g., in EntryAbility.onCreate or Index.aboutToAppear):
import { httpClient, HttpConfig } from '@happy/http';
import { APIConstants } from '../common/APIConstants';
// Global network parameters (run once)
HttpConfig.getInstance()
.setBaseUrl(APIConstants.BASE_URL)
.setTimeout(30000) // global timeout in ms
.setDefaultHeader('Content-Type', 'application/json');
// Request interceptor: auto-inject token
httpClient.addRequestInterceptor({
onRequest: (options) => {
options.token = token;
return options;
}
});
// Response interceptor: unified 401 handling
httpClient.addResponseInterceptor({
onResponse: (response) => {
if (response.status === 401) console.log('Unauthorized, please log in again');
return response;
},
onError: (error) => console.error('Network error:', error.message)
});After configuration, every request automatically includes the baseUrl, default timeout, default headers, and interceptors for token injection and error handling.
Before vs. After Comparison
Repeated httpRequest creation/destruction → Library manages lifecycle internally
Scattered baseUrl and timeout settings → HttpConfig singleton centralizes configuration
Manual JSON parsing of response → Automatic handling of string, ArrayBuffer, Object → JSON
No unified token/401 handling → Request/response interceptors
Progress callbacks, request cancellation → Native support via builder methods
Large-file memory overflow → Library targets small files (≤10 MB); large files use future request.agent solution
Overall Architecture
Call Layer (business code)
│
Export Layer (index.ets) ← exposes only httpClient singleton & types
│
Interface Layer (HttpClientInterface) ← get/post/put/delete/upload + interceptor management
│
Builder Layer (GetBuilder/PostBuilder/…) ← chainable params & request body
│
Core Layer (HttpClient) ← sends request, runs interceptors, parses response
│
Config Layer (HttpConfig) ← baseUrl, default timeout, default headersAll Builder classes are not exported; they are only accessible via the httpClient singleton, ensuring encapsulation.
Core Code Snippets
1. Global Config (Singleton)
// config/HttpConfig.ets
export class HttpConfig {
private static instance: HttpConfig;
public baseUrl = '';
public defaultTimeout = 30000;
public defaultHeaders: Record<string, string> = {};
static getInstance(): HttpConfig {
if (!HttpConfig.instance) {
HttpConfig.instance = new HttpConfig();
}
return HttpConfig.instance;
}
setBaseUrl(url: string): HttpConfig {
this.baseUrl = url;
return this;
}
setTimeout(ms: number): HttpConfig {
this.defaultTimeout = ms;
return this;
}
setDefaultHeader(key: string, value: string): HttpConfig {
this.defaultHeaders[key] = value;
return this;
}
}2. Abstract Builder Base Class
All builders inherit this class, unifying URL params, headers, timeout, progress, and cancellation.
// builder/BaseBuilder.ets (key methods)
export abstract class BaseBuilder<T> {
protected options: RequestOptions;
private onProgressCallback?: (loaded: number, total: number) => void;
private httpRequest?: http.HttpRequest;
public param(key: string, value: string | number | boolean): BaseBuilder<T> {
if (!this.options.params) this.options.params = new url.URLParams();
this.options.params.append(key, String(value));
return this;
}
public header(key: string, value: string): BaseBuilder<T> {
if (!this.options.headers) this.options.headers = {};
this.options.headers[key] = value;
return this;
}
public token(token: string): BaseBuilder<T> {
this.options.token = token;
return this;
}
public timeout(ms: number): BaseBuilder<T> {
this.options.timeout = ms;
return this;
}
public onProgress(cb: (loaded: number, total: number) => void): BaseBuilder<T> {
this.onProgressCallback = cb;
return this;
}
public cancel(): void {
this.httpRequest?.destroy();
}
public async execute<TRes = object>(): Promise<HttpResponse<TRes>> {
return this.send<TRes>();
}
}3. Concrete Builder (PostBuilder Example)
// builder/PostBuilder.ets
export class PostBuilder extends BaseBuilder<PostBuilder> {
constructor(url: string) {
super(url, 'POST');
}
public json(data: object): PostBuilder {
this.options.body = JSON.stringify(data);
this.header('Content-Type', 'application/json');
return this;
}
public form(data: Record<string, string>): PostBuilder {
const parts = Object.entries(data).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);
this.options.body = parts.join('&');
this.header('Content-Type', 'application/x-www-form-urlencoded');
return this;
}
public binary(data: ArrayBuffer): PostBuilder {
this.options.body = data;
return this;
}
}4. Interceptor Interfaces
// interceptors/Interceptor.ets
export interface RequestInterceptor {
onRequest: (options: RequestOptions) => RequestOptions | Promise<RequestOptions>;
}
export interface ResponseInterceptor {
onResponse: (response: HttpResponse<object>) => HttpResponse<object> | Promise<HttpResponse<object>>;
onError?: (error: Error) => void;
}5. Core Sender (HttpClient, Simplified)
// core/HttpClient.ets
export class HttpClient implements HttpClientInterface {
// ... singleton, get/post/put/delete/upload methods
public async send<T>(
options: RequestOptions,
onProgress?: (loaded: number, total: number) => void,
builder?: BaseBuilder<object>
): Promise<HttpResponse<T>> {
// 1. Run request interceptors
const intercepted = await this.applyRequestInterceptors(options);
// 2. Create httpRequest
const httpRequest = http.createHttp();
builder?.setHttpRequest(httpRequest);
// 3. Set headers, timeout, progress
// 4. Send request
const raw = await httpRequest.request(intercepted.url, reqOptions);
// 5. Auto-parse response (string / ArrayBuffer / Object)
let data: T;
const result = raw.result;
if (typeof result === 'string') {
try { data = JSON.parse(result) as T; } catch { data = result as T; }
} else if (result instanceof ArrayBuffer) {
const str = new util.TextDecoder().decodeToString(new Uint8Array(result));
try { data = JSON.parse(str) as T; } catch { data = str as T; }
} else {
data = result as T;
}
// 6. Run response interceptors
// 7. Destroy request
return { status: raw.responseCode, data };
}
}6. Export Layer (index.ets)
export { HttpConfig } from './src/main/ets/config/HttpConfig';
export { httpClient } from './src/main/ets/core/HttpClient';
export { RequestInterceptor, ResponseInterceptor } from './src/main/ets/interceptors/Interceptor';
export type { HttpResponse, HttpMethod, RequestOptions } from './src/main/ets/model/types';
export type { GetBuilder, PostBuilder, PutBuilder, DeleteBuilder, UploadBuilder } from './src/main/ets/builder';Usage Examples
1. POST Login (as shown above)
2. GET Request with Query Params
interface User {
id: number;
name: string;
}
const users = await httpClient.get('/users')
.param('page', 1)
.param('size', 10)
.execute<User[]>();3. File Upload (multipart)
Test against a real backend; adjust parameters accordingly.
interface UploadResponse {
url: string;
}
const fileBuffer = await readFileToArrayBuffer(selectedImageUri); // implement yourself
const result = await httpClient.upload('/api/upload')
.field('description', 'My photo')
.file('file', fileBuffer, 'photo.jpg', 'image/jpeg')
.timeout(60000)
.onProgress((loaded, total) => console.log(`${loaded}/${total}`))
.execute<UploadResponse>();4. Manual Request Cancellation
const builder = httpClient.get('/long-report').param('year', 2025);
const promise = builder.execute();
builder.cancel();
promise.catch(err => console.log('Cancelled:', err.message));Pitfalls & Best Practices
ArkTS forbids anonymous object literals. All request bodies, parameter objects, and response types must be defined via interface; you cannot write inline types like { token: string } inside generics.
Chainable call order matters. You must call the request-body method first ( .json(), .form(), .file()), then common methods ( .param(), .timeout()). Common methods return BaseBuilder<T>, losing subclass-specific methods. execute always comes last.
Not suitable for large-file downloads. The http module loads the entire response into memory; files over 10 MB cause OOM. Wait for the upcoming request.agent solution for large files.
Progress callbacks only for small files. Same memory constraint — avoid monitoring tens of MB.
Response type auto-parsing. The library handles string / ArrayBuffer / Object → JSON conversion; you only specify the business type via generics.
Automatic cleanup after request. No manual destroy() needed; call builder.cancel() to abort.
Interceptors support async. onRequest / onResponse can return a Promise, enabling async token refresh.
Full Source Code
Repository:
https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.gitStructure:
http/
├── config/HttpConfig.ets
├── model/types.ets
├── interceptors/Interceptor.ets
├── builder/BaseBuilder.ets
├── builder/GetBuilder.ets
├── builder/PostBuilder.ets
├── builder/PutBuilder.ets
├── builder/DeleteBuilder.ets
├── builder/UploadBuilder.ets
├── core/HttpClient.ets
├── interface/HttpClientInterface.ets
└── index.etsNext Preview
A large-file upload/download component based on request.agent (background execution, resumable transfers, system notification bar, task grouping, task recovery). Stay tuned!
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
51CTO HarmonyOS Developer Community
The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
