Mobile Development 16 min read

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.

51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
51CTO HarmonyOS Developer Community
HarmonyOS Network Library: Chainable API, Interceptors & Multi-format Requests

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 headers

All 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.git

Structure:

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.ets

Next Preview

A large-file upload/download component based on request.agent (background execution, resumable transfers, system notification bar, task grouping, task recovery). Stay tuned!

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

HarmonyOSFile UploadArkTSHTTP ClientInterceptorsNetwork LibraryChainable API
51CTO HarmonyOS Developer Community
Written by

51CTO HarmonyOS Developer Community

The HarmonyOS Developer Community is a learning-oriented community for developers to learn, communicate, ask questions, and share.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.