From 1bda2686e9806b6dce422c9660e125a6be8fdee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 21 Feb 2025 15:58:03 +0000 Subject: [PATCH 1/2] feat: log failures in `cache.put` --- packages/cache/src/bootstrap/cache.test.ts | 44 +++++++++++++++++++++- packages/cache/src/bootstrap/cache.ts | 10 ++++- packages/cache/src/bootstrap/errors.ts | 18 +++++++++ packages/cache/src/headers.ts | 1 + 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/cache/src/bootstrap/errors.ts diff --git a/packages/cache/src/bootstrap/cache.test.ts b/packages/cache/src/bootstrap/cache.test.ts index edb9b1d6..a4d82a50 100644 --- a/packages/cache/src/bootstrap/cache.test.ts +++ b/packages/cache/src/bootstrap/cache.test.ts @@ -1,8 +1,9 @@ import { Buffer } from 'node:buffer' -import { describe, test, expect } from 'vitest' +import { describe, test, expect, vi } from 'vitest' import { NetlifyCache } from './cache.js' +import { ERROR_CODES } from './errors.js' import { getMockFetch } from '../test/fetch.js' import { decodeHeaders } from '../test/headers.js' @@ -204,5 +205,46 @@ describe('Cache API', () => { expect([...resourceHeaders]).toStrictEqual([...headers]) }) + + test('logs a message when the response is not added to the cache', async () => { + const consoleWarn = vi.spyOn(globalThis.console, 'warn') + const mockFetch = getMockFetch({ + responses: { + 'https://example.netlify/.netlify/cache/https%3A%2F%2Fnetlify.com%2F': [ + (_, init) => { + const headers = init?.headers as Record + + expect(headers.Authorization).toBe(`Bearer ${token}`) + expect(headers['netlify-forwarded-host']).toBe(host) + + return new Response(null, { headers: { 'netlify-programmable-error': 'no_ttl' }, status: 400 }) + }, + ], + }, + }) + const cache = new NetlifyCache({ + base64Encode, + getHost: () => host, + getToken: () => token, + getURL: () => url, + name: 'my-cache', + userAgent, + }) + + const headers = new Headers() + headers.set('content-type', 'text/html') + headers.set('x-custom-header', 'foobar') + + const response = new Response('

Hello world

', { headers }) + + await cache.put(new Request('https://netlify.com'), response) + + mockFetch.restore() + + expect(consoleWarn).toHaveBeenCalledWith(`Failed to write to the cache: ${ERROR_CODES.no_ttl}`) + consoleWarn.mockRestore() + + expect(mockFetch.requests.length).toBe(1) + }) }) }) diff --git a/packages/cache/src/bootstrap/cache.ts b/packages/cache/src/bootstrap/cache.ts index 3ee2a365..b040322f 100644 --- a/packages/cache/src/bootstrap/cache.ts +++ b/packages/cache/src/bootstrap/cache.ts @@ -1,5 +1,6 @@ import type { Base64Encoder, EnvironmentOptions, Factory } from './environment.js' +import { ERROR_CODES, GENERIC_ERROR } from './errors.js' import * as HEADERS from '../headers.js' const allowedProtocols = new Set(['http:', 'https:']) @@ -147,7 +148,7 @@ export class NetlifyCache implements Cache { const resourceURL = extractAndValidateURL(request) - await fetch(`${this.#getURL()}/${toCacheKey(resourceURL)}`, { + const cacheResponse = await fetch(`${this.#getURL()}/${toCacheKey(resourceURL)}`, { body: response.body, headers: { ...this[getInternalHeaders](), @@ -158,6 +159,13 @@ export class NetlifyCache implements Cache { duplex: 'half', method: 'POST', }) + + if (!cacheResponse.ok) { + const errorDetail = cacheResponse.headers.get(HEADERS.ErrorDetail) ?? '' + const errorMessage = ERROR_CODES[errorDetail as keyof typeof ERROR_CODES] || GENERIC_ERROR + + console.warn(`Failed to write to the cache: ${errorMessage}`) + } } } diff --git a/packages/cache/src/bootstrap/errors.ts b/packages/cache/src/bootstrap/errors.ts new file mode 100644 index 00000000..2afaf574 --- /dev/null +++ b/packages/cache/src/bootstrap/errors.ts @@ -0,0 +1,18 @@ +export const ERROR_CODES = { + invalid_vary: + 'Responses must not use unsupported directives of the `Netlify-Vary` header (https://ntl.fyi/cache_api_invalid_vary).', + no_cache: + 'Responses must not set cache control headers with the `private`, `no-cache` or `no-store` directives (https://ntl.fyi/cache_api_no_cache).', + low_ttl: + 'Responses must have a cache control header with a `max-age` or `s-maxage` directive (https://ntl.fyi/cache_api_low_ttl).', + no_directive: + 'Responses must have a cache control header with caching directives (https://ntl.fyi/cache_api_no_directive).', + no_ttl: + 'Responses must have a cache control header with a `max-age` or `s-maxage` directive (https://ntl.fyi/cache_api_no_ttl).', + no_status: 'Responses must specify a status code (https://ntl.fyi/cache_api_no_status).', + invalid_directive: + 'Responses must have a cache control header with caching directives (https://ntl.fyi/cache_api_invalid_directive).', + status: 'Responses must have a status code between 200 and 299 (https://ntl.fyi/cache_api_status).', +} + +export const GENERIC_ERROR = 'The server has returned an unexpected error (https://ntl.fyi/cache_api_error).' diff --git a/packages/cache/src/headers.ts b/packages/cache/src/headers.ts index 00dfc855..828b5b34 100644 --- a/packages/cache/src/headers.ts +++ b/packages/cache/src/headers.ts @@ -3,6 +3,7 @@ export const NetlifyCacheId = 'netlify-cache-id' export const NetlifyCacheTag = 'netlify-cache-tag' export const NetlifyCdnCacheControl = 'netlify-cdn-cache-control' export const NetlifyVary = 'netlify-vary' +export const ErrorDetail = 'netlify-programmable-error' export const ResourceHeaders = 'netlify-programmable-headers' export const ResourceStatus = 'netlify-programmable-status' export const ResourceStore = 'netlify-programmable-store' From 388e8270ccf19f9ae496432a559af58959cc2a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Wed, 12 Mar 2025 17:28:56 +0000 Subject: [PATCH 2/2] chore: update test --- packages/cache/src/bootstrap/cache.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/cache/src/bootstrap/cache.test.ts b/packages/cache/src/bootstrap/cache.test.ts index 2b274186..94ccb502 100644 --- a/packages/cache/src/bootstrap/cache.test.ts +++ b/packages/cache/src/bootstrap/cache.test.ts @@ -216,9 +216,7 @@ describe('Cache API', () => { }) const cache = new NetlifyCache({ base64Encode, - getHost: () => host, - getToken: () => token, - getURL: () => url, + getContext: () => ({ host, token, url }), name: 'my-cache', userAgent, })