Skip to content

Commit f728c4a

Browse files
authored
chore: follow up to SWR to address not blocking comments (#238)
* test: rename test to have more accurate name * feat: create OTEL span for generating response, add some useful attributes * fix: fix wrong cache-status for miss * chore: move http attributes to entry of handler, add status_code, rethrow error * feat: add span for getting blob for date calculation
1 parent d2eeda9 commit f728c4a

File tree

8 files changed

+102
-54
lines changed

8 files changed

+102
-54
lines changed

src/build/templates/handler-monorepo.tmpl.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,23 @@ export default async function (req, context) {
1717
'deploy.id': context.deploy.id,
1818
'request.id': context.requestId,
1919
'site.id': context.site.id,
20+
'http.method': req.method,
21+
'http.target': req.url,
2022
monorepo: true,
2123
cwd: '{{cwd}}',
2224
})
2325
if (!cachedHandler) {
2426
const { default: handler } = await import('./{{nextServerHandler}}')
2527
cachedHandler = handler
2628
}
27-
return cachedHandler(req, context)
29+
const response = await cachedHandler(req, context)
30+
span.setAttributes({
31+
'http.status_code': response.status,
32+
})
33+
return response
2834
} catch (error) {
2935
span.recordException(error)
36+
throw error
3037
} finally {
3138
span.end()
3239
}

src/build/templates/handler.tmpl.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import serverHandler from './dist/run/handlers/server.js'
22
import tracing, { trace } from './dist/run/handlers/tracing.js'
33

4-
export default function handler(req, context) {
4+
export default async function handler(req, context) {
55
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
66
tracing.start()
77
}
@@ -14,10 +14,17 @@ export default function handler(req, context) {
1414
'deploy.id': context.deploy.id,
1515
'request.id': context.requestId,
1616
'site.id': context.site.id,
17+
'http.method': req.method,
18+
'http.target': req.url,
1719
})
18-
return serverHandler(req, context)
20+
const response = await serverHandler(req, context)
21+
span.setAttributes({
22+
'http.status_code': response.status,
23+
})
24+
return response
1925
} catch (error) {
2026
span.recordException(error)
27+
throw error
2128
} finally {
2229
span.end()
2330
}

src/run/handlers/server.ts

+45-40
Original file line numberDiff line numberDiff line change
@@ -46,50 +46,55 @@ export default async (request: Request) => {
4646
})
4747
}
4848

