Skip to content

Commit 0917e8c

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/main/types/node-24.5.2
2 parents 4bf12e9 + 227ce00 commit 0917e8c

File tree

14 files changed

+390
-37
lines changed

14 files changed

+390
-37
lines changed

docs/requirements.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ mkdocs-material==9.6.20
33
mkdocs-git-revision-date-plugin==0.3.2
44
mkdocs-exclude==1.0.2
55
mkdocs-typedoc==1.0.4
6-
mkdocs-llmstxt==0.3.1
6+
mkdocs-llmstxt==0.3.2

docs/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ mkdocs-get-deps==0.2.0 \
274274
mkdocs-git-revision-date-plugin==0.3.2 \
275275
--hash=sha256:2e67956cb01823dd2418e2833f3623dee8604cdf223bddd005fe36226a56f6ef
276276
# via -r requirements.in
277-
mkdocs-llmstxt==0.3.1 \
278-
--hash=sha256:123119d9b984c1d1224ed5af250bfbc49879ad83decdaff59d8b0ebb459ddc54 \
279-
--hash=sha256:31f5b6aaae6123c09a2b1c32912c3eb21ccb356b5db7abb867f105e8cc392653
277+
mkdocs-llmstxt==0.3.2 \
278+
--hash=sha256:dd63acb8257fca3244058fd820acd4700c1626dbe48ad3a1a2cc9c599f8e4b7f \
279+
--hash=sha256:fb363205d6f1452411dc5069f62012cb6b29e1788f6db9cc17793bdca7eabea8
280280
# via -r requirements.in
281281
mkdocs-material==9.6.20 \
282282
--hash=sha256:b8d8c8b0444c7c06dd984b55ba456ce731f0035c5a1533cc86793618eb1e6c82 \

