-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
121 lines (112 loc) · 4.19 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
'use strict'
import crypto from 'crypto'
import onEnd from 'on-http-end'
/**
* Creates a middleware function that implements idempotency based on a request-specific key
* (commonly referred to as an 'idempotency key' or 'request id').
*
* This pattern is especially useful for ensuring that retrying the same request (due to network
* issues or client-side retries) does not produce duplicate side effects on the server (such as
* creating the same resource multiple times).
*
* @param {Object} options - Configuration options for the middleware.
* @param {Object} options.cache - A cache instance that supports `.get(key)` and `.set(key, value, { ttl })` methods.
* @param {number} options.ttl - Time-to-live in milliseconds for cached responses.
* @param {string} [options.idempotencyKeyExtractor] - A function that extracts the idempotency key from the request object.
* @param {Object} [options.logger=console] - A logger object with `.error()` and possibly other methods for logging.
*
* @returns {Function} Connect-style middleware function `(req, res, next)`.
*
* @throws {Error} If `cache` or `ttl` is not provided.
*/
export function idempotencyMiddleware({
cache,
ttl,
idempotencyKeyExtractor = (req) => req.headers['x-request-id'],
logger = console,
}) {
// Validate the mandatory parameters
if (
!cache ||
typeof cache.get !== 'function' ||
typeof cache.set !== 'function'
) {
throw new Error(
'IdempotencyMiddleware: A valid cache instance with .get and .set methods is required.',
)
}
if (typeof ttl !== 'number' || ttl <= 0) {
throw new Error(
'IdempotencyMiddleware: A positive numeric ttl (in milliseconds) is required.',
)
}
return function (req, res, next) {
if (
req.method === 'POST' ||
req.method === 'PUT' ||
req.method === 'PATCH' ||
req.method === 'DELETE'
) {
let idempotencyKey = idempotencyKeyExtractor(req)
// If no idempotency key is found, there's no special handling needed
if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0) {
return next()
}
// Hash the idempotency key to ensure it's a valid cache key
idempotencyKey = 'idemp-key-' + hashSha256(idempotencyKey)
// Attempt to retrieve a cached response
cache
.get(idempotencyKey)
.then(function (cachedResponse) {
if (cachedResponse) {
res.statusCode = 204
res.setHeader('X-Idempotency-Status', 'hit')
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end('') // Ensuring protocol consistency
} else {
// No cached response found: set up a post-response hook
onEnd(res, function (payload) {
// Only cache the response if it's a success (2xx)
if (
payload.status >= 200 &&
payload.status < 300 &&
typeof idempotencyKey === 'string'
) {
// Store a simple flag or a derived response as needed.
// Here we store `true` to indicate a successful processed request.
// If you need to store the actual payload, store `payload.body` instead.
cache
.set(idempotencyKey, '1', {ttl: ttl})
.catch(function (err) {
logger.error(
'IdempotencyMiddleware - Cache WRITE Error:',
err,
)
})
}
})
// Proceed to the next handler in the chain
next()
}
})
.catch(function (error) {
// If there's an error reading from the cache, log and proceed without caching
logger.error('IdempotencyMiddleware - Cache READ Error:', error)
next()
})
} else {
// For non-idempotent methods, proceed without special handling
next()
}
}
}
/**
* Generates a SHA-256 hash of the provided string.
*
* @param {string} str - The string to hash.
*
* @returns {string} The SHA-256 hash of the string.
*/
export function hashSha256(str) {
return crypto.createHash('sha256').update(str).digest('hex')
}