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
11 changes: 11 additions & 0 deletions backend/src/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class EnvironmentConfig {
// Cache Configuration
readonly cache: {
TTL: number;
RESOURCE_TTL: number;
};

// Documentation Configuration
Expand Down Expand Up @@ -105,9 +106,19 @@ class EnvironmentConfig {
? 0
: Math.floor(rawCacheTTL);

// Server-side resource cache freshness window (seconds).
// Within this window, resources are served from the in-memory cache without
// contacting GitHub at all. Helps absorb upstream rate limits/outages.
const rawResourceTTL = this.getNumber('RESOURCE_CACHE_TTL', 60);
const resourceTTL =
!Number.isFinite(rawResourceTTL) || rawResourceTTL < 0
? 0
: Math.floor(rawResourceTTL);

// Cache Configuration (TTL in seconds, non-negative integer)
this.cache = {
TTL: cacheTTL, // 5 minutes by default when env var is not set
RESOURCE_TTL: resourceTTL, // 60 seconds server-side freshness by default
};

// Documentation Configuration
Expand Down
20 changes: 9 additions & 11 deletions backend/src/routes/composable.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { config } from '../config/env.config.js';
import { Request, Response } from 'express';
import { z } from 'zod';
import { ApiRouter } from '../utils/api.router.js';
import { ResourceManager, logger, extractConditionalHeaders, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ResourceManager, logger, checkClientCache, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { sendErrorFormatted, sendFormatted } from '../utils/response.format.js';
import {
ComposableImageDto,
Expand Down Expand Up @@ -52,12 +52,11 @@ composableRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
// Load composable resource with metadata, supporting conditional requests
const conditionalHeaders = extractConditionalHeaders(req);
const { data: composableData, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/composable.json', conditionalHeaders);
// Load composable resource with metadata (server-side cache + stale-on-error)
const { data: composableData, metadata } = await resourceManager.getResourceWithMetadata('image/composable.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -132,12 +131,11 @@ composableRouter.route({
const requestedChannel = typeof req.query.channel === 'string' ? req.query.channel : undefined;
const queryParams = requestedChannel ? { channel: requestedChannel } : undefined;

// Load composable resource with metadata, supporting conditional requests
const conditionalHeaders = extractConditionalHeaders(req);
const { data: composableData, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/composable.json', conditionalHeaders);
// Load composable resource with metadata (server-side cache + stale-on-error)
const { data: composableData, metadata } = await resourceManager.getResourceWithMetadata('image/composable.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata, queryParams)) {
return sendNotModified(res, metadata, config.cache.TTL, queryParams);
}

Expand Down
77 changes: 35 additions & 42 deletions backend/src/routes/extension.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { ApiRouter } from '../utils/api.router.js';
import { ResourceManager, escapeHtml, logger, extractConditionalHeaders, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ResourceManager, escapeHtml, logger, checkClientCache, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ErrorDetailsSchema, HeaderAcceptSchema } from '../schemas/api.schema.js';
import {
RuntimeExtensionListSchema,
Expand Down Expand Up @@ -88,11 +88,10 @@ extensionRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json', conditionalHeaders);

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json');

// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -152,11 +151,10 @@ extensionRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json', conditionalHeaders);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -228,12 +226,11 @@ extensionRouter.route({
const { id } = req.params as { id: string };
const imageId = escapeHtml(id);

const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json', conditionalHeaders);

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
return sendNotModified(res, metadata, config.cache.TTL);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/php_extensions.json');

// Return 304 when the client's cached representation still matches (keyed by extension id)
if (checkClientCache(req, metadata, { id })) {
return sendNotModified(res, metadata, config.cache.TTL, { id });
}

const extensionEntry = data?.cloud?.[id];
Expand All @@ -247,8 +244,8 @@ extensionRouter.route({
});
}

// Set cache headers
setCacheHeaders(res, metadata, config.cache.TTL);
// Set cache headers (keyed by extension id)
setCacheHeaders(res, metadata, config.cache.TTL, { id });

sendFormatted<RuntimeExtensionVersion>(res, extensionEntry);
} catch (error: any) {
Expand Down Expand Up @@ -295,11 +292,10 @@ extensionRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/postgresql_extensions.json', conditionalHeaders);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/postgresql_extensions.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -371,12 +367,11 @@ extensionRouter.route({
const { id } = req.params as { id: string };
const imageId = escapeHtml(id);

const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/postgresql_extensions.json', conditionalHeaders);

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
return sendNotModified(res, metadata, config.cache.TTL);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/postgresql_extensions.json');

// Return 304 when the client's cached representation still matches (keyed by extension id)
if (checkClientCache(req, metadata, { id })) {
return sendNotModified(res, metadata, config.cache.TTL, { id });
}

const extensionEntry = data?.cloud?.[id];
Expand All @@ -390,8 +385,8 @@ extensionRouter.route({
});
}

// Set cache headers
setCacheHeaders(res, metadata, config.cache.TTL);
// Set cache headers (keyed by extension id)
setCacheHeaders(res, metadata, config.cache.TTL, { id });

sendFormatted<RuntimeExtensionVersion>(res, extensionEntry);
} catch (error: any) {
Expand Down Expand Up @@ -438,11 +433,10 @@ extensionRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/solr_extensions.json', conditionalHeaders);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/solr_extensions.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -514,12 +508,11 @@ extensionRouter.route({
const { id } = req.params as { id: string };
const imageId = escapeHtml(id);

const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceWithMetadata('extension/solr_extensions.json', conditionalHeaders);

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
return sendNotModified(res, metadata, config.cache.TTL);
const { data, metadata } = await resourceManager.getResourceWithMetadata('extension/solr_extensions.json');

// Return 304 when the client's cached representation still matches (keyed by extension id)
if (checkClientCache(req, metadata, { id })) {
return sendNotModified(res, metadata, config.cache.TTL, { id });
}

const extensionEntry = data?.cloud?.[id];
Expand All @@ -533,8 +526,8 @@ extensionRouter.route({
});
}

// Set cache headers
setCacheHeaders(res, metadata, config.cache.TTL);
// Set cache headers (keyed by extension id)
setCacheHeaders(res, metadata, config.cache.TTL, { id });

sendFormatted<RuntimeExtensionVersion>(res, extensionEntry);
} catch (error: any) {
Expand Down
26 changes: 12 additions & 14 deletions backend/src/routes/image.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Request, Response } from 'express';
import { z } from 'zod';
import { ApiRouter } from '../utils/api.router.js';
import { withSelfLink } from '../utils/api.schema.js';
import { ResourceManager, escapeHtml, logger, extractConditionalHeaders, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ResourceManager, escapeHtml, logger, checkClientCache, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { sendErrorFormatted, sendFormatted } from '../utils/response.format.js';
import {
DeployImageListDto,
Expand Down Expand Up @@ -53,12 +53,11 @@ imageRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
// Load registry from resources with metadata, supporting conditional requests
const conditionalHeaders = extractConditionalHeaders(req);
const { data: registryRaw, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/registry.json', conditionalHeaders);
// Load registry from resources with metadata (server-side cache + stale-on-error)
const { data: registryRaw, metadata } = await resourceManager.getResourceWithMetadata('image/registry.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -123,13 +122,12 @@ imageRouter.route({
const { id } = req.params as { id: string };
const imageId = escapeHtml(id);

// Load registry from resources with metadata, supporting conditional requests
const conditionalHeaders = extractConditionalHeaders(req);
const { data: registryRaw, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/registry.json', conditionalHeaders);
// Load registry from resources with metadata (server-side cache + stale-on-error)
const { data: registryRaw, metadata } = await resourceManager.getResourceWithMetadata('image/registry.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
return sendNotModified(res, metadata, config.cache.TTL);
// Return 304 when the client's cached representation still matches (keyed by image id)
if (checkClientCache(req, metadata, { id })) {
return sendNotModified(res, metadata, config.cache.TTL, { id });
}

// Check if image exists
Expand All @@ -151,8 +149,8 @@ imageRouter.route({
? DeployImageSchemaDtoInternal.parse(imageRaw)
: DeployImageSchemaDtoPublic.parse(imageRaw);

// Set cache headers
setCacheHeaders(res, metadata, config.cache.TTL);
// Set cache headers (keyed by image id)
setCacheHeaders(res, metadata, config.cache.TTL, { id });

// Send formatted response
sendFormatted<DeployImageDto>(res, imageParsed);
Expand Down
20 changes: 10 additions & 10 deletions backend/src/routes/openapi.upsun.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { z } from 'zod';
import { config } from '../config/env.config.js';
import { ApiRouter } from '../utils/api.router.js';
import { ResourceManager, logger, extractConditionalHeaders, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ResourceManager, logger, checkClientCache, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { HeaderAcceptSchema, ErrorDetailsSchema } from '../schemas/api.schema.js';

const TAG = 'OpenAPI Specification';
Expand Down Expand Up @@ -63,17 +63,17 @@ openapiRouter.route({
fileName = format === 'yaml' ? 'openapispec-upsun.yaml' : 'openapispec-upsun.json';
}
try {
// serving the raw file according to the format with metadata
const conditionalHeaders = extractConditionalHeaders(req);
const { data, metadata, notModified } = await resourceManager.getResourceRawWithMetadata(`openapi/${fileName}`, conditionalHeaders);
// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
return sendNotModified(res, metadata, config.cache.TTL);
// serving the raw file according to the format (server-side cache + stale-on-error)
const cacheParams = { sdks: sdks ? 'true' : 'false' };
const { data, metadata } = await resourceManager.getResourceRawWithMetadata(`openapi/${fileName}`);

// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata, cacheParams)) {
return sendNotModified(res, metadata, config.cache.TTL, cacheParams);
}

// Set cache headers
setCacheHeaders(res, metadata, config.cache.TTL);
setCacheHeaders(res, metadata, config.cache.TTL, cacheParams);

if (format === 'yaml' && !sdks) {
res.type('text/plain; charset=utf-8').send(data);
Expand Down
30 changes: 13 additions & 17 deletions backend/src/routes/validation.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Request, Response } from 'express';
import { z } from 'zod';
import { config } from '../config/env.config.js';
import { ApiRouter } from '../utils/api.router.js';
import { ResourceManager, logger, extractConditionalHeaders, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ResourceManager, logger, checkClientCache, setCacheHeaders, sendNotModified } from '../utils/index.js';
import { ErrorDetailsSchema } from '../schemas/api.schema.js';
import { sendErrorFormatted, sendFormatted } from '../utils/response.format.js';
import { Validation, ValidationSchema } from '../schemas/validation.schema.js';
Expand Down Expand Up @@ -46,11 +46,10 @@ This file is used to validate Upsun configuration files .upsun/config.yaml.
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data: schema, metadata, notModified } = await resourceManager.getResourceWithMetadata('validation/upsun.json', conditionalHeaders);
const { data: schema, metadata } = await resourceManager.getResourceWithMetadata('validation/upsun.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -90,11 +89,10 @@ validationRouter.route({
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data: schema, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/registry.schema.json', conditionalHeaders);
const { data: schema, metadata } = await resourceManager.getResourceWithMetadata('image/registry.schema.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -150,11 +148,10 @@ The result is a JSON Schema snippet:
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data: registry, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/registry.json', conditionalHeaders);
const { data: registry, metadata } = await resourceManager.getResourceWithMetadata('image/registry.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down Expand Up @@ -235,11 +232,10 @@ for example: \`["php:7.2", "php:7.3", "nodejs:24"]\`.
},
handler: async (req: Request, res: Response) => {
try {
const conditionalHeaders = extractConditionalHeaders(req);
const { data: registry, metadata, notModified } = await resourceManager.getResourceWithMetadata('image/registry.json', conditionalHeaders);
const { data: registry, metadata } = await resourceManager.getResourceWithMetadata('image/registry.json');

// If upstream returned 304, respond with 304 (avoids unnecessary parsing)
if (notModified) {
// Return 304 when the client's cached representation still matches
if (checkClientCache(req, metadata)) {
return sendNotModified(res, metadata, config.cache.TTL);
}

Expand Down
Loading