Skip to content

Commit ef90ebb

Browse files
authored
feat(storage-*): add support for browser-based caching via etags (#10014)
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.
1 parent 194a8c1 commit ef90ebb

File tree

5 files changed

+83
-2
lines changed

5 files changed

+83
-2
lines changed

packages/storage-azure/src/staticHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ export const getHandler = ({ collection, getStorageClient }: Args): StaticHandle
2929

3030
const response = blob._response
3131

32+
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
33+
const objectEtag = response.headers.get('etag')
34+
35+
if (etagFromHeaders && etagFromHeaders === objectEtag) {
36+
return new Response(null, {
37+
headers: new Headers({
38+
...response.headers.rawHeaders(),
39+
}),
40+
status: 304,
41+
})
42+
}
43+
3244
// Manually create a ReadableStream for the web from a Node.js stream.
3345
const readableStream = new ReadableStream({
3446
start(controller) {

packages/storage-gcs/src/staticHandler.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
1919

2020
const [metadata] = await file.getMetadata()
2121

22+
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
23+
const objectEtag = metadata.etag
24+
25+
if (etagFromHeaders && etagFromHeaders === objectEtag) {
26+
return new Response(null, {
27+
headers: new Headers({
28+
'Content-Length': String(metadata.size),
29+
'Content-Type': String(metadata.contentType),
30+
ETag: String(metadata.etag),
31+
}),
32+
status: 304,
33+
})
34+
}
35+
2236
// Manually create a ReadableStream for the web from a Node.js stream.
2337
const readableStream = new ReadableStream({
2438
start(controller) {

packages/storage-s3/src/staticHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ export const getHandler = ({ bucket, collection, getStorageClient }: Args): Stat
3535
return new Response(null, { status: 404, statusText: 'Not Found' })
3636
}
3737

38+
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
39+
const objectEtag = object.ETag
40+
41+
if (etagFromHeaders && etagFromHeaders === objectEtag) {
42+
return new Response(null, {
43+
headers: new Headers({
44+
'Accept-Ranges': String(object.AcceptRanges),
45+
'Content-Length': String(object.ContentLength),
46+
'Content-Type': String(object.ContentType),
47+
ETag: String(object.ETag),
48+
}),
49+
status: 304,
50+
})
51+
}
52+
3853
const bodyBuffer = await streamToBuffer(object.Body)
3954

4055
return new Response(bodyBuffer, {

packages/storage-uploadthing/src/staticHandler.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
4949
}
5050

5151
const key = getKeyFromFilename(retrievedDoc, filename)
52+
5253
if (!key) {
5354
return new Response(null, { status: 404, statusText: 'Not Found' })
5455
}
@@ -67,10 +68,25 @@ export const getHandler = ({ utApi }: Args): StaticHandler => {
6768

6869
const blob = await response.blob()
6970

71+
const etagFromHeaders = req.headers.get('etag') || req.headers.get('if-none-match')
72+
const objectEtag = response.headers.get('etag') as string
73+
74+
if (etagFromHeaders && etagFromHeaders === objectEtag) {
75+
return new Response(null, {
76+
headers: new Headers({
77+
'Content-Length': String(blob.size),
78+
'Content-Type': blob.type,
79+
ETag: objectEtag,
80+
}),
81+
status: 304,
82+
})
83+
}
84+
7085
return new Response(blob, {
7186
headers: new Headers({
7287
'Content-Length': String(blob.size),
7388
'Content-Type': blob.type,
89+
ETag: objectEtag,
7490
}),
7591
status: 200,
7692
})

packages/storage-vercel-blob/src/staticHandler.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,38 @@ export const getStaticHandler = (
1818
return async (req, { params: { filename } }) => {
1919
try {
2020
const prefix = await getFilePrefix({ collection, filename, req })
21+
const fileKey = path.posix.join(prefix, filename)
2122

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

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

31+
const uploadedAtString = blobMetadata.uploadedAt.toISOString()
32+
const ETag = `"${fileKey}-${uploadedAtString}"`
33+
2934
const { contentDisposition, contentType, size } = blobMetadata
30-
const response = await fetch(fileUrl)
35+
36+
if (etagFromHeaders && etagFromHeaders === ETag) {
37+
return new Response(null, {
38+
headers: new Headers({
39+
'Cache-Control': `public, max-age=${cacheControlMaxAge}`,
40+
'Content-Disposition': contentDisposition,
41+
'Content-Length': String(size),
42+
'Content-Type': contentType,
43+
ETag,
44+
}),
45+
status: 304,
46+
})
47+
}
48+
49+
const response = await fetch(`${fileUrl}?${uploadedAtString}`, {
50+
cache: 'no-store',
51+
})
52+
3153
const blob = await response.blob()
3254

3355
if (!blob) {
@@ -42,6 +64,8 @@ export const getStaticHandler = (
4264
'Content-Disposition': contentDisposition,
4365
'Content-Length': String(size),
4466
'Content-Type': contentType,
67+
ETag,
68+
'Last-Modified': blobMetadata.uploadedAt.toUTCString(),
4569
}),
4670
status: 200,
4771
})

0 commit comments

Comments
 (0)