Skip to content

Commit f038fab

Browse files
chrisradeksrchase
authored andcommitted
Http Handlers (#57)
* Adds stream collectors * Adds node-http-handler and fetch-http-handler * Adds core-handler
1 parent e2f5f31 commit f038fab

27 files changed

+1153
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
*.js.map
3+
*.d.ts
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/src/
2+
/coverage/
3+
tsconfig.test.json
4+
5+
*.spec.js
6+
*.spec.d.ts
7+
*.spec.js.map
8+
9+
*.mock.js
10+
*.mock.d.ts
11+
*.mock.js.map
12+
13+
*.fixture.js
14+
*.fixture.d.ts
15+
*.fixture.js.map
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@aws/fetch-http-handler",
3+
"private": true,
4+
"version": "0.0.1",
5+
"description": "Provides a way to make requests",
6+
"scripts": {
7+
"prepublishOnly": "tsc",
8+
"pretest": "tsc -p tsconfig.test.json",
9+
"test": "jest --coverage"
10+
},
11+
"author": "[email protected]",
12+
"license": "Apache-2.0",
13+
"main": "./build/index.js",
14+
"types": "./build/index.d.ts",
15+
"dependencies": {
16+
"@aws/types": "^0.0.1",
17+
"tslib": "^1.8.0"
18+
},
19+
"devDependencies": {
20+
"@aws/abort-controller": "^0.0.1",
21+
"@types/jest": "^20.0.2",
22+
"@types/node": "^8.0.34",
23+
"jest": "^20.0.4",
24+
"typescript": "^2.3"
25+
}
26+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import {FetchHttpHandler} from './fetch-http-handler';
2+
import {AbortController} from '@aws/abort-controller';
3+
import * as timeouts from './request-timeout';
4+
5+
const mockRequest = jest.fn();
6+
let mockResponse: any;
7+
let timeoutSpy: jest.SpyInstance<any>;
8+
9+
(global as any).Request = mockRequest;
10+
(global as any).Headers = jest.fn();
11+
12+
describe('httpHandler', () => {
13+
beforeEach(() => {
14+
(global as any).AbortController = void 0;
15+
jest.clearAllMocks();
16+
mockResponse = {
17+
headers: {
18+
entries: jest.fn(() => {
19+
return [
20+
['foo', 'bar'],
21+
['bizz', 'bazz']
22+
];
23+
})
24+
},
25+
arrayBuffer: jest.fn(() => Promise.resolve())
26+
}
27+
});
28+
29+
afterEach(() => {
30+
jest.clearAllTimers();
31+
if (timeoutSpy) {
32+
timeoutSpy.mockRestore();
33+
}
34+
});
35+
36+
it('makes requests using fetch', async () => {
37+
const mockResponse = {
38+
headers: {
39+
entries: jest.fn(() => {
40+
return [
41+
['foo', 'bar'],
42+
['bizz', 'bazz']
43+
];
44+
})
45+
},
46+
arrayBuffer: jest.fn(() => Promise.resolve())
47+
}
48+
const mockFetch = jest.fn(() => {
49+
return Promise.resolve(mockResponse);
50+
});
51+
52+
(global as any).fetch = mockFetch;
53+
const fetchHttpHandler = new FetchHttpHandler();
54+
55+
let response = await fetchHttpHandler.handle({} as any, {});
56+
57+
expect(mockFetch.mock.calls.length).toBe(1);
58+
});
59+
60+
it('properly constructs url', async () => {
61+
let mockResponse = {
62+
headers: {
63+
entries: jest.fn(() => {
64+
return [
65+
['foo', 'bar'],
66+
['bizz', 'bazz']
67+
];
68+
})
69+
},
70+
arrayBuffer: jest.fn(() => Promise.resolve())
71+
}
72+
const mockFetch = jest.fn(() => {
73+
return Promise.resolve(mockResponse);
74+
});
75+
76+
(global as any).fetch = mockFetch;
77+
78+
let httpRequest = {
79+
headers: {},
80+
hostname: 'foo.amazonaws.com',
81+
method: 'GET',
82+
path: '/test/?bar=baz',
83+
protocol: 'https:',
84+
port: 443,
85+
};
86+
const fetchHttpHandler = new FetchHttpHandler();
87+
88+
let response = await fetchHttpHandler.handle(httpRequest, {});
89+
90+
expect(mockFetch.mock.calls.length).toBe(1);
91+
let requestCall = mockRequest.mock.calls[0];
92+
expect(requestCall[0]).toBe(
93+
'https://foo.amazonaws.com:443/test/?bar=baz'
94+
);
95+
});
96+
97+
it('prefers response body if it is available', async () => {
98+
let mockResponse = {
99+
headers: {
100+
entries: jest.fn(() => {
101+
return [
102+
['foo', 'bar'],
103+
['bizz', 'bazz']
104+
];
105+
})
106+
},
107+
arrayBuffer: jest.fn(() => Promise.resolve()),
108+
body: 'test'
109+
}
110+
const mockFetch = jest.fn(() => {
111+
return Promise.resolve(mockResponse);
112+
});
113+
114+
(global as any).fetch = mockFetch;
115+
116+
let httpRequest = {
117+
headers: {},
118+
hostname: 'foo.amazonaws.com',
119+
method: 'GET',
120+
path: '/test/?bar=baz',
121+
protocol: 'https:',
122+
port: 443,
123+
};
124+
const fetchHttpHandler = new FetchHttpHandler();
125+
126+
let response = await fetchHttpHandler.handle(httpRequest, {});
127+
128+
expect(mockFetch.mock.calls.length).toBe(1);
129+
expect(mockResponse.arrayBuffer.mock.calls.length).toBe(0);
130+
expect(response.body).toBe('test');
131+
});
132+
133+
it('will not make request if already aborted', async () => {
134+
let mockResponse = {
135+
headers: {
136+
entries: jest.fn(() => {
137+
return [
138+
['foo', 'bar'],
139+
['bizz', 'bazz']
140+
];
141+
})
142+
},
143+
arrayBuffer: jest.fn(() => Promise.resolve()),
144+
body: 'test'
145+
};
146+
const mockFetch = jest.fn(() => {
147+
return Promise.resolve(mockResponse);
148+
});
149+
150+
(global as any).fetch = mockFetch;
151+
const fetchHttpHandler = new FetchHttpHandler();
152+
153+
await expect(fetchHttpHandler.handle({} as any, {
154+
abortSignal: {
155+
aborted: true
156+
}
157+
})).rejects.toHaveProperty('name', 'AbortError');
158+
159+
expect(mockFetch.mock.calls.length).toBe(0);
160+
});
161+
162+
it('will pass abortSignal to fetch if supported', async () => {
163+
let mockResponse = {
164+
headers: {
165+
entries: jest.fn(() => {
166+
return [
167+
['foo', 'bar'],
168+
['bizz', 'bazz']
169+
];
170+
})
171+
},
172+
arrayBuffer: jest.fn(() => Promise.resolve()),
173+
body: 'test'
174+
};
175+
const mockFetch = jest.fn(() => {
176+
return Promise.resolve(mockResponse);
177+
});
178+
(global as any).fetch = mockFetch;
179+
(global as any).AbortController = jest.fn();
180+
const fetchHttpHandler = new FetchHttpHandler();
181+
182+
let response = await fetchHttpHandler.handle({} as any, {
183+
abortSignal: {
184+
aborted: false
185+
}
186+
});
187+
188+
expect(mockRequest.mock.calls[0][1]).toHaveProperty('signal');
189+
expect(mockFetch.mock.calls.length).toBe(1);
190+
});
191+
192+
it('will pass timeout to request timeout', async () => {
193+
let mockResponse = {
194+
headers: {
195+
entries: jest.fn(() => {
196+
return [
197+
['foo', 'bar'],
198+
['bizz', 'bazz']
199+
];
200+
})
201+
},
202+
arrayBuffer: jest.fn(() => Promise.resolve()),
203+
body: 'test'
204+
};
205+
const mockFetch = jest.fn(() => {
206+
return Promise.resolve(mockResponse);
207+
});
208+
(global as any).fetch = mockFetch;
209+
210+
timeoutSpy = jest.spyOn(timeouts, 'requestTimeout');
211+
const fetchHttpHandler = new FetchHttpHandler({
212+
requestTimeout: 500
213+
});
214+
215+
let response = await fetchHttpHandler.handle({} as any, {});
216+
217+
expect(mockFetch.mock.calls.length).toBe(1);
218+
expect(timeoutSpy.mock.calls[0][0]).toBe(500);
219+
});
220+
221+
it('will throw timeout error it timeout finishes before request', async () => {
222+
const mockFetch = jest.fn(() => {
223+
return new Promise((resolve, reject) => {});
224+
});
225+
(global as any).fetch = mockFetch;
226+
const fetchHttpHandler = new FetchHttpHandler({
227+
requestTimeout: 5
228+
});
229+
230+
await expect(fetchHttpHandler.handle(
231+
{} as any,
232+
{})
233+
).rejects.toHaveProperty('name', 'TimeoutError');
234+
expect(mockFetch.mock.calls.length).toBe(1);
235+
});
236+
237+
it('can be aborted before fetch completes', async () => {
238+
const abortController = new AbortController();
239+
240+
const mockFetch = jest.fn(() => {
241+
return new Promise((resolve, reject) => {});
242+
});
243+
(global as any).fetch = mockFetch;
244+
245+
setTimeout(() => {
246+
abortController.abort();
247+
}, 100)
248+
const fetchHttpHandler = new FetchHttpHandler();
249+
250+
await expect(fetchHttpHandler.handle({} as any, {
251+
abortSignal: abortController.signal
252+
})).rejects.toHaveProperty('name', 'AbortError');
253+
254+
// ensure that fetch's built-in mechanism isn't being used
255+
expect(mockRequest.mock.calls[0][1]).not.toHaveProperty('signal');
256+
});
257+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
BrowserHttpOptions,
3+
Handler,
4+
HeaderBag,
5+
HttpHandler,
6+
HttpHandlerOptions,
7+
HttpRequest,
8+
HttpResponse
9+
} from '@aws/types';
10+
11+
import {requestTimeout} from './request-timeout';
12+
13+
declare var AbortController: any;
14+
15+
export class FetchHttpHandler implements HttpHandler<ReadableStream, BrowserHttpOptions> {
16+
constructor(private readonly httpOptions: BrowserHttpOptions = {}){}
17+
18+
handle(
19+
request: HttpRequest<ReadableStream>,
20+
options: HttpHandlerOptions
21+
): Promise<HttpResponse<ReadableStream>> {
22+
const abortSignal = options && options.abortSignal;
23+
const requestTimeoutInMs = this.httpOptions.requestTimeout;
24+
25+
// if the request was already aborted, prevent doing extra work
26+
if (abortSignal && abortSignal.aborted) {
27+
const abortError = new Error('Request aborted');
28+
abortError.name = 'AbortError';
29+
return Promise.reject(abortError);
30+
}
31+
32+
const url = `${request.protocol}//${request.hostname}:${request.port}${request.path}`;
33+
const requestOptions: RequestInit = {
34+
body: request.body,
35+
headers: new Headers(request.headers),
36+
method: request.method,
37+
mode: 'cors'
38+
};
39+
40+
// some browsers support abort signal
41+
if (typeof AbortController !== 'undefined') {
42+
(requestOptions as any)['signal'] = abortSignal;
43+
}
44+
45+
const fetchRequest = new Request(url, requestOptions);
46+
const raceOfPromises = [
47+
fetch(fetchRequest)
48+
.then(response => {
49+
const fetchHeaders: any = response.headers;
50+
const transformedHeaders: HeaderBag = {};
51+
52+
for (let pair of <Array<string[]>>fetchHeaders.entries()) {
53+
transformedHeaders[pair[0]] = pair[1];
54+
}
55+
56+
const httpResponse: HttpResponse<ReadableStream> = {
57+
headers: transformedHeaders,
58+
statusCode: response.status
59+
};
60+
61+
if (response.body) {
62+
httpResponse.body = response.body;
63+
return httpResponse;
64+
} else {
65+
return response.arrayBuffer()
66+
.then(buffer => {
67+
httpResponse.body = new Uint8Array(buffer);
68+
return httpResponse;
69+
});
70+
}
71+
}),
72+
requestTimeout(requestTimeoutInMs),
73+
];
74+
if (abortSignal) {
75+
raceOfPromises.push(
76+
new Promise<never>((resolve, reject) => {
77+
abortSignal.onabort = () => {
78+
const abortError = new Error('Request aborted');
79+
abortError.name = 'AbortError';
80+
reject(abortError);
81+
};
82+
})
83+
);
84+
}
85+
return Promise.race(raceOfPromises);
86+
}
87+
}

0 commit comments

Comments
 (0)