diff --git a/packages/cache/src/bootstrap/cache.test.ts b/packages/cache/src/bootstrap/cache.test.ts index 36ddf102..94ccb502 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' @@ -196,5 +197,44 @@ 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, + getContext: () => ({ host, token, 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 917026ef..b71f0e97 100644 --- a/packages/cache/src/bootstrap/cache.ts +++ b/packages/cache/src/bootstrap/cache.ts @@ -1,5 +1,6 @@ import type { Base64Encoder, EnvironmentOptions, RequestContext, RequestContextFactory } from './environment.js' +import { ERROR_CODES, GENERIC_ERROR } from './errors.js' import * as HEADERS from '../headers.js' const allowedProtocols = new Set(['http:', 'https:']) @@ -146,7 +147,7 @@ export class NetlifyCache implements Cache { const context = this.#getContext() const resourceURL = extractAndValidateURL(request) - await fetch(`${context.url}/${toCacheKey(resourceURL)}`, { + const cacheResponse = await fetch(`${context.url}/${toCacheKey(resourceURL)}`, { body: response.body, headers: { ...this[getInternalHeaders](context), @@ -157,6 +158,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'