Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';
import express, { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
Expand Down Expand Up @@ -48,6 +46,8 @@ import { messageQueue } from './services/queue.js';
import { registerDefaultProcessors } from './services/queue-producers.js';
import { slaTrackingMiddleware } from './middleware/slaTracking.js';
import { requestIdMiddleware, REQUEST_ID_HEADER } from './middleware/requestId.js';
import { traceMiddleware } from './middleware/trace.js';
import { cacheControlNoStore } from './middleware/cache-control.js';
import { validateEnv, config as getConfig } from './config/env.js';
import { flagsRouter } from './routes/flags.js';
import { rateLimitAnalyticsRouter } from './routes/rate-limit-analytics.js';
Expand Down Expand Up @@ -118,7 +118,7 @@ if (env.IP_ALLOWLIST_ENABLED || env.IP_ALLOWLIST) {
console.log(`[IP Allowlist] Enabled with ${allowedIps.length} IP(s)`);
}

const traceStorage = new AsyncLocalStorage<string>();
import { traceStorage } from './middleware/trace.js';

const originalConsole = {
log: console.log,
Expand Down Expand Up @@ -197,32 +197,10 @@ app.use(
);

app.use(requestIdMiddleware);

app.use((req: Request, res: Response, next: NextFunction) => {
const traceId = (req.headers['x-trace-id'] as string) || randomUUID();
res.setHeader('X-Trace-Id', traceId);

traceStorage.run(traceId, () => {
console.log(`${req.method} ${req.url} [RequestID: ${req.requestId}] - Started`);

res.on('finish', () => {
console.log(`${req.method} ${req.url} [RequestID: ${req.requestId}] - Finished with status ${res.statusCode}`);
});

next();
});
});

app.use(traceMiddleware);
app.use(slaTrackingMiddleware);
app.use(sessionMiddleware);

app.use((req: Request, res: Response, next: NextFunction) => {
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.setHeader('Cache-Control', 'no-store');
}
res.setHeader('Vary', 'Accept-Encoding');
next();
});
app.use(cacheControlNoStore);

app.use(healthRouter);

Expand Down
99 changes: 99 additions & 0 deletions backend/src/middleware/__tests__/cache-control.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect, vi } from 'vitest';
import type { Request, Response } from 'express';
import { cacheControlNoStore, CACHE_NOSTORE_HEADER, VARY_HEADER } from '../cache-control.js';

function makeReq(overrides: Partial<Request> = {}): Request {
return { method: 'GET', ...overrides } as unknown as Request;
}

function makeRes(): { res: Response; headers: Record<string, string> } {
const headers: Record<string, string> = {};
const res = {
setHeader: vi.fn((name: string, value: string) => { headers[name] = value; }),
} as unknown as Response;
return { res, headers };
}

