1 | import axios, {
|
2 | AxiosInstance,
|
3 | AxiosError,
|
4 | AxiosResponse,
|
5 | AxiosRequestConfig,
|
6 | } from "axios";
|
7 | import { Readable } from "node:stream";
|
8 | import { HTTPError, ReadError, RequestError } from "./exceptions.js";
|
9 | import { USER_AGENT } from "./version.js";
|
10 |
|
11 | interface httpClientConfig extends Partial<AxiosRequestConfig> {
|
12 | baseURL?: string;
|
13 | defaultHeaders?: any;
|
14 | responseParser?: <T>(res: AxiosResponse) => T;
|
15 | }
|
16 |
|
17 | export default class HTTPClient {
|
18 | private instance: AxiosInstance;
|
19 | private readonly config: httpClientConfig;
|
20 |
|
21 | constructor(config: httpClientConfig = {}) {
|
22 | this.config = config;
|
23 | const { baseURL, defaultHeaders } = config;
|
24 | this.instance = axios.create({
|
25 | baseURL,
|
26 | headers: Object.assign({}, defaultHeaders, {
|
27 | "User-Agent": USER_AGENT,
|
28 | }),
|
29 | });
|
30 |
|
31 | this.instance.interceptors.response.use(
|
32 | res => res,
|
33 | err => Promise.reject(this.wrapError(err)),
|
34 | );
|
35 | }
|
36 |
|
37 | public async get<T>(url: string, params?: any): Promise<T> {
|
38 | const res = await this.instance.get(url, { params });
|
39 | return res.data;
|
40 | }
|
41 |
|
42 | public async getStream(url: string, params?: any): Promise<Readable> {
|
43 | const res = await this.instance.get(url, {
|
44 | params,
|
45 | responseType: "stream",
|
46 | });
|
47 | return res.data as Readable;
|
48 | }
|
49 |
|
50 | public async post<T>(
|
51 | url: string,
|
52 | body?: any,
|
53 | config?: Partial<AxiosRequestConfig>,
|
54 | ): Promise<T> {
|
55 | const res = await this.instance.post(url, body, {
|
56 | headers: {
|
57 | "Content-Type": "application/json",
|
58 | ...(config && config.headers),
|
59 | },
|
60 | ...config,
|
61 | });
|
62 |
|
63 | return this.responseParse(res);
|
64 | }
|
65 |
|
66 | private responseParse(res: AxiosResponse) {
|
67 | const { responseParser } = this.config;
|
68 | if (responseParser) return responseParser(res);
|
69 | else return res.data;
|
70 | }
|
71 |
|
72 | public async put<T>(
|
73 | url: string,
|
74 | body?: any,
|
75 | config?: Partial<AxiosRequestConfig>,
|
76 | ): Promise<T> {
|
77 | const res = await this.instance.put<T>(url, body, {
|
78 | headers: {
|
79 | "Content-Type": "application/json",
|
80 | ...(config && config.headers),
|
81 | },
|
82 | ...config,
|
83 | });
|
84 |
|
85 | return this.responseParse(res);
|
86 | }
|
87 |
|
88 | public async postForm<T>(url: string, body?: any): Promise<T> {
|
89 | const params = new URLSearchParams();
|
90 | for (const key in body) {
|
91 | if (body.hasOwnProperty(key)) {
|
92 | params.append(key, body[key]);
|
93 | }
|
94 | }
|
95 | const res = await this.instance.post(url, params.toString(), {
|
96 | headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
97 | });
|
98 |
|
99 | return res.data;
|
100 | }
|
101 |
|
102 | public async postFormMultipart<T>(url: string, form: FormData): Promise<T> {
|
103 | const res = await this.instance.post<T>(url, form);
|
104 | return res.data;
|
105 | }
|
106 |
|
107 | public async putFormMultipart<T>(
|
108 | url: string,
|
109 | form: FormData,
|
110 | config?: Partial<AxiosRequestConfig>,
|
111 | ): Promise<T> {
|
112 | const res = await this.instance.put<T>(url, form, config);
|
113 | return res.data;
|
114 | }
|
115 |
|
116 | public async toBuffer(data: Buffer | Readable) {
|
117 | if (Buffer.isBuffer(data)) {
|
118 | return data;
|
119 | } else if (data instanceof Readable) {
|
120 | return await new Promise<Buffer>((resolve, reject) => {
|
121 | const buffers: Buffer[] = [];
|
122 | let size = 0;
|
123 | data.on("data", (chunk: Buffer) => {
|
124 | buffers.push(chunk);
|
125 | size += chunk.length;
|
126 | });
|
127 | data.on("end", () => resolve(Buffer.concat(buffers, size)));
|
128 | data.on("error", reject);
|
129 | });
|
130 | } else {
|
131 | throw new Error("invalid data type for binary data");
|
132 | }
|
133 | }
|
134 |
|
135 | public async postBinary<T>(
|
136 | url: string,
|
137 | data: Buffer | Readable,
|
138 | contentType?: string,
|
139 | ): Promise<T> {
|
140 | const buffer = await this.toBuffer(data);
|
141 |
|
142 | const res = await this.instance.post(url, buffer, {
|
143 | headers: {
|
144 | "Content-Type": contentType || "image/png",
|
145 | "Content-Length": buffer.length,
|
146 | },
|
147 | });
|
148 |
|
149 | return res.data;
|
150 | }
|
151 |
|
152 | public async postBinaryContent<T>(url: string, body: Blob): Promise<T> {
|
153 | const res = await this.instance.post(url, body, {
|
154 | headers: {
|
155 | "Content-Type": body.type,
|
156 | "Content-Length": body.size,
|
157 | },
|
158 | });
|
159 |
|
160 | return res.data;
|
161 | }
|
162 |
|
163 | public async delete<T>(url: string, params?: any): Promise<T> {
|
164 | const res = await this.instance.delete(url, { params });
|
165 | return res.data;
|
166 | }
|
167 |
|
168 | private wrapError(err: AxiosError): Error {
|
169 | if (err.response) {
|
170 | const { status, statusText } = err.response;
|
171 | const { message } = err;
|
172 |
|
173 | return new HTTPError(message, {
|
174 | statusCode: status,
|
175 | statusMessage: statusText,
|
176 | originalError: err,
|
177 | });
|
178 | } else if (err.code) {
|
179 | const { message, code } = err;
|
180 | return new RequestError(message, { code, originalError: err });
|
181 | } else if (err.config) {
|
182 | // unknown, but from axios
|
183 | const { message } = err;
|
184 |
|
185 | return new ReadError(message, { originalError: err });
|
186 | }
|
187 |
|
188 | // otherwise, just rethrow
|
189 | return err;
|
190 | }
|
191 | }
|
192 |
|
\ | No newline at end of file |