49-
const { req, res } = toReqRes(request)
49+
return await tracer.startActiveSpan('generate response', async (span) => {
50+
const { req, res } = toReqRes(request)
5051

51-
const resProxy = nextResponseProxy(res)
52+
const resProxy = nextResponseProxy(res)
5253

53-
// temporary workaround for https://linear.app/netlify/issue/ADN-111/
54-
delete req.headers['accept-encoding']
54+
// temporary workaround for https://linear.app/netlify/issue/ADN-111/
55+
delete req.headers['accept-encoding']
5556

56-
// We don't await this here, because it won't resolve until the response is finished.
57-
const nextHandlerPromise = nextHandler(req, resProxy).catch((error) => {
58-
logger.withError(error).error('next handler error')
59-
console.error(error)
60-
resProxy.statusCode = 500
61-
resProxy.end('Internal Server Error')
62-
})
57+
// We don't await this here, because it won't resolve until the response is finished.
58+
const nextHandlerPromise = nextHandler(req, resProxy).catch((error) => {
59+
logger.withError(error).error('next handler error')
60+
console.error(error)
61+
resProxy.statusCode = 500
62+
resProxy.end('Internal Server Error')
63+
})
6364

64-
// Contrary to the docs, this resolves when the headers are available, not when the stream closes.
65-
// See https://github.com/fastly/http-compute-js/blob/main/src/http-compute-js/http-server.ts#L168-L173
66-
const response = await toComputeResponse(resProxy)
67-
68-
await adjustDateHeader(response.headers, request)
69-
70-
setCacheControlHeaders(response.headers, request)
71-
setCacheTagsHeaders(response.headers, request, tagsManifest)
72-
setVaryHeaders(response.headers, request, nextConfig)
73-
handleNextCacheHeader(response.headers)
74-
75-
// Temporary workaround for an issue where sending a response with an empty
76-
// body causes an unhandled error. This doesn't catch everything, but redirects are the
77-
// most common case of sending empty bodies. We can't check it directly because these are streams.
78-
// The side effect is that responses which do contain data will not be streamed to the client,
79-
// but that's fine for redirects.
80-
// TODO: Remove once a fix has been rolled out.
81-
if ((response.status > 300 && response.status < 400) || response.status >= 500) {
82-
const body = await response.text()
83-
return new Response(body || null, response)
84-
}
65+
// Contrary to the docs, this resolves when the headers are available, not when the stream closes.
66+
// See https://github.com/fastly/http-compute-js/blob/main/src/http-compute-js/http-server.ts#L168-L173
67+
const response = await toComputeResponse(resProxy)
68+
69+
await adjustDateHeader(response.headers, request, span, tracer)
70+
71+
setCacheControlHeaders(response.headers, request)
72+
setCacheTagsHeaders(response.headers, request, tagsManifest)
73+
setVaryHeaders(response.headers, request, nextConfig)
74+
handleNextCacheHeader(response.headers)
75+
76+
// Temporary workaround for an issue where sending a response with an empty
77+
// body causes an unhandled error. This doesn't catch everything, but redirects are the
78+
// most common case of sending empty bodies. We can't check it directly because these are streams.
79+
// The side effect is that responses which do contain data will not be streamed to the client,
80+
// but that's fine for redirects.
81+
// TODO: Remove once a fix has been rolled out.
82+
if ((response.status > 300 && response.status < 400) || response.status >= 500) {
83+
const body = await response.text()
84+
span.end()
85+
return new Response(body || null, response)
86+
}
87+
88+
const keepOpenUntilNextFullyRendered = new TransformStream({
89+
flush() {
90+
// it's important to keep the stream open until the next handler has finished,
91+
// or otherwise the cache revalidates might not go through
92+
return nextHandlerPromise.then(() => {
93+
span.end()
94+
})
95+
},
96+
})
8597

86-
const keepOpenUntilNextFullyRendered = new TransformStream({
87-
flush() {
88-
// it's important to keep the stream open until the next handler has finished,
89-
// or otherwise the cache revalidates might not go through
90-
return nextHandlerPromise
91-
},
98+
return new Response(response.body?.pipeThrough(keepOpenUntilNextFullyRendered), response)
9299
})
93-
94-
return new Response(response.body?.pipeThrough(keepOpenUntilNextFullyRendered), response)
95100
}

src/run/headers.ts

+31-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getDeployStore } from '@netlify/blobs'
2+
import type { Span, Tracer } from '@opentelemetry/api'
23
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
34

