Skip to content

Commit

Permalink
feat(storage-*): add support for browser-based caching via etags (#10014
Browse files Browse the repository at this point in the history
)

This PR makes changes to every storage adapter in order to add
browser-based caching by returning etags, then checking for them into
incoming requests and responding a status code of `304` so the data
doesn't have to be returned again.

Performance improvements for cached subsequent requests:

![image](https://github.com/user-attachments/assets/e51b812c-63a0-4bdb-a396-0f172982cb07)


This respects `disableCache` in the dev tools.



Also fixes a bug with getting the latest image when using the Vercel
Blob Storage adapter.
  • Loading branch information
paulpopus authored Dec 18, 2024
1 parent 194a8c1 commit ef90ebb
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 2 deletions.
12 changes: 12 additions & 0 deletions packages/storage-azure/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle

const response = blob._response

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = response.headers.get('etag')

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
...response.headers.rawHeaders(),
}),
status: 304,
})
}

// Manually create a ReadableStream for the web from a Node.js stream.
const readableStream = new ReadableStream({
start(controller) {
Expand Down
14 changes: 14 additions & 0 deletions packages/storage-gcs/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat

const [metadata] = await file.getMetadata()

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = metadata.etag

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Content-Length': String(metadata.size),
'Content-Type': String(metadata.contentType),
ETag: String(metadata.etag),
}),
status: 304,
})
}

// Manually create a ReadableStream for the web from a Node.js stream.
const readableStream = new ReadableStream({
start(controller) {
Expand Down
15 changes: 15 additions & 0 deletions packages/storage-s3/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
return new Response(null, { status: 404, statusText: 'Not Found' })
}

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = object.ETag

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Accept-Ranges': String(object.AcceptRanges),
'Content-Length': String(object.ContentLength),
'Content-Type': String(object.ContentType),
ETag: String(object.ETag),
}),
status: 304,
})
}

const bodyBuffer = await streamToBuffer(object.Body)

return new Response(bodyBuffer, {
Expand Down
16 changes: 16 additions & 0 deletions packages/storage-uploadthing/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
}

const key = getKeyFromFilename(retrievedDoc, filename)

if (!key) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}
Expand All @@ -67,10 +68,25 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {

const blob = await response.blob()

const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
const objectEtag = response.headers.get('etag') as string

if (etagFromHeaders && etagFromHeaders === objectEtag) {
return new Response(null, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
status: 304,
})
}

return new Response(blob, {
headers: new Headers({
'Content-Length': String(blob.size),
'Content-Type': blob.type,
ETag: objectEtag,
}),
status: 200,
})
Expand Down
28 changes: 26 additions & 2 deletions packages/storage-vercel-blob/src/staticHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,38 @@ export const getStaticHandler = (
return async (req, { params: { filename } }) => {
try {
const prefix = await getFilePrefix({ collection, filename, req })
const fileKey = path.posix.join(prefix, filename)

const fileUrl = `${baseUrl}/${path.posix.join(prefix, filename)}`
const fileUrl = `${baseUrl}/${fileKey}`
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')

const blobMetadata = await head(fileUrl, { token })
if (!blobMetadata) {
return new Response(null, { status: 404, statusText: 'Not Found' })
}

const uploadedAtString = blobMetadata.uploadedAt.toISOString()
const ETag = `"${fileKey}-${uploadedAtString}"`

const { contentDisposition, contentType, size } = blobMetadata
const response = await fetch(fileUrl)

if (etagFromHeaders && etagFromHeaders === ETag) {
return new Response(null, {
headers: new Headers({
'Cache-Control': `public, max-age=${cacheControlMaxAge}`,
'Content-Disposition': contentDisposition,
'Content-Length': String(size),
'Content-Type': contentType,
ETag,
}),
status: 304,
})
}

const response = await fetch(`${fileUrl}?${uploadedAtString}`, {
cache: 'no-store',
})

const blob = await response.blob()

if (!blob) {
Expand All @@ -42,6 +64,8 @@ export const getStaticHandler = (
'Content-Disposition': contentDisposition,
'Content-Length': String(size),
'Content-Type': contentType,
ETag,
'Last-Modified': blobMetadata.uploadedAt.toUTCString(),
}),
status: 200,
})
Expand Down

0 comments on commit ef90ebb

Please sign in to comment.