Skip to content

Commit

Permalink
feat: stale-while-revalidate (#416)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos authored Jan 17, 2024
1 parent 610b4f3 commit 830ea71
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 5 deletions.
9 changes: 9 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ SERVER_HEADERS_TIMEOUT=65
SERVER_REGION=region-of-where-your-service-is-running


#######################################
# Request / Response
#######################################
REQUEST_URL_LENGTH_LIMIT=7500
REQUEST_TRACE_HEADER=trace-id
REQUEST_ETAG_HEADERS=if-none-match
RESPONSE_S_MAXAGE=0


#######################################
# Auth
#######################################
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type StorageConfigType = {
databaseConnectionTimeout: number
region: string
requestTraceHeader?: string
requestEtagHeaders: string[]
responseSMaxAge: number
serviceKey: string
storageBackendType: StorageBackendType
tenantId: string
Expand Down Expand Up @@ -153,6 +155,10 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
requestUrlLengthLimit:
Number(getOptionalConfigFromEnv('REQUEST_URL_LENGTH_LIMIT', 'URL_LENGTH_LIMIT')) || 7_500,
requestTraceHeader: getOptionalConfigFromEnv('REQUEST_TRACE_HEADER', 'REQUEST_ID_HEADER'),
requestEtagHeaders: getOptionalConfigFromEnv('REQUEST_ETAG_HEADERS')?.trim().split(',') || [
'if-none-match',
],
responseSMaxAge: parseInt(getOptionalConfigFromEnv('RESPONSE_S_MAXAGE') || '0', 10),

// Admin
adminApiKeys: getOptionalConfigFromEnv('SERVER_ADMIN_API_KEYS', 'ADMIN_API_KEYS') || '',
Expand Down
27 changes: 25 additions & 2 deletions src/storage/renderer/head.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AssetResponse, Renderer, RenderOptions } from './renderer'
import { FastifyRequest } from 'fastify'
import { StorageBackendAdapter } from '../backend'
import { FastifyReply, FastifyRequest } from 'fastify'
import { ObjectMetadata, StorageBackendAdapter } from '../backend'
import { ImageRenderer, TransformOptions } from './image'

/**
Expand All @@ -20,4 +20,27 @@ export class HeadRenderer extends Renderer {
transformations: ImageRenderer.applyTransformation(request.query as TransformOptions),
}
}

protected handleCacheControl(
request: FastifyRequest<any>,
response: FastifyReply<any>,
metadata: ObjectMetadata
) {
const etag = this.findEtagHeader(request)

const cacheControl = [metadata.cacheControl]

if (!etag) {
response.header('Cache-Control', cacheControl.join(', '))
return
}

if (etag !== metadata.eTag) {
cacheControl.push('must-revalidate')
} else if (this.sMaxAge > 0) {
cacheControl.push(`s-maxage=${this.sMaxAge}`)
}

response.header('Cache-Control', cacheControl.join(', '))
}
}
50 changes: 47 additions & 3 deletions src/storage/renderer/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FastifyReply, FastifyRequest } from 'fastify'
import { ObjectMetadata } from '../backend'
import { Readable } from 'stream'
import { getConfig } from '../../config'

export interface RenderOptions {
bucket: string
Expand All @@ -16,12 +17,16 @@ export interface AssetResponse {
transformations?: string[]
}

const { requestEtagHeaders, responseSMaxAge } = getConfig()

/**
* Renderer
* a generic renderer that respond to a request with an asset content
* and all the important headers
*/
export abstract class Renderer {
protected sMaxAge = responseSMaxAge

abstract getAsset(request: FastifyRequest, options: RenderOptions): Promise<AssetResponse>

/**
Expand All @@ -34,7 +39,7 @@ export abstract class Renderer {
try {
const data = await this.getAsset(request, options)

await this.setHeaders(response, data, options)
this.setHeaders(request, response, data, options)

return response.send(data.body)
} catch (err: any) {
Expand All @@ -55,7 +60,12 @@ export abstract class Renderer {
}
}

protected setHeaders(response: FastifyReply<any>, data: AssetResponse, options: RenderOptions) {
protected setHeaders(
request: FastifyRequest<any>,
response: FastifyReply<any>,
data: AssetResponse,
options: RenderOptions
) {
response
.status(data.metadata.httpStatusCode ?? 200)
.header('Accept-Ranges', 'bytes')
Expand All @@ -67,7 +77,7 @@ export abstract class Renderer {
if (options.expires) {
response.header('Expires', options.expires)
} else {
response.header('Cache-Control', data.metadata.cacheControl)
this.handleCacheControl(request, response, data.metadata)
}

if (data.metadata.contentRange) {
Expand Down Expand Up @@ -95,6 +105,40 @@ export abstract class Renderer {
}
}
}

protected handleCacheControl(
request: FastifyRequest<any>,
response: FastifyReply<any>,
metadata: ObjectMetadata
) {
const etag = this.findEtagHeader(request)

const cacheControl = [metadata.cacheControl]

if (!etag) {
response.header('Cache-Control', cacheControl.join(', '))
return
}

if (this.sMaxAge > 0) {
cacheControl.push(`s-maxage=${this.sMaxAge}`)
}

if (etag !== metadata.eTag) {
cacheControl.push('stale-while-revalidate=30')
}

response.header('Cache-Control', cacheControl.join(', '))
}

protected findEtagHeader(request: FastifyRequest<any>) {
for (const header of requestEtagHeaders) {
const etag = request.headers[header]
if (etag) {
return etag
}
}
}
}

function normalizeContentType(contentType: string | undefined): string | undefined {
Expand Down

0 comments on commit 830ea71

Please sign in to comment.