-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmiddleware.ts
164 lines (154 loc) · 6.05 KB
/
middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createHash } from 'crypto';
import { encodeSafeJSON, escapeHtml } from './html';
import {
acceptsContentType,
ApiResponse,
BadRequest,
HttpRequest,
HttpResponse,
HttpStatus,
isReadHttpMethod,
isResponse,
isWriteHttpMethod,
normalizeHeaders,
} from './http';
import { countBytes, findAllMatches } from './strings';
type Response = HttpResponse | ApiResponse;
const compatibilityMiddleware = requestMiddleware(async (request: HttpRequest) => ({
...request,
// Convert headers to capitalized format, e.g. `content-type` => `Content-Type`
headers: normalizeHeaders(request.headers),
}));
const queryMethodSupportMiddleware = requestMiddleware(async (request: HttpRequest) => {
const httpMethod = request.method;
const { method, ...queryParameters } = request.queryParameters;
if (!method) {
return request;
}
// Allow changing the HTTP method with 'method' query string parameter
if ((httpMethod === 'GET' && isReadHttpMethod(method)) || (httpMethod === 'POST' && isWriteHttpMethod(method))) {
return { ...request, method, queryParameters };
}
throw new BadRequest(`Cannot perform ${httpMethod} as ${method} request`);
});
const apiMiddleware = responseMiddleware(async (response: Response, request: HttpRequest): Promise<HttpResponse> => {
if ('body' in response) {
// Already a response with encoded body
return response;
}
const { statusCode, headers, data } = response;
// If requesting a HTML page, then render as a HTML page
if (acceptsContentType(request, 'text/html')) {
// TODO: Improved page!
const statusCodeHtml = escapeHtml(String(statusCode));
const jsonHtml = (data && encodeSafeJSON(data, null, 4)) || '';
return {
...response,
headers: {
...headers,
'Content-Type': 'text/html; charset=utf-8',
},
body: `<div>${statusCodeHtml}</div><pre>${jsonHtml}</pre>`,
};
}
// Convert to JSON
return {
...response,
body: data == null ? '' : JSON.stringify(data),
headers: {
...headers,
'Content-Type': 'application/json',
},
};
});
const finalizerMiddleware = responseMiddleware(async (response: HttpResponse, request: HttpRequest) => {
const { statusCode, body, headers } = response;
const hash = createHash('md5').update(body).digest('hex');
return {
statusCode,
body,
headers: {
// Add the CORS headers
'Access-Control-Allow-Origin': request.serverOrigin,
'Access-Control-Allow-Headers':
'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,X-Requested-With',
'Access-Control-Allow-Credentials': 'true',
// Calculate the length for the response body
'Content-Length': String(countBytes(body)),
// Return the ETag
'ETag': `"${hash}"`,
...headers,
},
};
});
function preconditionMiddleware<P extends any[]>(
handler: (request: HttpRequest, ...params: P) => Promise<HttpResponse>,
): (request: HttpRequest, ...params: P) => Promise<HttpResponse> {
async function handlePrecondition(request: HttpRequest, ...params: P): Promise<HttpResponse> {
const { method } = request;
const response = await handler(request, ...params);
if (method !== 'GET' && method !== 'HEAD') {
return response;
}
const etags = response.headers.ETag;
const etag = Array.isArray(etags) ? etags[0] : etags;
const ifNoneMatch = request.headers['If-None-Match'];
if (response.statusCode !== HttpStatus.OK || !etag || !ifNoneMatch) {
return response;
}
const requiredTags = findAllMatches(ifNoneMatch, /"[^"]*"/g);
if (requiredTags.indexOf(etag) < 0) {
return response;
}
// Respond with 304 and without the body
return {
...response,
statusCode: HttpStatus.NotModified,
body: '',
};
}
return handlePrecondition;
}
export function middleware<P extends any[]>(
handler: (request: HttpRequest, ...params: P) => Promise<Response>,
): (request: HttpRequest, ...params: P) => Promise<HttpResponse> {
return compatibilityMiddleware(
preconditionMiddleware(
finalizerMiddleware(apiMiddleware(errorMiddleware(queryMethodSupportMiddleware(handler)))),
),
);
}
export function requestMiddleware<I, O>(handleRequest: (request: I) => Promise<O>) {
return <P extends any[], R>(handler: (request: O, ...params: P) => Promise<R>) =>
async (request: I, ...params: P): Promise<R> => {
const newRequest = await handleRequest(request);
return handler(newRequest, ...params);
};
}
export function responseMiddleware<I, O, R>(handleResponse: (response: I, request: R) => Promise<O>) {
return <P extends any[]>(handler: (request: R, ...params: P) => Promise<I>) =>
async (request: R, ...params: P): Promise<O> => {
const response = await handler(request, ...params);
return handleResponse(response, request);
};
}
export function errorMiddleware<R, P extends any[]>(
handler: (request: R, ...params: P) => Promise<Response>,
): (request: R, ...params: P) => Promise<Response> {
async function catchError(request: R, ...params: P): Promise<Response> {
try {
return await handler(request, ...params);
} catch (error) {
// Determine if the error was a HTTP response
if (isResponse(error)) {
// This was an intentional HTTP error, so it should be considered
// a successful execution of the lambda function.
return error;
}
// This doesn't seem like a HTTP response -> Pass through for the internal server error
throw error;
}
}
return catchError;
}