Skip to content

Commit 38b97d0

Browse files
authored
add X-Line-Retry-Key support (#224)
* add X-Line-Retry-Key support * add docs
1 parent b407e0d commit 38b97d0

File tree

4 files changed

+119
-24
lines changed

4 files changed

+119
-24
lines changed

docs/api-reference/client.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ class Client {
1111

1212
constructor(config: ClientConfig) {}
1313

14+
// requestOption
15+
setRequestOptionOnce(option: Partial<{
16+
retryKey: string;
17+
}>)
18+
1419
// Message
1520
pushMessage(to: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
1621
replyMessage(replyToken: string, messages: Message | Message[], notificationDisabled: boolean = false): Promise<MessageAPIResponseBase>
@@ -152,6 +157,11 @@ interface ClientConfig {
152157
}
153158
```
154159

160+
## Common Specifications
161+
162+
Regarding to things like [Retrying an API request](https://developers.line.biz/en/reference/messaging-api/#retry-api-request), there's an API called `setRequestOptionOnce`.
163+
When you call this first and call the API support that request option, then it will be set to that request and will be cleared automatically.
164+
155165
## Methods
156166

157167
For a parameter `messages: messages: Message | Message[]`, you can provide a

lib/client.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { AxiosResponse, AxiosRequestConfig } from "axios";
66
import { ensureJSON, toArray } from "./utils";
77

88
type ChatType = "group" | "room";
9+
type RequestOption = {
10+
retryKey: string;
11+
};
912
import {
1013
MESSAGING_API_PREFIX,
1114
DATA_API_PREFIX,
@@ -16,6 +19,8 @@ export default class Client {
1619
public config: Types.ClientConfig;
1720
private http: HTTPClient;
1821

22+
private requestOption: Partial<RequestOption> = {};
23+
1924
constructor(config: Types.ClientConfig) {
2025
if (!config.channelAccessToken) {
2126
throw new Error("no channel access token");
@@ -30,6 +35,20 @@ export default class Client {
3035
...config.httpConfig,
3136
});
3237
}
38+
public setRequestOptionOnce(option: Partial<RequestOption>) {
39+
this.requestOption = option;
40+
}
41+
42+
private generateRequestConfig(): Partial<AxiosRequestConfig> {
43+
const config: Partial<AxiosRequestConfig> = { headers: {} };
44+
if (this.requestOption.retryKey) {
45+
config.headers["X-Line-Retry-Key"] = this.requestOption.retryKey;
46+
}
47+
48+
// clear requestOption
49+
this.requestOption = {};
50+
return config;
51+
}
3352

3453
private parseHTTPResponse(response: AxiosResponse) {
3554
const { LINE_REQUEST_ID_HTTP_HEADER_NAME } = Types;
@@ -48,11 +67,15 @@ export default class Client {
4867
messages: Types.Message | Types.Message[],
4968
notificationDisabled: boolean = false,
5069
): Promise<Types.MessageAPIResponseBase> {
51-
return this.http.post(`${MESSAGING_API_PREFIX}/message/push`, {
52-
messages: toArray(messages),
53-
to,
54-
notificationDisabled,
55-
});
70+
return this.http.post(
71+
`${MESSAGING_API_PREFIX}/message/push`,
72+
{
73+
messages: toArray(messages),
74+
to,
75+
notificationDisabled,
76+
},
77+
this.generateRequestConfig(),
78+
);
5679
}
5780

5881
public replyMessage(
@@ -72,11 +95,15 @@ export default class Client {
7295
messages: Types.Message | Types.Message[],
7396
notificationDisabled: boolean = false,
7497
): Promise<Types.MessageAPIResponseBase> {
75-
return this.http.post(`${MESSAGING_API_PREFIX}/message/multicast`, {
76-
messages: toArray(messages),
77-
to,
78-
notificationDisabled,
79-
});
98+
return this.http.post(
99+
`${MESSAGING_API_PREFIX}/message/multicast`,
100+
{
101+
messages: toArray(messages),
102+
to,
103+
notificationDisabled,
104+
},
105+
this.generateRequestConfig(),
106+
);
80107
}
81108

82109
public async narrowcast(
@@ -86,23 +113,31 @@ export default class Client {
86113
limit?: { max: number },
87114
notificationDisabled: boolean = false,
88115
): Promise<Types.MessageAPIResponseBase> {
89-
return this.http.post(`${MESSAGING_API_PREFIX}/message/narrowcast`, {
90-
messages: toArray(messages),
91-
recipient,
92-
filter,
93-
limit,
94-
notificationDisabled,
95-
});
116+
return this.http.post(
117+
`${MESSAGING_API_PREFIX}/message/narrowcast`,
118+
{
119+
messages: toArray(messages),
120+
recipient,
121+
filter,
122+
limit,
123+
notificationDisabled,
124+
},
125+
this.generateRequestConfig(),
126+
);
96127
}
97128

98129
public async broadcast(
99130
messages: Types.Message | Types.Message[],
100131
notificationDisabled: boolean = false,
101132
): Promise<Types.MessageAPIResponseBase> {
102-
return this.http.post(`${MESSAGING_API_PREFIX}/message/broadcast`, {
103-
messages: toArray(messages),
104-
notificationDisabled,
105-
});
133+
return this.http.post(
134+
`${MESSAGING_API_PREFIX}/message/broadcast`,
135+
{
136+
messages: toArray(messages),
137+
notificationDisabled,
138+
},
139+
this.generateRequestConfig(),
140+
);
106141
}
107142

108143
public async getProfile(userId: string): Promise<Types.Profile> {

lib/http.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,17 @@ export default class HTTPClient {
5050
return res.data as Readable;
5151
}
5252

53-
public async post<T>(url: string, body?: any): Promise<T> {
53+
public async post<T>(
54+
url: string,
55+
body?: any,
56+
config?: Partial<AxiosRequestConfig>,
57+
): Promise<T> {
5458
const res = await this.instance.post(url, body, {
55-
headers: { "Content-Type": "application/json" },
59+
headers: {
60+
"Content-Type": "application/json",
61+
...(config && config.headers),
62+
},
63+
...config,
5664
});
5765

5866
return this.responseParse(res);
@@ -70,7 +78,10 @@ export default class HTTPClient {
7078
config?: Partial<AxiosRequestConfig>,
7179
): Promise<T> {
7280
const res = await this.instance.put(url, body, {
73-
headers: { "Content-Type": "application/json" },
81+
headers: {
82+
"Content-Type": "application/json",
83+
...(config && config.headers),
84+
},
7485
...config,
7586
});
7687

test/client.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,45 @@ describe("client", () => {
807807
equal(scope.isDone(), true);
808808
});
809809

810+
it("set option once and clear option", async () => {
811+
const expectedBody = {
812+
messages: [testMsg],
813+
to: "test_user_id",
814+
notificationDisabled: false,
815+
};
816+
const retryKey = "retryKey";
817+
818+
const firstRequest = nock(MESSAGING_API_PREFIX, {
819+
reqheaders: {
820+
...interceptionOption.reqheaders,
821+
"X-Line-Retry-Key": retryKey,
822+
},
823+
})
824+
.post(`/message/push`, expectedBody)
825+
.reply(responseFn);
826+
const secondRequest = mockPost(MESSAGING_API_PREFIX, `/message/push`, {
827+
messages: [testMsg],
828+
to: "test_user_id",
829+
notificationDisabled: false,
830+
});
831+
832+
client.setRequestOptionOnce({
833+
retryKey,
834+
});
835+
836+
const firstResPromise = client.pushMessage("test_user_id", testMsg);
837+
const secondResPromise = client.pushMessage("test_user_id", testMsg);
838+
839+
const [firstRes, secondRes] = await Promise.all([
840+
firstResPromise,
841+
secondResPromise,
842+
]);
843+
equal(firstRequest.isDone(), true);
844+
equal(secondRequest.isDone(), true);
845+
equal(firstRes["x-line-request-id"], "X-Line-Request-Id");
846+
equal(secondRes["x-line-request-id"], "X-Line-Request-Id");
847+
});
848+
810849
it("fails on construct with no channelAccessToken", () => {
811850
try {
812851
new Client({ channelAccessToken: null });

0 commit comments

Comments
 (0)