All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m56s
219 lines
7.7 KiB
TypeScript
219 lines
7.7 KiB
TypeScript
// src/middleware/deprecation.middleware.ts
|
|
/**
|
|
* @file Deprecation Headers Middleware - RFC 8594 Compliant
|
|
* Implements ADR-008 Phase 2: API Versioning Infrastructure.
|
|
*
|
|
* This middleware adds standard deprecation headers to API responses when
|
|
* a deprecated API version is being accessed. It follows:
|
|
* - RFC 8594: The "Sunset" HTTP Header Field
|
|
* - draft-ietf-httpapi-deprecation-header: The "Deprecation" HTTP Header Field
|
|
* - RFC 8288: Web Linking (for successor-version relation)
|
|
*
|
|
* Headers added for deprecated versions:
|
|
* - `Deprecation: true` - Indicates the endpoint is deprecated
|
|
* - `Sunset: <ISO 8601 date>` - When the endpoint will be removed
|
|
* - `Link: </api/vX>; rel="successor-version"` - URL to the replacement version
|
|
* - `X-API-Deprecation-Notice: <message>` - Human-readable deprecation message
|
|
*
|
|
* Always added (for all versions):
|
|
* - `X-API-Version: <version>` - The API version being accessed
|
|
*
|
|
* @see docs/architecture/api-versioning-infrastructure.md
|
|
* @see https://datatracker.ietf.org/doc/html/rfc8594
|
|
*/
|
|
|
|
import { Request, Response, NextFunction } from 'express';
|
|
import { ApiVersion, VERSION_CONFIGS, getVersionDeprecation } from '../config/apiVersions';
|
|
import { createScopedLogger } from '../services/logger.server';
|
|
|
|
// Create a module-scoped logger for deprecation tracking
|
|
const deprecationLogger = createScopedLogger('deprecation-middleware');
|
|
|
|
/**
|
|
* HTTP header names for deprecation signaling.
|
|
* Using constants to ensure consistency and prevent typos.
|
|
*/
|
|
export const DEPRECATION_HEADERS = {
|
|
/** RFC draft-ietf-httpapi-deprecation-header: Indicates deprecation status */
|
|
DEPRECATION: 'Deprecation',
|
|
/** RFC 8594: ISO 8601 date when the endpoint will be removed */
|
|
SUNSET: 'Sunset',
|
|
/** RFC 8288: Link to successor version with rel="successor-version" */
|
|
LINK: 'Link',
|
|
/** Custom header: Human-readable deprecation notice */
|
|
DEPRECATION_NOTICE: 'X-API-Deprecation-Notice',
|
|
/** Custom header: Current API version being accessed */
|
|
API_VERSION: 'X-API-Version',
|
|
} as const;
|
|
|
|
/**
|
|
* Creates middleware that adds RFC 8594 compliant deprecation headers
|
|
* to responses when a deprecated API version is accessed.
|
|
*
|
|
* This is a middleware factory function that takes a version parameter
|
|
* and returns the configured middleware function. This pattern allows
|
|
* different version routers to have their own deprecation configuration.
|
|
*
|
|
* @param version - The API version this middleware is handling
|
|
* @returns Express middleware function that adds appropriate headers
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // In a versioned router factory:
|
|
* const v1Router = Router();
|
|
* v1Router.use(addDeprecationHeaders('v1'));
|
|
*
|
|
* // When v1 is deprecated, responses will include:
|
|
* // Deprecation: true
|
|
* // Sunset: 2027-01-01T00:00:00Z
|
|
* // Link: </api/v2>; rel="successor-version"
|
|
* // X-API-Deprecation-Notice: API v1 is deprecated...
|
|
* // X-API-Version: v1
|
|
* ```
|
|
*/
|
|
export function addDeprecationHeaders(version: ApiVersion) {
|
|
// Pre-fetch configuration at middleware creation time for efficiency.
|
|
// This avoids repeated lookups on every request.
|
|
const config = VERSION_CONFIGS[version];
|
|
const deprecationInfo = getVersionDeprecation(version);
|
|
|
|
return function deprecationHeadersMiddleware(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
): void {
|
|
// Always set the API version header for transparency and debugging.
|
|
// This helps clients know which version they're using, especially
|
|
// useful when default version routing is in effect.
|
|
res.set(DEPRECATION_HEADERS.API_VERSION, version);
|
|
|
|
// Only add deprecation headers if this version is actually deprecated.
|
|
// Active versions should not have any deprecation headers.
|
|
if (config.status === 'deprecated') {
|
|
// RFC draft-ietf-httpapi-deprecation-header: Set to "true" to indicate deprecation
|
|
res.set(DEPRECATION_HEADERS.DEPRECATION, 'true');
|
|
|
|
// RFC 8594: Sunset header with ISO 8601 date indicating removal date
|
|
if (config.sunsetDate) {
|
|
res.set(DEPRECATION_HEADERS.SUNSET, config.sunsetDate);
|
|
}
|
|
|
|
// RFC 8288: Link header with successor-version relation
|
|
// This tells clients where to migrate to
|
|
if (config.successorVersion) {
|
|
res.set(
|
|
DEPRECATION_HEADERS.LINK,
|
|
`</api/${config.successorVersion}>; rel="successor-version"`,
|
|
);
|
|
}
|
|
|
|
// Custom header: Human-readable message for developers
|
|
// This provides context that may not be obvious from the standard headers
|
|
if (deprecationInfo.message) {
|
|
res.set(DEPRECATION_HEADERS.DEPRECATION_NOTICE, deprecationInfo.message);
|
|
}
|
|
|
|
// Attach deprecation info to the request for use in route handlers.
|
|
// This allows handlers to implement version-specific behavior or logging.
|
|
req.versionDeprecation = deprecationInfo;
|
|
|
|
// Log deprecation access at debug level to avoid log spam.
|
|
// This provides visibility into deprecated API usage without overwhelming logs.
|
|
// Use debug level because high-traffic APIs could generate significant volume.
|
|
// Production monitoring should use the access logs or metrics aggregation
|
|
// to track deprecation usage patterns.
|
|
deprecationLogger.debug(
|
|
{
|
|
apiVersion: version,
|
|
method: req.method,
|
|
path: req.path,
|
|
sunsetDate: config.sunsetDate,
|
|
successorVersion: config.successorVersion,
|
|
userAgent: req.get('User-Agent'),
|
|
// Include request ID if available from the request logger
|
|
requestId: (req.log as { bindings?: () => { request_id?: string } })?.bindings?.()
|
|
?.request_id,
|
|
},
|
|
'Deprecated API version accessed',
|
|
);
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Standalone middleware for adding deprecation headers based on
|
|
* the `apiVersion` property already set on the request.
|
|
*
|
|
* This middleware should be used after the version extraction middleware
|
|
* has set `req.apiVersion`. It provides a more flexible approach when
|
|
* the version is determined dynamically rather than statically.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // After version extraction middleware:
|
|
* router.use(extractApiVersion);
|
|
* router.use(addDeprecationHeadersFromRequest);
|
|
* ```
|
|
*/
|
|
export function addDeprecationHeadersFromRequest(
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction,
|
|
): void {
|
|
const version = req.apiVersion;
|
|
|
|
// If no version is set on the request, skip deprecation handling.
|
|
// This should not happen if the version extraction middleware ran first,
|
|
// but we handle it gracefully for safety.
|
|
if (!version) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const config = VERSION_CONFIGS[version];
|
|
const deprecationInfo = getVersionDeprecation(version);
|
|
|
|
// Always set the API version header
|
|
res.set(DEPRECATION_HEADERS.API_VERSION, version);
|
|
|
|
// Add deprecation headers if version is deprecated
|
|
if (config.status === 'deprecated') {
|
|
res.set(DEPRECATION_HEADERS.DEPRECATION, 'true');
|
|
|
|
if (config.sunsetDate) {
|
|
res.set(DEPRECATION_HEADERS.SUNSET, config.sunsetDate);
|
|
}
|
|
|
|
if (config.successorVersion) {
|
|
res.set(
|
|
DEPRECATION_HEADERS.LINK,
|
|
`</api/${config.successorVersion}>; rel="successor-version"`,
|
|
);
|
|
}
|
|
|
|
if (deprecationInfo.message) {
|
|
res.set(DEPRECATION_HEADERS.DEPRECATION_NOTICE, deprecationInfo.message);
|
|
}
|
|
|
|
req.versionDeprecation = deprecationInfo;
|
|
|
|
deprecationLogger.debug(
|
|
{
|
|
apiVersion: version,
|
|
method: req.method,
|
|
path: req.path,
|
|
sunsetDate: config.sunsetDate,
|
|
successorVersion: config.successorVersion,
|
|
userAgent: req.get('User-Agent'),
|
|
requestId: (req.log as { bindings?: () => { request_id?: string } })?.bindings?.()
|
|
?.request_id,
|
|
},
|
|
'Deprecated API version accessed',
|
|
);
|
|
}
|
|
|
|
next();
|
|
}
|