45
import { encodeBlobKey } from '../shared/blobkey.js'
@@ -81,17 +82,42 @@ export const setVaryHeaders = (
8182
* By default, Next.js sets the date header to the current time, even if it's served
8283
* from the cache, meaning that the CDN will cache it for 10 seconds, which is incorrect.
8384
*/
84-
export const adjustDateHeader = async (headers: Headers, request: Request) => {
85+
export const adjustDateHeader = async (
86+
headers: Headers,
87+
request: Request,
88+
span: Span,
89+
tracer: Tracer,
90+
) => {
8591
const cacheState = headers.get('x-nextjs-cache')
8692
const isServedFromCache = cacheState === 'HIT' || cacheState === 'STALE'
93+
94+
span.setAttributes({
95+
'x-nextjs-cache': cacheState ?? undefined,
96+
isServedFromCache,
97+
})
98+
8799
if (!isServedFromCache) {
88100
return
89101
}
90-
const path = new URL(request.url).pathname
91-
const key = await encodeBlobKey(path)
102+
const key = new URL(request.url).pathname
103+
const blobKey = await encodeBlobKey(key)
92104
const blobStore = getDeployStore()
105+
93106
// TODO: use metadata for this
94-
const { lastModified } = (await blobStore.get(key, { type: 'json' })) ?? {}
107+
const { lastModified } = await tracer.startActiveSpan(
108+
'get cache to calculate date header',
109+
async (getBlobForDateSpan) => {
110+
getBlobForDateSpan.setAttributes({
111+
key,
112+
blobKey,
113+
})
114+
const blob = (await blobStore.get(blobKey, { type: 'json' })) ?? {}
115+
116+
getBlobForDateSpan.addEvent(blob ? 'Cache hit' : 'Cache miss')
117+
getBlobForDateSpan.end()
118+
return blob
119+
},
120+
)
95121

96122
if (!lastModified) {
97123
return
@@ -142,7 +168,7 @@ export const setCacheTagsHeaders = (headers: Headers, request: Request, manifest
142168
*/
143169
const NEXT_CACHE_TO_CACHE_STATUS: Record<string, string> = {
144170
HIT: `hit`,
145-
MISS: `miss,`,
171+
MISS: `fwd=miss`,
146172
STALE: `hit; fwd=stale`,
147173
}
148174

tests/e2e/simple-app.test.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ test('next/image is using Netlify Image CDN', async ({ page }) => {
5858
const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
5959

6060
// adaptation of https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/app-static/app-static.test.ts#L1716-L1755
61-
test('should stream properly', async ({ page }) => {
61+
test('streams stale responses', async ({ page }) => {
6262
// Prime the cache.
6363
const path = `${ctx.url}/stale-cache-serving/app-page`
6464
const res = await fetch(path)
@@ -99,6 +99,9 @@ test('should stream properly', async ({ page }) => {
9999
)
100100
})
101101

102-
expect(timings.startedStreaming - timings.start, `streams in less than 3s, run #${i}/6`).toBeLessThan(3000)
102+
expect(
103+
timings.startedStreaming - timings.start,
104+
`streams in less than 3s, run #${i}/6`,
105+
).toBeLessThan(3000)
103106
}
104107
})

tests/integration/cache-handler.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ describe('app router', () => {
205205
expect(load(post3.body)('h1').text()).toBe('Revalidate Fetch')
206206
expect(post3.headers, 'a cache miss on a non prerendered page').toEqual(
207207
expect.objectContaining({
208-
'cache-status': expect.stringMatching(/"Next.js"; miss/),
208+
'cache-status': '"Next.js"; fwd=miss',
209209
}),
210210
)
211211

tests/integration/revalidate-path.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ test<FixtureTestContext>('should revalidate a route by path', async (ctx) => {
7575
expect(load(post2.body)('h1').text()).toBe('Hello, Statically fetched show 1')
7676
expect(post2.headers, 'a cache miss on the on demand revalidated path /1').toEqual(
7777
expect.objectContaining({
78-
'cache-status': expect.stringMatching(/"Next.js"; miss/),
78+
'cache-status': '"Next.js"; fwd=miss',
7979
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
8080
}),
8181
)
8282
expect(post2Route2.headers, 'a cache miss on the on demand revalidated path /2').toEqual(
8383
expect.objectContaining({
84-
'cache-status': expect.stringMatching(/"Next.js"; miss/),
84+
'cache-status': '"Next.js"; fwd=miss',
8585
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
8686
}),
8787
)

tests/integration/revalidate-tags.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ test<FixtureTestContext>('should revalidate a route by tag', async (ctx) => {
5757
expect(load(post2.body)('h1').text()).toBe('Hello, Static Fetch 1')
5858
expect(post2.headers, 'a cache miss on the on demand revalidated page').toEqual(
5959
expect.objectContaining({
60-
'cache-status': expect.stringMatching(/"Next.js"; miss/),
60+
'cache-status': '"Next.js"; fwd=miss',
6161
'netlify-cdn-cache-control': 's-maxage=31536000, stale-while-revalidate=31536000',
6262
}),
6363
)

0 commit comments

Comments
 (0)