packages/event-handler/src/rest/Router.ts

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,33 +226,40 @@ class Router {
226226

227227
const route = this.routeRegistry.resolve(method, path);
228228

229-
if (route === null) {
230-
throw new NotFoundError(`Route ${path} for method ${method} not found`);
231-
}
232-
233-
const handler =
234-
options?.scope != null
235-
? route.handler.bind(options.scope)
236-
: route.handler;
237-
238229
const handlerMiddleware: Middleware = async (params, reqCtx, next) => {
239-
const handlerResult = await handler(params, reqCtx);
240-
reqCtx.res = handlerResultToWebResponse(
241-
handlerResult,
242-
reqCtx.res.headers
243-
);
230+
if (route === null) {
231+
const notFoundRes = await this.handleError(
232+
new NotFoundError(`Route ${path} for method ${method} not found`),
233+
{ ...reqCtx, scope: options?.scope }
234+
);
235+
reqCtx.res = handlerResultToWebResponse(
236+
notFoundRes,
237+
reqCtx.res.headers
238+
);
239+
} else {
240+
const handler =
241+
options?.scope != null
242+
? route.handler.bind(options.scope)
243+
: route.handler;
244+
245+
const handlerResult = await handler(params, reqCtx);
246+
reqCtx.res = handlerResultToWebResponse(
247+
handlerResult,
248+
reqCtx.res.headers
249+
);
250+
}
244251

245252
await next();
246253
};
247254

248255
const middleware = composeMiddleware([
249256
...this.middleware,
250-
...route.middleware,
257+
...(route?.middleware ?? []),
251258
handlerMiddleware,
252259
]);
253260

254261
const middlewareResult = await middleware(
255-
route.params,
262+
route?.params ?? {},
256263
requestContext,
257264
() => Promise.resolve()
258265
);

packages/event-handler/src/rest/constants.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ export const SAFE_CHARS = "-._~()'!*:@,;=+&$";
8888

8989
export const UNSAFE_CHARS = '%<> \\[\\]{}|^';
9090

91+
/**
92+
* Default CORS configuration
93+
*/
94+
export const DEFAULT_CORS_OPTIONS = {
95+
origin: '*',
96+
allowMethods: ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT'],
97+
allowHeaders: [
98+
'Authorization',
99+
'Content-Type',
100+
'X-Amz-Date',
101+
'X-Api-Key',
102+
'X-Amz-Security-Token',
103+
],
104+
exposeHeaders: [],
105+
credentials: false
106+
};
107+
91108
export const DEFAULT_COMPRESSION_RESPONSE_THRESHOLD = 1024;
92109

93110
export const CACHE_CONTROL_NO_TRANSFORM_REGEX =

packages/event-handler/src/rest/converters.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,14 @@ export const handlerResultToWebResponse = (
140140
resHeaders?: Headers
141141
): Response => {
142142
if (response instanceof Response) {
143-
return response;
143+
const headers = new Headers(resHeaders);
144+
for (const [key, value] of response.headers.entries()) {
145+
headers.set(key, value);
146+
}
147+
return new Response(response.body, {
148+
status: response.status,
149+
headers,
150+
});
144151
}
145152

146153
const headers = new Headers(resHeaders);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import type {
2+
CorsOptions,
3+
Middleware,
4+
} from '../../types/rest.js';
5+
import {
6+
DEFAULT_CORS_OPTIONS,
7+
HttpErrorCodes,
8+
HttpVerbs,
9+
} from '../constants.js';
10+
11+
/**
12+
* Resolves the origin value based on the configuration
13+
*/
14+
const resolveOrigin = (
15+
originConfig: NonNullable<CorsOptions['origin']>,
16+
requestOrigin: string | null,
17+
): string => {
18+
if (Array.isArray(originConfig)) {
19+
return requestOrigin && originConfig.includes(requestOrigin) ? requestOrigin : '';
20+
}
21+
return originConfig;
22+
};
23+
24+
/**
25+
* Creates a CORS middleware that adds appropriate CORS headers to responses
26+
* and handles OPTIONS preflight requests.
27+
*
28+
* @example
29+
* ```typescript
30+
* import { Router } from '@aws-lambda-powertools/event-handler/experimental-rest';
31+
* import { cors } from '@aws-lambda-powertools/event-handler/experimental-rest/middleware';
32+
*
33+
* const app = new Router();
34+
*
35+
* // Use default configuration
36+
* app.use(cors());
37+
*
38+
* // Custom configuration
39+
* app.use(cors({
40+
* origin: 'https://example.com',
41+
* allowMethods: ['GET', 'POST'],
42+
* credentials: true,
43+
* }));
44+
*
45+
* // Dynamic origin with function
46+
* app.use(cors({
47+
* origin: (origin, reqCtx) => {
48+
* const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
49+
* return origin && allowedOrigins.includes(origin);
50+
* }
51+
* }));
52+
* ```
53+
*
54+
* @param options.origin - The origin to allow requests from
55+
* @param options.allowMethods - The HTTP methods to allow
56+
* @param options.allowHeaders - The headers to allow
57+
* @param options.exposeHeaders - The headers to expose
58+
* @param options.credentials - Whether to allow credentials
59+
* @param options.maxAge - The maximum age for the preflight response
60+
*/
61+
export const cors = (options?: CorsOptions): Middleware => {
62+
const config = {
63+
...DEFAULT_CORS_OPTIONS,
64+
...options
65+
};
66+
67+
return async (_params, reqCtx, next) => {
68+
const requestOrigin = reqCtx.request.headers.get('Origin');
69+
const resolvedOrigin = resolveOrigin(config.origin, requestOrigin);
70+
71+
reqCtx.res.headers.set('access-control-allow-origin', resolvedOrigin);
72+
if (resolvedOrigin !== '*') {
73+
reqCtx.res.headers.set('Vary', 'Origin');
74+
}
75+
config.allowMethods.forEach(method => {
76+
reqCtx.res.headers.append('access-control-allow-methods', method);
77+
});
78+
config.allowHeaders.forEach(header => {
79+
reqCtx.res.headers.append('access-control-allow-headers', header);
80+
});
81+
config.exposeHeaders.forEach(header => {
82+
reqCtx.res.headers.append('access-control-expose-headers', header);
83+
});
84+
reqCtx.res.headers.set('access-control-allow-credentials', config.credentials.toString());
85+
if (config.maxAge !== undefined) {
86+
reqCtx.res.headers.set('access-control-max-age', config.maxAge.toString());
87+
}
88+
89+
// Handle preflight OPTIONS request
90+
if (reqCtx.request.method === HttpVerbs.OPTIONS && reqCtx.request.headers.has('Access-Control-Request-Method')) {
91+
return new Response(null, {
92+
status: HttpErrorCodes.NO_CONTENT,
93+
headers: reqCtx.res.headers,
94+
});
95+
}
96+
await next();
97+
};
98+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { compress } from './compress.js';
2+
export { cors } from './cors.js';

packages/event-handler/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type {
3232
} from './common.js';
3333

3434
export type {
35+
CorsOptions,
3536
ErrorHandler,
3637
ErrorResolveOptions,
3738
ErrorResponse,

packages/event-handler/src/types/rest.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,56 @@ type ValidationResult = {
111111
issues: string[];
112112
};
113113

114+
/**
115+
* Configuration options for CORS middleware
116+
*/
117+
type CorsOptions = {
118+
/**
119+
* The Access-Control-Allow-Origin header value.
120+
* Can be a string, array of strings.
121+
* @default '*'
122+
*/
123+
origin?: string | string[];
124+
125+
/**
126+
* The Access-Control-Allow-Methods header value.
127+
* @default ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']
128+
*/
129+
allowMethods?: string[];
130+
131+
/**
132+
* The Access-Control-Allow-Headers header value.
133+
* @default ['Authorization', 'Content-Type', 'X-Amz-Date', 'X-Api-Key', 'X-Amz-Security-Token']
134+
*/
135+
allowHeaders?: string[];
136+
137+
/**
138+
* The Access-Control-Expose-Headers header value.
139+
* @default []
140+
*/
141+
exposeHeaders?: string[];
142+
143+
/**
144+
* The Access-Control-Allow-Credentials header value.
145+
* @default false
146+
*/
147+
credentials?: boolean;
148+
149+
/**
150+
* The Access-Control-Max-Age header value in seconds.
151+
* Only applicable for preflight requests.
152+
*/
153+
maxAge?: number;
154+
};
155+
114156
type CompressionOptions = {
115157
encoding?: 'gzip' | 'deflate';
116158
threshold?: number;
117159
};
118160

119161
export type {
120162
CompiledRoute,
163+
CorsOptions,
121164
DynamicRoute,
122165
ErrorResponse,
123166
ErrorConstructor,

packages/event-handler/tests/unit/rest/Router/decorators.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,29 +256,32 @@ describe('Class: Router - Decorators', () => {
256256
});
257257
});
258258

259-
it('works with notFound decorator', async () => {
259+
it('works with notFound decorator and preserves scope', async () => {
260260
// Prepare
261261
const app = new Router();
262262

263263
class Lambda {
264+
public scope = 'scoped';
265+
264266
@app.notFound()
265267
public async handleNotFound(error: NotFoundError) {
266268
return {
267269
statusCode: HttpErrorCodes.NOT_FOUND,
268270
error: 'Not Found',
269-
message: `Decorated: ${error.message}`,
271+
message: `${this.scope}: ${error.message}`,
270272
};
271273
}
272274

273275
public async handler(event: unknown, _context: Context) {
274-
return app.resolve(event, _context);
276+
return app.resolve(event, _context, { scope: this });
275277
}
276278
}
277279

278280
const lambda = new Lambda();
281+
const handler = lambda.handler.bind(lambda);
279282

280283
// Act
281-
const result = await lambda.handler(
284+
const result = await handler(
282285
createTestEvent('/nonexistent', 'GET'),
283286
context
284287
);
@@ -289,7 +292,7 @@ describe('Class: Router - Decorators', () => {
289292
body: JSON.stringify({
290293
statusCode: HttpErrorCodes.NOT_FOUND,
291294
error: 'Not Found',
292-
message: 'Decorated: Route /nonexistent for method GET not found',
295+
message: 'scoped: Route /nonexistent for method GET not found',
293296
}),
294297
headers: { 'content-type': 'application/json' },
295298
isBase64Encoded: false,

0 commit comments

Comments
 (0)