diff --git a/docs/architecture/request-limits.md b/docs/architecture/request-limits.md new file mode 100644 index 0000000..a0354d9 --- /dev/null +++ b/docs/architecture/request-limits.md @@ -0,0 +1,46 @@ +# Request Body Size Limits + +The API uses configurable request body size limits to prevent abuse (like large payload attacks) while still allowing necessary bulk operations where needed. Rather than applying a single global body size limit to all routes, the application uses route-specific limits grouped by functional area. + +## Configuration Defaults and Overrides + +Limits are configured via environment variables. The following variables control the maximum payload size for JSON and URL-encoded requests: + +| Environment Variable | Default Limit | Target Route Group | +| ------------------------ | ------------- | ---------------------------------------------------------------- | +| `MAX_BODY_SIZE_DEFAULT` | `1mb` | Used for standard routes (`/auth`, `/health`, `/activity`, etc.) | +| `MAX_BODY_SIZE_CREATORS` | `5mb` | Used for creator-specific routes. | +| `MAX_BODY_SIZE_ADMIN` | `10mb` | Used for administrative routes (e.g. bulk data uploads). | + +### Overriding Limits + +To override a limit, simply provide the appropriate environment variable in your `.env` file or environment configuration. + +```env +# Example overrides in .env +MAX_BODY_SIZE_DEFAULT=2mb +MAX_BODY_SIZE_ADMIN=50mb +MAX_BODY_SIZE_CREATORS=10mb +``` + +Valid values include sizes with unit suffixes (e.g., `100kb`, `1mb`, `5mb`) which are parsed by the `bytes` library used internally by Express `body-parser`. + +## Adding Custom Limits for New Route Groups + +To introduce a distinct size limit for a new set of routes: + +1. **Define Configuration:** Add a new configuration property in `src/config.ts` under the `envSchema` (e.g., `MAX_BODY_SIZE_CUSTOM`). +2. **Create Middleware:** In `src/middlewares/body-parser.middleware.ts`, export a new parser using the helper function `createBodyParser(envConfig.MAX_BODY_SIZE_CUSTOM)`. +3. **Apply to Router:** In `src/modules/index.ts`, apply the new exported middleware to the relevant `router.use()` definition. + +## Error Handling + +When a client sends a payload that exceeds the configured limit for the route group, the server intercepts the process early and fails fast without fully consuming the body. It will return a standard API error response with HTTP `413 Payload Too Large`: + +```json +{ + "success": false, + "code": "PAYLOAD_TOO_LARGE", + "message": "Request body exceeds configured size limit" +} +``` diff --git a/src/app.ts b/src/app.ts index 7f868ae..f25a767 100644 --- a/src/app.ts +++ b/src/app.ts @@ -28,14 +28,11 @@ app.use(schemaVersionMiddleware); app.use(requestIdMiddleware); app.use(corsMiddleware()); app.use(helmet()); -app.use(express.json({ limit: '10mb' })); - if (!envConfig.ENABLE_REQUEST_LOGGING) { app.use(morgan('combined')); } app.use(requestLoggerMiddleware); -app.use(express.urlencoded({ extended: true })); app.use(appRateLimit); // Health check endpoints are now in /api/v1/health diff --git a/src/config.ts b/src/config.ts index 543f3e1..b32a395 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,9 +3,43 @@ import { envSchema } from './config.schema'; export { envSchema }; -// Load environment variables from .env file -// Note: Does not override existing environment variables -dotenv.config(); + // URLs + BACKEND_URL: z.string().url(), + FRONTEND_URL: z + .string() + .url('FRONTEND_URL must be a valid URL') + .min(1, 'FRONTEND_URL is required'), + + // Cloudinary + CLOUDINARY_CLOUD_NAME: z + .string() + .min(1, 'CLOUDINARY_CLOUD_NAME is required for image uploads'), + CLOUDINARY_API_KEY: z + .string() + .min(1, 'CLOUDINARY_API_KEY is required for image uploads'), + CLOUDINARY_API_SECRET: z + .string() + .min(1, 'CLOUDINARY_API_SECRET is required for image uploads'), + + PAYSTACK_SECRET_KEY: z + .string() + .min(1, 'PAYSTACK_SECRET_KEY is required for payment processing'), + PAYSTACK_PUBLIC_KEY: z + .string() + .min(1, 'PAYSTACK_PUBLIC_KEY is required for payment processing') + .optional(), + ENABLE_RESPONSE_TIMING: z.coerce.boolean().default(true), + API_VERSION: z.string().default('1.0.0'), + ENABLE_API_VERSION_HEADER: z.coerce.boolean().default(true), + ENABLE_SCHEMA_VERSION_HEADER: z.coerce.boolean().default(true), + ENABLE_REQUEST_LOGGING: z.coerce.boolean().default(true), + INDEXER_JITTER_FACTOR: z.coerce.number().min(0).max(1).default(0.1), + + // Body size limits + MAX_BODY_SIZE_DEFAULT: z.string().default('1mb'), + MAX_BODY_SIZE_ADMIN: z.string().default('10mb'), + MAX_BODY_SIZE_CREATORS: z.string().default('5mb'), +}); /** * Validated and typed environment configuration. diff --git a/src/constants/error.constants.ts b/src/constants/error.constants.ts index c546ac2..3ffd5f4 100644 --- a/src/constants/error.constants.ts +++ b/src/constants/error.constants.ts @@ -13,6 +13,7 @@ export const ErrorCode = { RATE_LIMIT: 'RATE_LIMIT', PRISMA_ERROR: 'DATABASE_ERROR', JWT_ERROR: 'TOKEN_ERROR', + PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE', } as const; export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]; diff --git a/src/middlewares/body-parser.middleware.ts b/src/middlewares/body-parser.middleware.ts new file mode 100644 index 0000000..62bfc20 --- /dev/null +++ b/src/middlewares/body-parser.middleware.ts @@ -0,0 +1,29 @@ +import express from 'express'; +import { envConfig } from '../config'; + +/** + * Creates JSON and URL-encoded body parsers with a specific size limit. + * + * @param limit The maximum body size (e.g., '1mb', '10mb') + */ +export const createBodyParser = (limit: string) => { + return [ + express.json({ limit }), + express.urlencoded({ extended: true, limit }) + ]; +}; + +/** + * Default body parser limit used by most routes. + */ +export const defaultBodyParser = createBodyParser(envConfig.MAX_BODY_SIZE_DEFAULT); + +/** + * Admin route body parser with higher limits for bulk operations. + */ +export const adminBodyParser = createBodyParser(envConfig.MAX_BODY_SIZE_ADMIN); + +/** + * Creators route body parser with specific limits for creator operations. + */ +export const creatorsBodyParser = createBodyParser(envConfig.MAX_BODY_SIZE_CREATORS); diff --git a/src/middlewares/error.middleware.ts b/src/middlewares/error.middleware.ts index 3b436be..47d4e9e 100644 --- a/src/middlewares/error.middleware.ts +++ b/src/middlewares/error.middleware.ts @@ -152,6 +152,16 @@ export const errorHandler: ErrorRequestHandler = ( return; } + // Handle request body too large + if (err.type === 'entity.too.large') { + res.status(413).json({ + success: false, + code: ErrorCode.PAYLOAD_TOO_LARGE, + message: 'Request body exceeds configured size limit', + }); + return; + } + // Log request details for debugging const chalkColor = { error: chalk.red, diff --git a/src/modules/index.ts b/src/modules/index.ts index e543c36..8d368e9 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -10,16 +10,17 @@ import activityRouter from './activity/activity.routes'; import ownershipRouter from './ownership/ownership.routes'; import { BASE as CREATORS_BASE } from '../constants/creator.constants'; +import { defaultBodyParser, adminBodyParser, creatorsBodyParser } from '../middlewares/body-parser.middleware'; + const router = Router(); -router.use('/health', healthRouter); -router.use('/auth', authRouter); -router.use('/config', configRouter); -router.use(CREATORS_BASE, creatorsRouter); -router.use('/metrics', metricsRouter); -router.use('/ledger', ledgerRouter); -router.use('/admin', adminRouter); -router.use('/activity', activityRouter); -router.use('/ownership', ownershipRouter); +router.use('/health', defaultBodyParser, healthRouter); +router.use('/auth', defaultBodyParser, authRouter); +router.use('/config', defaultBodyParser, configRouter); +router.use(CREATORS_BASE, creatorsBodyParser, creatorsRouter); +router.use('/metrics', defaultBodyParser, metricsRouter); +router.use('/admin', adminBodyParser, adminRouter); +router.use('/activity', defaultBodyParser, activityRouter); +router.use('/ownership', defaultBodyParser, ownershipRouter); export default router;