Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ The handler must return a Web Standard [Response](https://developer.mozilla.org/
| `ctx.query` | `Record<string, string>` | Query string parameters |
| `ctx.request` | `FastifyRequest` | Original Fastify request |
| `ctx.reply` | `FastifyReply` | Original Fastify reply |
| `ctx.abortController` | `AbortController` | AbortController instance |

### Hooks

Expand Down
22 changes: 18 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const fp = require('fastify-plugin')
const { Readable } = require('node:stream')

function createWebRequest (fastifyRequest) {
function createWebRequest (fastifyRequest, abortController) {
const url = new URL(fastifyRequest.url, `http://${fastifyRequest.headers.host}`)
const hasBody = !['GET', 'HEAD'].includes(fastifyRequest.method)
const body = fastifyRequest.body
Expand All @@ -21,7 +21,8 @@ function createWebRequest (fastifyRequest) {
method: fastifyRequest.method,
headers: new Headers(fastifyRequest.headers),
body: webBody,
duplex: webBody ? 'half' : undefined
duplex: webBody ? 'half' : undefined,
signal: abortController.signal
})
}

Expand All @@ -46,28 +47,41 @@ async function fastifyFetch (fastify, options) {
done(null, payload)
})

const abortControllerMap = new WeakMap()
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']
const fetch = {}

for (const method of methods) {
fetch[method] = (path, handler) => {
fastify[method](path, async (request, reply) => {
const webRequest = createWebRequest(request)
const abortController = new AbortController()
const webRequest = createWebRequest(request, abortController)
const ctx = {
log: request.log,
server: fastify,
params: request.params,
query: request.query,
request,
reply
reply,
abortController
}

abortControllerMap.set(request, abortController)

const webResponse = await handler(webRequest, ctx)
await sendWebResponse(reply, webResponse)
})
}
}

async function invalidateWebRequest (request) {
abortControllerMap.get(request)?.abort()
}

for (const hookName of ['onRequestAbort', 'onError', 'onResponse']) {
fastify.addHook(hookName, invalidateWebRequest)
}

fastify.decorate('fetch', fetch)
}

Expand Down
55 changes: 55 additions & 0 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,21 @@ describe('fastify-fetch', async () => {
})
})

test('AbortController accessible via ctx.abortController', async () => {
const fastify = Fastify()
await fastify.register(fastifyFetch)

fastify.fetch.get('/abort-controller', async (request, ctx) => {
assert.ok(ctx.abortController instanceof AbortController)
return new Response('ok')
})

await fastify.inject({
method: 'GET',
url: '/abort-controller'
})
})

test('Fastify request accessible via ctx.request', async () => {
const fastify = Fastify()
await fastify.register(fastifyFetch)
Expand Down Expand Up @@ -396,4 +411,44 @@ describe('fastify-fetch', async () => {
assert.strictEqual(response.statusCode, 500)
assert.ok(response.body.includes('Handler must return a Response object'))
})

test('Request must be aborted after the handler is worked', async () => {
const fastify = Fastify()
await fastify.register(fastifyFetch)

let cachedRequest

fastify.fetch.get('/caching-request', async (request, ctx) => {
cachedRequest = request
return 'response'
})

await fastify.inject({
method: 'GET',
url: '/caching-request'
})

assert.ok(cachedRequest.signal.aborted)
})

test('Request must be aborted after the handler throws an error', async () => {
const fastify = Fastify()
await fastify.register(fastifyFetch)

let cachedRequest

fastify.fetch.get('/throwing-error', async (request, ctx) => {
cachedRequest = request
throw new Error('Unexpected exception raised')
})

try {
await fastify.inject({
method: 'GET',
url: '/throwing-error'
})
} catch {}

assert.ok(cachedRequest.signal.aborted)
})
})