UNPKG

4.96 kBPlain TextView Raw
1import axios, {
2 AxiosInstance,
3 AxiosError,
4 AxiosResponse,
5 AxiosRequestConfig,
6} from "axios";
7import { Readable } from "node:stream";
8import { HTTPError, ReadError, RequestError } from "./exceptions.js";
9import { USER_AGENT } from "./version.js";
10
11interface httpClientConfig extends Partial<AxiosRequestConfig> {
12 baseURL?: string;
13 defaultHeaders?: any;
14 responseParser?: <T>(res: AxiosResponse) => T;
15}
16
17export 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