Skip to content

Commit 9b291b6

Browse files
fix(javascript): ensure requesters work as in v4
algolia/api-clients-automation#823 Co-authored-by: Clément Vannicatte <[email protected]>
1 parent fd2731d commit 9b291b6

File tree

12 files changed

+770
-19
lines changed

12 files changed

+770
-19
lines changed

packages/client-common/src/transporter/createTransporter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function createTransporter({
180180
return stackFrame;
181181
};
182182

183-
const response = await requester.send(payload, request);
183+
const response = await requester.send(payload);
184184

185185
if (isRetryable(response)) {
186186
const stackFrame = pushToStackTrace(response);

packages/client-common/src/types/Requester.ts

+26-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import type { Headers, QueryParameters } from './Transporter';
22

3+
/**
4+
* The method of the request.
5+
*/
36
export type Method = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
47

58
export type Request = {
69
method: Method;
10+
/**
11+
* The path of the REST API to send the request to.
12+
*/
713
path: string;
814
queryParameters: QueryParameters;
915
data?: Array<Record<string, any>> | Record<string, any>;
@@ -20,26 +26,42 @@ export type Request = {
2026
useReadTransporter?: boolean;
2127
};
2228

23-
export type EndRequest = {
24-
method: Method;
29+
export type EndRequest = Pick<Request, 'headers' | 'method'> & {
30+
/**
31+
* The full URL of the REST API.
32+
*/
2533
url: string;
34+
/**
35+
* The connection timeout, in milliseconds.
36+
*/
2637
connectTimeout: number;
38+
/**
39+
* The response timeout, in milliseconds.
40+
*/
2741
responseTimeout: number;
28-
headers: Headers;
2942
data?: string;
3043
};
3144

3245
export type Response = {
46+
/**
47+
* The body of the response.
48+
*/
3349
content: string;
50+
/**
51+
* Whether the API call is timed out or not.
52+
*/
3453
isTimedOut: boolean;
54+
/**
55+
* The HTTP status code of the response.
56+
*/
3557
status: number;
3658
};
3759

3860
export type Requester = {
3961
/**
4062
* Sends the given `request` to the server.
4163
*/
42-
send: (request: EndRequest, originalRequest: Request) => Promise<Response>;
64+
send: (request: EndRequest) => Promise<Response>;
4365
};
4466

4567
export type EchoResponse = Omit<EndRequest, 'data'> &

packages/client-common/src/types/Transporter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type RequestOptions = Pick<Request, 'cacheable'> & {
2626
queryParameters?: QueryParameters;
2727

2828
/**
29-
* Custom data for the request. This data are
29+
* Custom data for the request. This data is
3030
* going to be merged the transporter data.
3131
*/
3232
data?: Array<Record<string, any>> | Record<string, any>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Config } from '@jest/types';
2+
3+
const config: Config.InitialOptions = {
4+
preset: 'ts-jest',
5+
roots: ['src/__tests__'],
6+
testEnvironment: 'jsdom',
7+
};
8+
9+
export default config;

packages/requester-browser-xhr/package.json

+8-3
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,20 @@
1414
"index.ts"
1515
],
1616
"scripts": {
17-
"build": "tsc",
18-
"clean": "rm -rf dist/"
17+
"clean": "rm -rf dist/",
18+
"test": "jest"
1919
},
2020
"dependencies": {
2121
"@algolia/client-common": "5.0.0-alpha.0"
2222
},
2323
"devDependencies": {
24+
"@types/jest": "28.1.4",
2425
"@types/node": "16.11.45",
25-
"typescript": "4.7.4"
26+
"jest": "28.1.2",
27+
"jest-environment-jsdom": "28.1.2",
28+
"ts-jest": "28.0.5",
29+
"typescript": "4.7.4",
30+
"xhr-mock": "2.5.1"
2631
},
2732
"engines": {
2833
"node": ">= 14.0.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import http from 'http';
2+
3+
import type { EndRequest, Headers } from '@algolia/client-common';
4+
import type { MockRequest, MockResponse } from 'xhr-mock';
5+
import mock from 'xhr-mock';
6+
7+
import { createXhrRequester } from '../..';
8+
9+
const requester = createXhrRequester();
10+
const BASE_URL = 'https://algolia-dns.net/foo?x-algolia-header=bar';
11+
12+
function getStringifiedBody(
13+
body: Record<string, any> = { foo: 'bar' }
14+
): string {
15+
return JSON.stringify(body);
16+
}
17+
18+
const headers: Headers = {
19+
'content-type': 'text/plain',
20+
};
21+
22+
const timeoutRequest: EndRequest = {
23+
url: 'missing-url-here',
24+
data: '',
25+
headers: {},
26+
method: 'GET',
27+
responseTimeout: 2000,
28+
connectTimeout: 1000,
29+
};
30+
31+
const requestStub: EndRequest = {
32+
url: BASE_URL,
33+
method: 'POST',
34+
headers,
35+
data: getStringifiedBody(),
36+
responseTimeout: 1000,
37+
connectTimeout: 2000,
38+
};
39+
40+
describe('status code handling', () => {
41+
beforeEach(() => mock.setup());
42+
afterEach(() => mock.teardown());
43+
44+
it('sends requests', async () => {
45+
mock.post(BASE_URL, (req: MockRequest, res: MockResponse): MockResponse => {
46+
expect(req.method()).toEqual('POST');
47+
expect(req.header('content-type')).toEqual('text/plain');
48+
expect(req.body()).toEqual(JSON.stringify({ foo: 'bar' }));
49+
50+
return res.status(200);
51+
});
52+
53+
await requester.send(requestStub);
54+
});
55+
56+
it('resolves status 200', async () => {
57+
const body = getStringifiedBody();
58+
59+
mock.post(BASE_URL, {
60+
status: 200,
61+
body: requestStub.data,
62+
});
63+
64+
const response = await requester.send(requestStub);
65+
66+
expect(response.status).toBe(200);
67+
expect(response.content).toBe(body);
68+
expect(response.isTimedOut).toBe(false);
69+
});
70+
71+
it('resolves status 300', async () => {
72+
const reason = 'Multiple Choices';
73+
74+
mock.post(BASE_URL, {
75+
status: 300,
76+
reason,
77+
});
78+
79+
const response = await requester.send(requestStub);
80+
81+
expect(response.status).toBe(300);
82+
expect(response.content).toBe(''); // No body returned here on xhr
83+
expect(response.isTimedOut).toBe(false);
84+
});
85+
86+
it('resolves status 400', async () => {
87+
const body = getStringifiedBody({
88+
message: 'Invalid Application-Id or API-Key',
89+
});
90+
91+
mock.post(BASE_URL, {
92+
status: 400,
93+
body,
94+
});
95+
96+
const response = await requester.send(requestStub);
97+
98+
expect(response.status).toBe(400);
99+
expect(response.content).toBe(body);
100+
expect(response.isTimedOut).toBe(false);
101+
});
102+
103+
it('handles the protocol', async () => {
104+
const body = getStringifiedBody();
105+
106+
mock.post('http://localhost/', {
107+
status: 200,
108+
body,
109+
});
110+
111+
const response = await requester.send({
112+
...requestStub,
113+
url: 'http://localhost',
114+
});
115+
116+
expect(response.status).toBe(200);
117+
expect(response.content).toBe(body);
118+
expect(response.isTimedOut).toBe(false);
119+
});
120+
});
121+
122+
describe('timeout handling', () => {
123+
let server: http.Server;
124+
// setup http server to test timeout
125+
beforeAll(() => {
126+
server = http.createServer(function (_req, res) {
127+
res.writeHead(200, {
128+
'content-type': 'text/plain',
129+
'access-control-allow-origin': '*',
130+
'x-powered-by': 'nodejs',
131+
});
132+
133+
res.write('{"foo":');
134+
135+
setTimeout(() => {
136+
res.write(' "bar"');
137+
}, 1000);
138+
139+
setTimeout(() => {
140+
res.write('}');
141+
res.end();
142+
}, 5000);
143+
});
144+
145+
server.listen('1111');
146+
});
147+
148+
afterAll((done) => {
149+
server.close(() => done());
150+
});
151+
152+
it('connection timeouts with the given 1 seconds connection timeout', async () => {
153+
const before = Date.now();
154+
const response = await requester.send({
155+
...timeoutRequest,
156+
connectTimeout: 1000,
157+
url: 'http://www.google.com:81',
158+
});
159+
160+
const now = Date.now();
161+
162+
expect(response.content).toBe('Connection timeout');
163+
expect(now - before).toBeGreaterThan(999);
164+
expect(now - before).toBeLessThan(1200);
165+
});
166+
167+
it('connection timeouts with the given 2 seconds connection timeout', async () => {
168+
const before = Date.now();
169+
const response = await requester.send({
170+
...timeoutRequest,
171+
connectTimeout: 2000,
172+
url: 'http://www.google.com:81',
173+
});
174+
175+
const now = Date.now();
176+
177+
expect(response.content).toBe('Connection timeout');
178+
expect(now - before).toBeGreaterThan(1990);
179+
expect(now - before).toBeLessThan(2200);
180+
});
181+
182+
it("socket timeouts if response don't appears before the timeout with 2 seconds timeout", async () => {
183+
const before = Date.now();
184+
185+
const response = await requester.send({
186+
...timeoutRequest,
187+
responseTimeout: 2000,
188+
url: 'http://localhost:1111',
189+
});
190+
191+
const now = Date.now();
192+
193+
expect(response.content).toBe('Socket timeout');
194+
expect(now - before).toBeGreaterThan(1990);
195+
expect(now - before).toBeLessThan(2200);
196+
});
197+
198+
it("socket timeouts if response don't appears before the timeout with 3 seconds timeout", async () => {
199+
const before = Date.now();
200+
201+
const response = await requester.send({
202+
...timeoutRequest,
203+
responseTimeout: 3000,
204+
url: 'http://localhost:1111',
205+
});
206+
207+
const now = Date.now();
208+
209+
expect(response.content).toBe('Socket timeout');
210+
expect(now - before).toBeGreaterThan(2999);
211+
expect(now - before).toBeLessThan(3200);
212+
});
213+
214+
it('do not timeouts if response appears before the timeout', async () => {
215+
const before = Date.now();
216+
const response = await requester.send({
217+
...requestStub,
218+
responseTimeout: 6000,
219+
url: 'http://localhost:1111',
220+
});
221+
222+
const now = Date.now();
223+
224+
expect(response.isTimedOut).toBe(false);
225+
expect(response.status).toBe(200);
226+
expect(response.content).toBe('{"foo": "bar"}');
227+
expect(now - before).toBeGreaterThan(4999);
228+
expect(now - before).toBeLessThan(5200);
229+
}, 10000); // This is a long-running test, default server timeout is set to 5000ms
230+
});
231+
232+
describe('error handling', () => {
233+
it('resolves dns not found', async () => {
234+
const request: EndRequest = {
235+
url: 'https://this-dont-exist.algolia.com',
236+
method: 'POST',
237+
headers,
238+
data: getStringifiedBody(),
239+
responseTimeout: 2000,
240+
connectTimeout: 1000,
241+
};
242+
243+
const response = await requester.send(request);
244+
245+
expect(response.status).toBe(0);
246+
expect(response.content).toBe('Network request failed');
247+
expect(response.isTimedOut).toBe(false);
248+
});
249+
250+
it('resolves general network errors', async () => {
251+
mock.post(BASE_URL, () =>
252+
Promise.reject(new Error('This is a general error'))
253+
);
254+
255+
const response = await requester.send(requestStub);
256+
257+
expect(response.status).toBe(0);
258+
expect(response.content).toBe('Network request failed');
259+
expect(response.isTimedOut).toBe(false);
260+
});
261+
});
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
{
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
4+
"types": ["node", "jest"],
45
"outDir": "dist"
56
},
67
"include": ["src", "index.ts"],
7-
"exclude": ["dist", "node_modules"]
8+
"exclude": ["dist", "node_modules", "src/__tests__"]
89
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { Config } from '@jest/types';
2+
3+
const config: Config.InitialOptions = {
4+
preset: 'ts-jest',
5+
roots: ['src/__tests__'],
6+
testEnvironment: 'node',
7+
};
8+
9+
export default config;

0 commit comments

Comments
 (0)