cdk-serverless-agentic-api
Version:
CDK construct for serverless web applications with CloudFront, S3, Cognito, API Gateway, and Lambda
329 lines • 12.3 kB
JavaScript
;
/**
* Error handling utilities for the serverless web app construct
* Provides structured error responses and error page configuration
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateLimitError = exports.ConflictError = exports.NotFoundError = exports.AuthorizationError = exports.AuthenticationError = exports.ValidationError = exports.CORS_HEADERS = exports.HttpStatusCode = void 0;
exports.createErrorResponse = createErrorResponse;
exports.createSuccessResponse = createSuccessResponse;
exports.withErrorHandling = withErrorHandling;
exports.validateCognitoClaims = validateCognitoClaims;
exports.validateRequestBody = validateRequestBody;
exports.createErrorPages = createErrorPages;
const aws_cdk_lib_1 = require("aws-cdk-lib");
const s3deploy = __importStar(require("aws-cdk-lib/aws-s3-deployment"));
const constructs_1 = require("constructs");
const path = __importStar(require("path"));
/**
* HTTP status codes for common error scenarios
*/
var HttpStatusCode;
(function (HttpStatusCode) {
HttpStatusCode[HttpStatusCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
HttpStatusCode[HttpStatusCode["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
HttpStatusCode[HttpStatusCode["FORBIDDEN"] = 403] = "FORBIDDEN";
HttpStatusCode[HttpStatusCode["NOT_FOUND"] = 404] = "NOT_FOUND";
HttpStatusCode[HttpStatusCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
HttpStatusCode[HttpStatusCode["CONFLICT"] = 409] = "CONFLICT";
HttpStatusCode[HttpStatusCode["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
HttpStatusCode[HttpStatusCode["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
HttpStatusCode[HttpStatusCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
HttpStatusCode[HttpStatusCode["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
HttpStatusCode[HttpStatusCode["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
HttpStatusCode[HttpStatusCode["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
})(HttpStatusCode || (exports.HttpStatusCode = HttpStatusCode = {}));
/**
* Standard CORS headers for error responses
*/
exports.CORS_HEADERS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Requested-With',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400'
};
/**
* Creates a standardized error response for Lambda functions
*/
function createErrorResponse(statusCode, error, message, requestId, details) {
const errorResponse = {
error,
message,
timestamp: new Date().toISOString(),
requestId,
details
};
// Remove undefined values
Object.keys(errorResponse).forEach(key => {
if (errorResponse[key] === undefined) {
delete errorResponse[key];
}
});
return {
statusCode,
headers: {
'Content-Type': 'application/json',
...exports.CORS_HEADERS
},
body: JSON.stringify(errorResponse)
};
}
/**
* Creates a standardized success response for Lambda functions
*/
function createSuccessResponse(data, statusCode = 200, additionalHeaders) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
...exports.CORS_HEADERS,
...additionalHeaders
},
body: JSON.stringify({
...data,
timestamp: new Date().toISOString()
})
};
}
/**
* Error handler wrapper for Lambda functions
*/
function withErrorHandling(handler) {
return async (event, context) => {
try {
console.log('Request received:', JSON.stringify(event, null, 2));
const result = await handler(event, context);
console.log('Response:', JSON.stringify(result, null, 2));
return result;
}
catch (error) {
console.error('Unhandled error:', error);
// Extract request ID from context if available
const requestId = context?.awsRequestId;
// Determine error type and status code
let statusCode = HttpStatusCode.INTERNAL_SERVER_ERROR;
let errorType = 'Internal Server Error';
let errorMessage = 'An unexpected error occurred while processing the request';
if (error instanceof ValidationError) {
statusCode = HttpStatusCode.BAD_REQUEST;
errorType = 'Validation Error';
errorMessage = error.message;
}
else if (error instanceof AuthenticationError) {
statusCode = HttpStatusCode.UNAUTHORIZED;
errorType = 'Authentication Error';
errorMessage = error.message;
}
else if (error instanceof AuthorizationError) {
statusCode = HttpStatusCode.FORBIDDEN;
errorType = 'Authorization Error';
errorMessage = error.message;
}
else if (error instanceof NotFoundError) {
statusCode = HttpStatusCode.NOT_FOUND;
errorType = 'Not Found';
errorMessage = error.message;
}
else if (error instanceof ConflictError) {
statusCode = HttpStatusCode.CONFLICT;
errorType = 'Conflict';
errorMessage = error.message;
}
else if (error instanceof RateLimitError) {
statusCode = HttpStatusCode.TOO_MANY_REQUESTS;
errorType = 'Rate Limit Exceeded';
errorMessage = error.message;
}
return createErrorResponse(statusCode, errorType, errorMessage, requestId, process.env.NODE_ENV === 'development' ? {
stack: error instanceof Error ? error.stack : undefined,
originalError: error instanceof Error ? error.message : String(error)
} : undefined);
}
};
}
/**
* Custom error classes for different error scenarios
*/
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.field = field;
this.name = 'ValidationError';
}
}
exports.ValidationError = ValidationError;
class AuthenticationError extends Error {
constructor(message = 'Authentication required') {
super(message);
this.name = 'AuthenticationError';
}
}
exports.AuthenticationError = AuthenticationError;
class AuthorizationError extends Error {
constructor(message = 'Insufficient permissions') {
super(message);
this.name = 'AuthorizationError';
}
}
exports.AuthorizationError = AuthorizationError;
class NotFoundError extends Error {
constructor(message = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
}
}
exports.NotFoundError = NotFoundError;
class ConflictError extends Error {
constructor(message = 'Resource conflict') {
super(message);
this.name = 'ConflictError';
}
}
exports.ConflictError = ConflictError;
class RateLimitError extends Error {
constructor(message = 'Rate limit exceeded') {
super(message);
this.name = 'RateLimitError';
}
}
exports.RateLimitError = RateLimitError;
/**
* Validates Cognito claims and throws appropriate errors
*/
function validateCognitoClaims(event, requiredGroup) {
const claims = event.requestContext?.authorizer?.claims;
if (!claims) {
throw new AuthenticationError('No authentication claims found');
}
if (requiredGroup) {
const userGroups = claims['cognito:groups'] ? claims['cognito:groups'].split(',') : [];
if (!userGroups.includes(requiredGroup)) {
throw new AuthorizationError(`Access denied. Required group: ${requiredGroup}`);
}
}
return claims;
}
/**
* Validates request body and throws validation errors
*/
function validateRequestBody(event, requiredFields = []) {
if (!event.body) {
if (requiredFields.length > 0) {
throw new ValidationError('Request body is required');
}
return {};
}
let body;
try {
body = JSON.parse(event.body);
}
catch (error) {
throw new ValidationError('Invalid JSON in request body');
}
// Check required fields
for (const field of requiredFields) {
if (body[field] === undefined || body[field] === null || body[field] === '') {
throw new ValidationError(`Missing required field: ${field}`, field);
}
}
return body;
}
/**
* Creates custom error pages for CloudFront distribution
*/
function createErrorPages(scope, bucket, constructId) {
// Get custom error pages path if provided
const errorPagesPath = scope instanceof constructs_1.Construct &&
scope.node.tryGetContext('props')?.errorPagesPath ||
path.join(__dirname, '../error-pages');
// Deploy error page assets to S3
new s3deploy.BucketDeployment(scope, `${constructId}ErrorPages`, {
sources: [s3deploy.Source.asset(errorPagesPath)],
destinationBucket: bucket,
destinationKeyPrefix: 'error-pages/',
cacheControl: [
s3deploy.CacheControl.setPublic(),
s3deploy.CacheControl.maxAge(aws_cdk_lib_1.Duration.hours(1))
],
});
// Define custom error responses
return [
{
httpStatus: 400,
responseHttpStatus: 400,
responsePagePath: '/error-pages/400.html',
ttl: aws_cdk_lib_1.Duration.minutes(5)
},
{
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/error-pages/403.html',
ttl: aws_cdk_lib_1.Duration.minutes(5)
},
{
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/error-pages/404.html',
ttl: aws_cdk_lib_1.Duration.hours(1)
},
{
httpStatus: 500,
responseHttpStatus: 500,
responsePagePath: '/error-pages/500.html',
ttl: aws_cdk_lib_1.Duration.minutes(1)
},
{
httpStatus: 502,
responseHttpStatus: 502,
responsePagePath: '/error-pages/502.html',
ttl: aws_cdk_lib_1.Duration.minutes(1)
},
{
httpStatus: 503,
responseHttpStatus: 503,
responsePagePath: '/error-pages/503.html',
ttl: aws_cdk_lib_1.Duration.minutes(1)
},
{
httpStatus: 504,
responseHttpStatus: 504,
responsePagePath: '/error-pages/504.html',
ttl: aws_cdk_lib_1.Duration.minutes(1)
}
];
}
//# sourceMappingURL=error-handling.js.map