Skip to content

Commit b701148

Browse files
authored
feat: make POST requests cacheable (#24)
1 parent 0ab63e8 commit b701148

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

src/http-data-source.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,13 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
140140
request: Request,
141141
response: Response<TResult>,
142142
): boolean {
143-
return statusCodeCacheableByDefault.has(response.statusCode) && request.method === 'GET'
143+
return statusCodeCacheableByDefault.has(response.statusCode) && this.isRequestCacheable(request)
144+
}
145+
146+
protected isRequestCacheable(request: Request): boolean {
147+
// default behaviour is to cache only get requests
148+
// If extending to non GET requests take care to provide an adequate onCacheKeyCalculation and isResponseCacheable
149+
return request.method === 'GET'
144150
}
145151

146152
/**
@@ -201,7 +207,7 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
201207
* Execute a HTTP GET request.
202208
* Note that the **memoizedResults** and **cache** will be checked before request is made.
203209
* By default the received response will be memoized.
204-
*
210+
*
205211
* @param path the path to the resource
206212
* @param requestOptions
207213
*/
@@ -380,9 +386,11 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
380386

381387
const cacheKey = this.onCacheKeyCalculation(request)
382388

383-
// check if we have any GET call in the cache to respond immediately
384-
if (request.method === 'GET') {
385-
// Memoize GET calls for the same data source instance
389+
const isRequestMemoizable = this.isRequestMemoizable(request)
390+
391+
// check if we have a memoizable call in the cache to respond immediately
392+
if (isRequestMemoizable) {
393+
// Memoize calls for the same data source instance
386394
// a single instance of the data sources is scoped to one graphql request
387395
if (this.memoizedResults.has(cacheKey)) {
388396
const response = await this.memoizedResults.get(cacheKey)!
@@ -403,7 +411,9 @@ export abstract class HTTPDataSource<TContext = any> extends DataSource {
403411
headers,
404412
}
405413

406-
if (options.method === 'GET') {
414+
const requestIsCacheable = this.isRequestCacheable(request)
415+
416+
if (requestIsCacheable) {
407417
// try to fetch from shared cache
408418
if (request.requestCache) {
409419
try {

test/http-data-source.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1310,7 +1310,7 @@ test('Should respond with stale-if-error cache on origin error', async (t) => {
13101310
t.is(cacheMap.size, 1)
13111311
})
13121312

1313-
test('Should not cache POST requests', async (t) => {
1313+
test('Should not cache POST requests by default', async (t) => {
13141314
t.plan(6)
13151315

13161316
const path = '/'
@@ -1481,6 +1481,76 @@ test('Should only cache GET successful responses with the correct status code',
14811481
t.is(cacheMap.size, 0)
14821482
})
14831483

1484+
test('Should cache POST successful responses if isRequestCacheable allows to do so', async (t) => {
1485+
t.plan(7)
1486+
1487+
const path = '/custom/cacheable/post/route'
1488+
1489+
const wanted = { name: 'foo' }
1490+
const server = http.createServer((req, res) => {
1491+
t.is(req.method, 'POST')
1492+
res.writeHead(200, {
1493+
'content-type': 'application/json',
1494+
})
1495+
res.write(JSON.stringify(wanted))
1496+
res.end()
1497+
res.socket?.unref()
1498+
})
1499+
1500+
t.teardown(server.close.bind(server))
1501+
1502+
server.listen()
1503+
1504+
const baseURL = `http://localhost:${(server.address() as AddressInfo)?.port}`
1505+
1506+
const dataSource = new (class extends HTTPDataSource {
1507+
constructor() {
1508+
super(baseURL)
1509+
}
1510+
protected isRequestCacheable(request: Request): boolean {
1511+
return request.method === 'GET' || (request.method === 'POST' && request.path === path)
1512+
}
1513+
postFoo() {
1514+
return this.post(path, {
1515+
body: wanted,
1516+
requestCache: {
1517+
maxTtl: 10,
1518+
maxTtlIfError: 20,
1519+
},
1520+
})
1521+
}
1522+
})()
1523+
1524+
const cacheMap = new Map<string, string>()
1525+
1526+
dataSource.initialize({
1527+
context: {
1528+
a: 1,
1529+
},
1530+
cache: {
1531+
async delete(key: string) {
1532+
return cacheMap.delete(key)
1533+
},
1534+
async get(key: string) {
1535+
return cacheMap.get(key)
1536+
},
1537+
async set(key: string, value: string) {
1538+
cacheMap.set(key, value)
1539+
},
1540+
},
1541+
})
1542+
1543+
let response = await dataSource.postFoo()
1544+
t.deepEqual(response.body, wanted)
1545+
t.false(response.memoized)
1546+
t.false(response.isFromCache)
1547+
1548+
response = await dataSource.postFoo()
1549+
t.deepEqual(response.body, wanted)
1550+
t.false(response.memoized)
1551+
t.true(response.isFromCache)
1552+
})
1553+
14841554
test.serial('Global maxAge should be used when no maxAge was set or similar.', async (t) => {
14851555
const path = '/'
14861556

0 commit comments

Comments
 (0)