Cache Service Integration Guide - Wiz-DevTech/prettygirllz GitHub Wiki
The Cache Service provides a REST API for retrieving cached server-side rendered (SSR) HTML content. It serves as a fallback mechanism when primary services are unavailable.
http://localhost:3000
GET /test-db
Description: Returns all cached entries for debugging and verification purposes.
Request:
GET /test-db HTTP/1.1
Host: localhost:3000
Response:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"route": "/home",
"html": "<html>...</html>",
"expiry": "2025-05-17T10:00:00.000Z",
"created_at": "2025-05-16T10:00:00.000Z"
}
]
Error Response:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": "relation \"ssr_cache\" does not exist"
}
GET /fallback/:route
Description: Retrieves cached HTML content for a specific route.
Parameters:
-
route
(path parameter): The route to retrieve (e.g., "home", "about", "products")
Request:
GET /fallback/home HTTP/1.1
Host: localhost:3000
Response (Success):
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<head><title>Home</title></head>
<body>Cached home page content</body>
</html>
Response (No Cache):
HTTP/1.1 200 OK
Content-Type: text/html
<div>No cached version</div>
Response (Error):
HTTP/1.1 500 Internal Server Error
Content-Type: text/html
Database error
// Function to get cached content with fallback
async function getCachedContent(route) {
try {
const response = await fetch(`http://localhost:3000/fallback/${route}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const html = await response.text();
return html;
} catch (error) {
console.error('Failed to fetch cached content:', error);
return '<div>Content temporarily unavailable</div>';
}
}
// Usage
getCachedContent('home').then(html => {
document.getElementById('content').innerHTML = html;
});
import React, { useState, useEffect } from 'react';
function CachedContent({ route }) {
const [html, setHtml] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchCached() {
try {
const response = await fetch(`/api/fallback/${route}`);
const content = await response.text();
setHtml(content);
} catch (error) {
setHtml('<div>Content unavailable</div>');
} finally {
setLoading(false);
}
}
fetchCached();
}, [route]);
if (loading) return <div>Loading...</div>;
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
- No Authentication: The API is currently open to all requests
- No Rate Limiting: No protection against abuse
- No Input Validation: Route parameters are not validated
- No HTTPS: Plain HTTP communication
// Add JWT middleware (example)
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
// Apply to protected routes
app.get('/fallback/:route', authenticateToken, async (req, res) => {
// ... existing route logic
});
// Add route validation
app.get('/fallback/:route', (req, res, next) => {
const route = req.params.route;
// Validate route format
if (!/^[a-zA-Z0-9-_/]+$/.test(route)) {
return res.status(400).send('Invalid route format');
}
// Prevent path traversal
if (route.includes('..') || route.includes('//')) {
return res.status(400).send('Invalid route');
}
next();
}, async (req, res) => {
// ... existing route logic
});
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP'
});
app.use('/fallback', limiter);
All errors should follow a consistent format:
interface ErrorResponse {
error: string;
code?: string;
details?: any;
timestamp: string;
}
// Global error handler
app.use((err, req, res, next) => {
const errorResponse = {
error: err.message || 'Internal Server Error',
code: err.code || 'UNKNOWN_ERROR',
timestamp: new Date().toISOString()
};
// Log error
console.error('API Error:', err);
// Respond with appropriate status
const status = err.status || 500;
res.status(status).json(errorResponse);
});
app.get('/fallback/:route', async (req, res) => {
try {
// Set cache headers
res.set({
'Cache-Control': 'public, max-age=300', // 5 minutes
'ETag': `W/"${Date.now()}"`,
'Last-Modified': new Date().toUTCString()
});
// ... existing logic
} catch (err) {
res.status(500).send('Database error');
}
});
const compression = require('compression');
app.use(compression());
const pool = new Pool({
user: 'gateway',
password: 'secret',
host: 'localhost',
database: 'gateway_cache',
port: 5432,
// Connection pool configuration
max: 20, // Maximum number of clients
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return error after 2 seconds if no connection
});
openapi: 3.0.0
info:
title: Cache Service API
version: 1.0.0
description: API for retrieving cached SSR content
paths:
/test-db:
get:
summary: Test database connection
responses:
'200':
description: List of cached entries
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
route:
type: string
html:
type: string
expiry:
type: string
format: date-time
/fallback/{route}:
get:
summary: Get cached content for route
parameters:
- name: route
in: path
required: true
schema:
type: string
responses:
'200':
description: Cached HTML content
content:
text/html:
schema:
type: string
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED';
this.failures = 0;
this.nextAttempt = Date.now();
}
async call(request) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await request();
this.reset();
return result;
} catch (error) {
this.recordFailure();
throw error;
}
}
recordFailure() {
this.failures++;
if (this.failures >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
}
reset() {
this.failures = 0;
this.state = 'CLOSED';
}
}
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
app.get('/health', async (req, res) => {
try {
// Check database connectivity
await pool.query('SELECT 1');
// Check cache table
const result = await pool.query('SELECT COUNT(*) FROM ssr_cache');
res.json({
status: 'healthy',
database: 'connected',
cache_entries: result.rows[0].count,
timestamp: new Date().toISOString()
});
} catch (error) {
res.status(503).json({
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString()
});
}
});
// Using Jest
describe('Cache Service', () => {
test('should return cached content for valid route', async () => {
const response = await request(app)
.get('/fallback/home')
.expect(200)
.expect('Content-Type', /html/);
expect(response.text).toBeDefined();
});
test('should return no cached version for invalid route', async () => {
const response = await request(app)
.get('/fallback/nonexistent')
.expect(200);
expect(response.text).toBe('<div>No cached version</div>');
});
});
describe('Integration Tests', () => {
beforeAll(async () => {
// Setup test database
await setupTestDB();
});
afterAll(async () => {
// Cleanup test database
await cleanupTestDB();
});
test('full workflow test', async () => {
// Insert test data
await pool.query(
'INSERT INTO ssr_cache (route, html, expiry) VALUES ($1, $2, $3)',
['/test', '<div>Test Content</div>', new Date(Date.now() + 86400000)]
);
// Test retrieval
const response = await request(app)
.get('/fallback/test')
.expect(200);
expect(response.text).toBe('<div>Test Content</div>');
});
});
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# Production environment variables
DB_HOST=postgres-host
DB_PORT=5432
DB_NAME=gateway_cache
DB_USER=gateway
DB_PASSWORD=secure_password
NODE_ENV=production
PORT=3000
- Set up application performance monitoring (APM)
- Configure log aggregation
- Implement metrics collection
- Set up alerting for critical errors