@fastify/caching 9.0.3 → 9.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,52 @@ version: 2
2
2
  updates:
3
3
  - package-ecosystem: "github-actions"
4
4
  directory: "/"
5
+ commit-message:
6
+ # Prefix all commit messages with "chore: "
7
+ prefix: "chore"
5
8
  schedule:
6
- interval: "monthly"
9
+ interval: "weekly"
10
+ cooldown:
11
+ default-days: 7
12
+ allow:
13
+ - dependency-name: "*"
14
+ update-types:
15
+ - "version-update:semver-major"
7
16
  open-pull-requests-limit: 10
8
17
 
9
18
  - package-ecosystem: "npm"
10
19
  directory: "/"
20
+ commit-message:
21
+ # Prefix all commit messages with "chore: "
22
+ prefix: "chore"
11
23
  schedule:
12
24
  interval: "weekly"
25
+ cooldown:
26
+ default-days: 7
27
+ versioning-strategy: "increase-if-necessary"
28
+ allow:
29
+ - dependency-name: "*"
30
+ update-types:
31
+ - "version-update:semver-major"
32
+ ignore:
33
+ # TODO: remove ignore until neostandard support ESLint 10
34
+ - dependency-name: "eslint"
35
+ - dependency-name: "neostandard"
36
+ - dependency-name: "@stylistic/*"
13
37
  open-pull-requests-limit: 10
38
+ groups:
39
+ # Production dependencies with breaking changes
40
+ dependencies:
41
+ dependency-type: "production"
42
+ # ESLint related dependencies
43
+ dev-dependencies-eslint:
44
+ patterns:
45
+ - "eslint"
46
+ - "neostandard"
47
+ - "@stylistic/*"
48
+ # TypeScript related dependencies
49
+ dev-dependencies-typescript:
50
+ patterns:
51
+ - "@types/*"
52
+ - "tstyche"
53
+ - "typescript"
@@ -4,7 +4,6 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
- - master
8
7
  - next
9
8
  - 'v*'
10
9
  paths-ignore:
@@ -15,9 +14,20 @@ on:
15
14
  - 'docs/**'
16
15
  - '*.md'
17
16
 
17
+ # This allows a subsequently queued workflow run to interrupt previous runs
18
+ concurrency:
19
+ group: "${{ github.workflow }}-${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
20
+ cancel-in-progress: true
21
+
22
+ permissions:
23
+ contents: read
24
+
18
25
  jobs:
19
26
  test:
20
- uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
27
+ permissions:
28
+ contents: write
29
+ pull-requests: write
30
+ uses: fastify/workflows/.github/workflows/plugins-ci.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
21
31
  with:
22
32
  license-check: true
23
33
  lint: true
@@ -0,0 +1,19 @@
1
+ name: Lock Threads
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 0 1 * *'
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: lock
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ lock-threads:
16
+ permissions:
17
+ issues: write
18
+ pull-requests: write
19
+ uses: fastify/workflows/.github/workflows/lock-threads.yml@ef591e2186785d5ab36b9fe6a79c7ce2f1d94e57 # v7.0.0
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 James Sumners <james.sumners@gmail.com>
3
+ Copyright (c) 2018-present James Sumners <james.sumners@gmail.com>
4
+ Copyright (c) 2018-present The Fastify team <https://github.com/fastify/fastify#team>
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @fastify/caching
2
2
 
