Skip to content

Commit b5c4b2d

Browse files
committed
feat: Enhance ApiService to support token refresh on 401 errors
1 parent 957e0b4 commit b5c4b2d

File tree

1 file changed

+46
-14
lines changed

1 file changed

+46
-14
lines changed

packages/api/src/api-services.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,38 @@ export type ApiServiceHook = {
1717
onResponse?: (res: Response, req: RequestInit & { url: string }) => void | Promise<void>;
1818
};
1919

20+
export interface ApiServiceConfigWithRefresh extends ApiServiceConfig {
21+
onRefreshToken?: () => Promise<string | null>;
22+
}
23+
2024
export class ApiService implements ApiService {
21-
private config: ApiServiceConfig;
25+
private config: ApiServiceConfigWithRefresh;
2226
private hooks: ApiServiceHook[] = [];
2327

24-
constructor(config: ApiServiceConfig) {
28+
constructor(config: ApiServiceConfigWithRefresh) {
2529
this.config = config;
2630
}
2731

32+
/**
33+
* Internal helper to handle 401, refresh token, and retry once
34+
*/
35+
private async handle401AndRetry(
36+
req: RequestInit & { url: string },
37+
doRequest: () => Promise<Response>,
38+
updateHeaders: (token: string) => void
39+
): Promise<Response> {
40+
let res = await doRequest();
41+
if (res.status === 401 && this.config.onRefreshToken) {
42+
const newToken = await this.config.onRefreshToken();
43+
if (newToken) {
44+
this.config.apiKey = newToken;
45+
updateHeaders(newToken);
46+
res = await doRequest();
47+
}
48+
}
49+
return res;
50+
}
51+
2852
/**
2953
* Register a request/response hook
3054
*/
@@ -45,8 +69,12 @@ export class ApiService implements ApiService {
4569
headers: this.config.apiKey ? { 'Authorization': `Bearer ${this.config.apiKey}` } : undefined,
4670
};
4771
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
72+
const updateHeaders = (token: string) => {
73+
req.headers = { ...(req.headers || {}), Authorization: `Bearer ${token}` };
74+
};
4875
try {
49-
const res = await fetch(req.url, req);
76+
const doRequest = () => fetch(req.url, req);
77+
const res = await this.handle401AndRetry(req, doRequest, updateHeaders);
5078
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
5179
const data = await res.json().catch(() => undefined);
5280
if (!res.ok) {
@@ -80,7 +108,11 @@ export class ApiService implements ApiService {
80108
body,
81109
};
82110
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
83-
const res = await fetch(req.url, req);
111+
const updateHeaders = (token: string) => {
112+
req.headers = { ...(req.headers || {}), Authorization: `Bearer ${token}` };
113+
};
114+
const doRequest = () => fetch(req.url, req);
115+
const res = await this.handle401AndRetry(req, doRequest, updateHeaders);
84116
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
85117
const respData = await res.json().catch(() => undefined);
86118
if (!res.ok) {
@@ -100,16 +132,16 @@ export class ApiService implements ApiService {
100132
headers: this.config.apiKey ? { 'Authorization': `Bearer ${this.config.apiKey}` } : undefined,
101133
};
102134
for (const hook of this.hooks) if (hook.onRequest) await hook.onRequest(req);
103-
try {
104-
const res = await fetch(req.url, req);
105-
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
106-
const data = await res.json().catch(() => undefined);
107-
if (!res.ok) {
108-
return { isSucceed: false, data, errors: [res.statusText], status: res.status };
109-
}
110-
return { isSucceed: true, data, status: res.status };
111-
} catch (err) {
112-
return { isSucceed: false, errors: [(err as Error).message] };
135+
const updateHeaders = (token: string) => {
136+
req.headers = { ...(req.headers || {}), Authorization: `Bearer ${token}` };
137+
};
138+
const doRequest = () => fetch(req.url, req);
139+
const res = await this.handle401AndRetry(req, doRequest, updateHeaders);
140+
for (const hook of this.hooks) if (hook.onResponse) await hook.onResponse(res, req);
141+
const data = await res.json().catch(() => undefined);
142+
if (!res.ok) {
143+
return { isSucceed: false, data, errors: [res.statusText], status: res.status };
113144
}
145+
return { isSucceed: true, data, status: res.status };
114146
}
115147
}

0 commit comments

Comments
 (0)