describe('cacheControlNoStore', () => {
it('sets Cache-Control: no-store for POST requests', () => {
const req = makeReq({ method: 'POST' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBe('no-store');
expect(next).toHaveBeenCalledOnce();
});

it('sets Cache-Control: no-store for PUT requests', () => {
const req = makeReq({ method: 'PUT' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBe('no-store');
});

it('sets Cache-Control: no-store for DELETE requests', () => {
const req = makeReq({ method: 'DELETE' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBe('no-store');
});

it('does NOT set Cache-Control: no-store for GET requests', () => {
const req = makeReq({ method: 'GET' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBeUndefined();
});

it('does NOT set Cache-Control: no-store for HEAD requests', () => {
const req = makeReq({ method: 'HEAD' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBeUndefined();
});

it('always sets Vary: Accept-Encoding header', () => {
const req = makeReq({ method: 'GET' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[VARY_HEADER]).toBe('Accept-Encoding');
});

it('sets both headers for mutations', () => {
const req = makeReq({ method: 'PATCH' });
const { res, headers } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(headers[CACHE_NOSTORE_HEADER]).toBe('no-store');
expect(headers[VARY_HEADER]).toBe('Accept-Encoding');
});

it('calls next()', () => {
const req = makeReq();
const { res } = makeRes();
const next = vi.fn();

cacheControlNoStore(req, res, next);

expect(next).toHaveBeenCalledOnce();
});
});
139 changes: 139 additions & 0 deletions backend/src/middleware/__tests__/compose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { describe, it, expect, vi } from 'vitest';
import type { Request, Response } from 'express';
import { composeMiddleware, createMiddlewareChain } from '../compose.js';

function makeReq(overrides: Partial<Request> = {}): Request {
return { method: 'GET', headers: {}, ...overrides } as unknown as Request;
}

function makeRes(): Response {
return { status: vi.fn().mockReturnThis(), json: vi.fn(), setHeader: vi.fn() } as unknown as Response;
}

describe('composeMiddleware', () => {
it('executes middleware in order', () => {
const order: number[] = [];
const mw1 = (_req: Request, _res: Response, next: any) => { order.push(1); next(); };
const mw2 = (_req: Request, _res: Response, next: any) => { order.push(2); next(); };
const mw3 = (_req: Request, _res: Response, next: any) => { order.push(3); next(); };

const composed = composeMiddleware(mw1, mw2, mw3);
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

expect(order).toEqual([1, 2, 3]);
expect(next).toHaveBeenCalledOnce();
});

it('handles async middleware', async () => {
const order: number[] = [];
const mw1 = async (_req: Request, _res: Response, next: any) => { order.push(1); next(); };
const mw2 = async (_req: Request, _res: Response, next: any) => { order.push(2); next(); };

const composed = composeMiddleware(mw1, mw2);
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

await new Promise(process.nextTick);
expect(order).toEqual([1, 2]);
});

it('passes errors to the outer next() callback', () => {
const testError = new Error('middleware error');
const mw1 = (_req: Request, _res: Response, next: any) => { next(testError); };

const composed = composeMiddleware(mw1);
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

expect(next).toHaveBeenCalledWith(testError);
});

it('catches thrown errors and forwards to next()', () => {
const mw1 = (_req: Request, _res: Response, _next: any) => { throw new Error('thrown error'); };

const composed = composeMiddleware(mw1);
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

expect(next).toHaveBeenCalledWith(expect.any(Error));
});

it('catches async rejections and forwards to next()', async () => {
const mw1 = async (_req: Request, _res: Response, _next: any) => {
throw new Error('async rejection');
};

const composed = composeMiddleware(mw1);
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

await new Promise(process.nextTick);
expect(next).toHaveBeenCalledWith(expect.any(Error));
});

it('calls next() when no middleware is provided', () => {
const composed = composeMiddleware();
const req = makeReq();
const res = makeRes();
const next = vi.fn();

composed(req, res, next);

expect(next).toHaveBeenCalledOnce();
});
});

describe('createMiddlewareChain', () => {
it('builds and executes a chain', () => {
const order: number[] = [];
const chain = createMiddlewareChain()
.use(
(_req: Request, _res: Response, next: any) => { order.push(1); next(); },
(_req: Request, _res: Response, next: any) => { order.push(2); next(); },
)
.use(
(_req: Request, _res: Response, next: any) => { order.push(3); next(); },
);

const req = makeReq();
const res = makeRes();
const next = vi.fn();

chain.execute(req, res, next);

expect(order).toEqual([1, 2, 3]);
expect(next).toHaveBeenCalledOnce();
});

it('supports chaining .use() multiple times on returned chain', () => {
const order: number[] = [];
const chain = createMiddlewareChain()
.use((_req, _res, next) => { order.push(1); next(); });

const chain2 = chain.use((_req, _res, next) => { order.push(2); next(); });

const req = makeReq();
const res = makeRes();
const next = vi.fn();

chain2.execute(req, res, next);

expect(order).toEqual([1, 2]);
});
});
Loading