3
- [![CI](https://github.com/fastify/fastify-caching/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/fastify-caching/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/fastify/fastify-caching/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-caching/actions/workflows/ci.yml)
4
4
  [![NPM version](https://img.shields.io/npm/v/@fastify/caching.svg?style=flat)](https://www.npmjs.com/package/@fastify/caching)
5
5
  [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
6
6
 
7
- *@fastify/caching* is a plugin for the [Fastify](http://fastify.dev/) framework
7
+ *@fastify/caching* is a plugin for the [Fastify](https://fastify.dev/) framework
8
8
  that provides server-side caching and mechanisms for manipulating HTTP cache headers according to
9
- [RFC 2616 §14.9](https://tools.ietf.org/html/rfc2616#section-14.9).
9
+ [RFC 2616 §14.9](https://datatracker.ietf.org/doc/html/rfc2616#section-14.9).
10
10
 
11
11
  This plugin fully supports Fastify's encapsulation. Therefore, routes that
12
12
  should have differing cache settings should be registered within different
@@ -29,11 +29,11 @@ npm i @fastify/caching
29
29
  ### Compatibility
30
30
  | Plugin version | Fastify version |
31
31
  | ---------------|-----------------|
32
- | `^9.x` | `^5.x` |
32
+ | `>=9.x` | `^5.x` |
33
33
  | `^8.x` | `^4.x` |
34
- | `^6.x` | `^3.x` |
35
- | `^4.x` | `^2.x` |
36
- | `^1.x` | `^1.x` |
34
+ | `>=6.x <8.x` | `^3.x` |
35
+ | `>=4.x <6.x` | `^2.x` |
36
+ | `>=1.x <4.x` | `^1.x` |
37
37
 
38
38
 
39
39
  Please note that if a Fastify version is out of support, then so are the corresponding versions of this plugin
@@ -161,7 +161,7 @@ This method allows setting of the `expires` header. It accepts a regular `Date`
161
161
  object or a string that is a valid date string according to
162
162
  [RFC 2616 §14.21][sec14.21].
163
163
 
164
- [sec14.21]: https://tools.ietf.org/html/rfc2616#section-14.21
164
+ [sec14.21]: https://datatracker.ietf.org/doc/html/rfc2616#section-14.21
165
165
 
166
166
  ## License
167
167
 
package/index.js CHANGED
@@ -60,7 +60,7 @@ function fastifyCaching (instance, options, next) {
60
60
  if (!_options.cache) _options.cache = abstractCache()
61
61
 
62
62
  if (_options.privacy) {
63
- // https://tools.ietf.org/html/rfc2616#section-14.9.4
63
+ // https://datatracker.ietf.org/doc/html/rfc2616#section-14.9.4
64
64
  let value = _options.privacy
65
65
  if (_options.privacy.toLowerCase() !== 'no-cache' && _options.expiresIn) {
66
66
  value = `${_options.privacy}, max-age=${_options.expiresIn}`
@@ -70,9 +70,16 @@ function fastifyCaching (instance, options, next) {
70
70
  value += `, s-maxage=${_options.serverExpiresIn}`
71
71
  }
72
72
 
73
- instance.addHook('onRequest', function cachingSetCacheControlHeader (_req, res, next) {
74
- if (!res.hasHeader('Cache-control')) {
75
- res.header('Cache-control', value)
73
+ instance.addHook('onSend', function cachingSetCacheControlHeader (_req, reply, _payload, next) {
74
+ if (!reply.hasHeader('Cache-control')) {
75
+ // Only apply the caching value to successful (2xx) responses.
76
+ // Error responses must not be cached, otherwise browsers may serve
77
+ // a stale 4xx/5xx for the duration of max-age.
78
+ if (reply.statusCode >= 200 && reply.statusCode < 300) {
79
+ reply.header('Cache-control', value)
80
+ } else {
81
+ reply.header('Cache-control', 'no-store')
82
+ }
76
83
  }
77
84
  next()
78
85
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/caching",
3
- "version": "9.0.3",
3
+ "version": "9.0.4",
4
4
  "description": "A plugin for Fastify to enable management of cache control headers",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -9,18 +9,14 @@
9
9
  "lint": "eslint",
10
10
  "lint:fix": "eslint --fix",
11
11
  "test": "npm run test:unit && npm run test:typescript",
12
- "test:typescript": "tsd",
12
+ "test:typescript": "tstyche",
13
13
  "test:unit": "c8 --100 node --test",
14
14
  "test:unit:report": "npm run test:unit -- --coverage-report=html",
15
15
  "test:unit:verbose": "npm run test:unit -- -Rspec"
16
16
  },
17
- "precommit": [
18
- "lint",
19
- "test"
20
- ],
21
17
  "repository": {
22
18
  "type": "git",
23
- "url": "git+ssh://git@github.com/fastify/fastify-caching.git"
19
+ "url": "git+https://github.com/fastify/fastify-caching.git"
24
20
  },
25
21
  "keywords": [
26
22
  "fastify",
@@ -63,24 +59,19 @@
63
59
  }
64
60
  ],
65
61
  "devDependencies": {
66
- "@fastify/pre-commit": "^2.1.0",
67
- "@types/node": "^22.0.0",
68
- "c8": "^10.1.2",
62
+ "@types/node": "^26.0.1",
63
+ "c8": "^12.0.0",
69
64
  "eslint": "^9.17.0",
70
65
  "fastify": "^5.0.0",
71
- "neostandard": "^0.12.0",
72
- "tsd": "^0.31.0"
66
+ "neostandard": "^0.13.0",
67
+ "tstyche": "^7.0.0"
73
68
  },
74
69
  "dependencies": {
75
70
  "abstract-cache": "^1.0.1",
76
- "fastify-plugin": "^5.0.0",
71
+ "fastify-plugin": "^6.0.0",
77
72
  "uid-safe": "^2.1.5"
78
73
  },
79
74
  "publishConfig": {
80
75
  "access": "public"
81
- },
82
- "pre-commit": [
83
- "lint",
84
- "test"
85
- ]
76
+ }
86
77
  }
@@ -247,6 +247,105 @@ test('sets no-store with max-age header', async (t) => {
247
247
  )
248
248
  })
249
249
 
250
+ test('does not cache 5xx error responses', async (t) => {
251
+ t.plan(2)
252
+
253
+ const opts = {
254
+ privacy: plugin.privacy.PRIVATE,
255
+ expiresIn: 300
256
+ }
257
+
258
+ const fastify = Fastify()
259
+ await fastify.register(plugin, opts)
260
+
261
+ fastify.get('/', () => {
262
+ throw new Error('kaboom')
263
+ })
264
+
265
+ await fastify.ready()
266
+
267
+ const response = await fastify.inject({
268
+ method: 'GET',
269
+ path: '/'
270
+ })
271
+ t.assert.strictEqual(response.statusCode, 500)
272
+ t.assert.strictEqual(response.headers['cache-control'], 'no-store')
273
+ })
274
+
275
+ test('does not cache 4xx error responses', async (t) => {
276
+ t.plan(2)
277
+
278
+ const opts = {
279
+ privacy: plugin.privacy.PRIVATE,
280
+ expiresIn: 300
281
+ }
282
+
283
+ const fastify = Fastify()
284
+ await fastify.register(plugin, opts)
285
+
286
+ fastify.get('/', (_req, reply) => {
287
+ reply.code(400).send({ error: 'bad request' })
288
+ })
289
+
290
+ await fastify.ready()
291
+
292
+ const response = await fastify.inject({
293
+ method: 'GET',
294
+ path: '/'
295
+ })
296
+ t.assert.strictEqual(response.statusCode, 400)
297
+ t.assert.strictEqual(response.headers['cache-control'], 'no-store')
298
+ })
299
+
300
+ test('does not cache framework 404 responses', async (t) => {
301
+ t.plan(2)
302
+
303
+ const opts = {
304
+ privacy: plugin.privacy.PRIVATE,
305
+ expiresIn: 300
306
+ }
307
+
308
+ const fastify = Fastify()
309
+ await fastify.register(plugin, opts)
310
+
311
+ await fastify.ready()
312
+
313
+ const response = await fastify.inject({
314
+ method: 'GET',
315
+ path: '/does-not-exist'
316
+ })
317
+ t.assert.strictEqual(response.statusCode, 404)
318
+ t.assert.strictEqual(response.headers['cache-control'], 'no-store')
319
+ })
320
+
321
+ test('does not override a Cache-control header set upstream on error responses', async (t) => {
322
+ t.plan(2)
323
+
324
+ const opts = {
325
+ privacy: plugin.privacy.PRIVATE,
326
+ expiresIn: 300
327
+ }
328
+
329
+ const fastify = Fastify()
330
+ fastify.addHook('onRequest', async function presetCacheControl (_req, reply) {
331
+ reply.header('cache-control', 'private, max-age=10')
332
+ })
333
+ await fastify.register(plugin, opts)
334
+
335
+ fastify.get('/', () => {
336
+ throw new Error('kaboom')
337
+ })
338
+
339
+ await fastify.ready()
340
+
341
+ const response = await fastify.inject({
342
+ method: 'GET',
343
+ path: '/'
344
+ })
345
+ t.assert.strictEqual(response.statusCode, 500)
346
+ t.assert.strictEqual(response.headers['cache-control'], 'private, max-age=10')
347
+ })
348
+
250
349
  test('sets the expires header', async (t) => {
251
350
  t.plan(2)
252
351
 
package/types/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- /// <reference types='node' />
2
-
3
1
  import { FastifyPluginCallback } from 'fastify'
4
2
 
5
3
  declare module 'fastify' {
@@ -0,0 +1,68 @@
1
+ import Fastify, { FastifyReply } from 'fastify'
2
+ import { expect } from 'tstyche'
3
+ import fastifyCaching, {
4
+ AbstractCacheCompliantObject,
5
+ FastifyCachingPluginOptions,
6
+ } from '..'
7
+
8
+ const fastify = Fastify({ logger: true })
9
+
10
+ const cachingOptions: FastifyCachingPluginOptions = {
11
+ privacy: fastifyCaching.privacy.PUBLIC,
12
+ expiresIn: 300,
13
+ cacheSegment: 'fastify-caching',
14
+ }
15
+ expect(cachingOptions).type.toBeAssignableTo<FastifyCachingPluginOptions>()
16
+
17
+ fastify.register(fastifyCaching, cachingOptions)
18
+
19
+ expect(fastify.cache).type.toBe<AbstractCacheCompliantObject>()
20
+ expect(fastify.cache.get).type.toBe<AbstractCacheCompliantObject['get']>()
21
+ expect(fastify.cache.set).type.toBe<AbstractCacheCompliantObject['set']>()
22
+ expect(fastify.cacheSegment).type.toBe<string>()
23
+
24
+ fastify.get('/one', async (_request, reply) => {
25
+ expect(reply.etag).type.toBe<(tag?: string, timeToLive?: number) => FastifyReply>()
26
+ expect(reply.expires).type.toBe<(date?: Date) => FastifyReply>()
27
+
28
+ expect(reply.etag('hello', 6000)).type.toBe<FastifyReply>()
29
+ expect(reply.expires(new Date(Date.now() + 6000))).type.toBe<FastifyReply>()
30
+
31
+ return { message: 'one' }
32
+ })
33
+
34
+ fastify.get('/two', async (_request, reply) => {
35
+ expect(
36
+ reply.etag('hello', 6000).expires(new Date(Date.now() + 6000))
37
+ ).type.toBe<FastifyReply>()
38
+
39
+ return { message: 'two' }
40
+ })
41
+
42
+ // We register a new instance that should trigger a typescript error.
43
+ const shouldErrorApp = Fastify({ logger: true })
44
+
45
+ const badCachingOptions = {
46
+ privacy: fastifyCaching.privacy.PRIVATE,
47
+ expiresIn: 'a string instead of a number of second',
48
+ cacheSegment: 'fastify-caching',
49
+ }
50
+
51
+ expect(shouldErrorApp.register).type.not.toBeCallableWith(fastifyCaching, badCachingOptions)
52
+
53
+ fastify.get('/three', async () => {
54
+ expect(fastify.cache.get('well-known')).type.toBeAssignableTo<Promise<unknown>>()
55
+
56
+ expect(fastify.cache.get<string>('well-known')).type.toBe<
57
+ Promise<{ item: string; stored: number; ttl: number; } | null>
58
+ >()
59
+
60
+ const result = fastify.cache.get<string>('well-known', (err, value) => {
61
+ expect(err).type.toBe<unknown>()
62
+ expect(value).type.toBe<{ item: string; stored: number; ttl: number; } | null>()
63
+ })
64
+
65
+ expect(result).type.toBe<void>()
66
+
67
+ return { message: 'two' }
68
+ })
package/.github/stale.yml DELETED
@@ -1,21 +0,0 @@
1
- # Number of days of inactivity before an issue becomes stale
2
- daysUntilStale: 15
3
- # Number of days of inactivity before a stale issue is closed
4
- daysUntilClose: 7
5
- # Issues with these labels will never be considered stale
6
- exemptLabels:
7
- - "discussion"
8
- - "feature request"
9
- - "bug"
10
- - "help wanted"
11
- - "plugin suggestion"
12
- - "good first issue"
13
- # Label to use when marking an issue as stale
14
- staleLabel: stale
15
- # Comment to post when marking an issue as stale. Set to `false` to disable
16
- markComment: >
17
- This issue has been automatically marked as stale because it has not had
18
- recent activity. It will be closed if no further activity occurs. Thank you
19
- for your contributions.
20
- # Comment to post when closing a stale issue. Set to `false` to disable
21
- closeComment: false
@@ -1,69 +0,0 @@
1
- import Fastify, { FastifyReply } from 'fastify'
2
- import { expectAssignable, expectError, expectType } from 'tsd'
3
- import fastifyCaching, {
4
- AbstractCacheCompliantObject,
5
- FastifyCachingPluginOptions,
6
- } from '..'
7
-
8
- const fastify = Fastify({ logger: true })
9
-
10
- const cachingOptions: FastifyCachingPluginOptions = {
11
- privacy: fastifyCaching.privacy.PUBLIC,
12
- expiresIn: 300,
13
- cacheSegment: 'fastify-caching',
14
- }
15
- expectAssignable<FastifyCachingPluginOptions>(cachingOptions)
16
-
17
- fastify.register(fastifyCaching, cachingOptions)
18
-
19
- expectType<AbstractCacheCompliantObject>(fastify.cache)
20
- expectType<AbstractCacheCompliantObject['get']>(fastify.cache.get)
21
- expectType<AbstractCacheCompliantObject['set']>(fastify.cache.set)
22
- expectType<string>(fastify.cacheSegment)
23
- // expectType<number>(fastify.etagMaxLife);
24
-
25
- fastify.get('/one', async (_request, reply) => {
26
- expectType<(tag?: string, timeToLive?: number) => FastifyReply>(reply.etag)
27
- expectType<(date?: Date) => FastifyReply>(reply.expires)
28
-
29
- expectType<FastifyReply>(reply.etag('hello', 6000))
30
- expectType<FastifyReply>(reply.expires(new Date(Date.now() + 6000)))
31
-
32
- return { message: 'one' }
33
- })
34
-
35
- fastify.get('/two', async (_request, reply) => {
36
- expectType<FastifyReply>(
37
- reply.etag('hello', 6000).expires(new Date(Date.now() + 6000))
38
- )
39
-
40
- return { message: 'two' }
41
- })
42
-
43
- // We register a new instance that should trigger a typescript error.
44
- const shouldErrorApp = Fastify({ logger: true })
45
-
46
- const badCachingOptions = {
47
- privacy: fastifyCaching.privacy.PRIVATE,
48
- expiresIn: 'a string instead of a number of second',
49
- cacheSegment: 'fastify-caching',
50
- }
51
-
52
- expectError(shouldErrorApp.register(fastifyCaching, badCachingOptions))
53
-
54
- fastify.get('/three', async () => {
55
- expectAssignable<Promise<unknown>>(
56
- fastify.cache.get('well-known')
57
- )
58
- expectAssignable<Promise<{ item: string; stored: number; ttl: number; } | null>>(
59
- fastify.cache.get<string>('well-known')
60
- )
61
- expectType<void>(
62
- fastify.cache.get<string>('well-known', (err, value) => {
63
- expectType<unknown>(err)
64
- expectAssignable<{ item: string; stored: number; ttl: number; } | null>(value)
65
- })
66
- )
67
-
68
- return { message: 'two' }
69
- })