@fastify/fetch 0.1.0
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.
- package/.gitattributes +2 -0
- package/.github/dependabot.yml +53 -0
- package/.github/workflows/ci.yml +33 -0
- package/.github/workflows/lock-threads.yml +19 -0
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/eslint.config.js +3 -0
- package/examples/example.mjs +40 -0
- package/index.js +158 -0
- package/package.json +51 -0
- package/test/index.test.js +496 -0
- package/types/index.d.ts +59 -0
- package/types/index.tst.ts +108 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "github-actions"
|
|
4
|
+
directory: "/"
|
|
5
|
+
commit-message:
|
|
6
|
+
# Prefix all commit messages with "chore: "
|
|
7
|
+
prefix: "chore"
|
|
8
|
+
schedule:
|
|
9
|
+
interval: "weekly"
|
|
10
|
+
cooldown:
|
|
11
|
+
default-days: 7
|
|
12
|
+
allow:
|
|
13
|
+
- dependency-name: "*"
|
|
14
|
+
update-types:
|
|
15
|
+
- "version-update:semver-major"
|
|
16
|
+
open-pull-requests-limit: 10
|
|
17
|
+
|
|
18
|
+
- package-ecosystem: "npm"
|
|
19
|
+
directory: "/"
|
|
20
|
+
commit-message:
|
|
21
|
+
# Prefix all commit messages with "chore: "
|
|
22
|
+
prefix: "chore"
|
|
23
|
+
schedule:
|
|
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/*"
|
|
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"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- next
|
|
8
|
+
- 'v*'
|
|
9
|
+
paths-ignore:
|
|
10
|
+
- 'docs/**'
|
|
11
|
+
- '*.md'
|
|
12
|
+
pull_request:
|
|
13
|
+
paths-ignore:
|
|
14
|
+
- 'docs/**'
|
|
15
|
+
- '*.md'
|
|
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
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
test:
|
|
27
|
+
permissions:
|
|
28
|
+
contents: write
|
|
29
|
+
pull-requests: write
|
|
30
|
+
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v6
|
|
31
|
+
with:
|
|
32
|
+
license-check: true
|
|
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@v6
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present The Fastify team <https://github.com/fastify/fastify#team>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @fastify/fetch
|
|
2
|
+
|
|
3
|
+
[](https://github.com/fastify/fastify-fetch/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@fastify/fetch)
|
|
5
|
+
[](https://github.com/neostandard/neostandard)
|
|
6
|
+
|
|
7
|
+
Handle requests using the Web Standard [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (`Request`/`Response`).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @fastify/fetch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import Fastify from 'fastify'
|
|
19
|
+
import fastifyFetch from '@fastify/fetch'
|
|
20
|
+
|
|
21
|
+
const fastify = Fastify()
|
|
22
|
+
|
|
23
|
+
await fastify.register(fastifyFetch)
|
|
24
|
+
|
|
25
|
+
fastify.fetch.get('/hello', async (request, ctx) => {
|
|
26
|
+
ctx.log.info('handling request')
|
|
27
|
+
return new Response('Hello World')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
fastify.fetch.post('/data', async (request, ctx) => {
|
|
31
|
+
const body = await request.json()
|
|
32
|
+
return Response.json({ received: body })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await fastify.listen({ port: 3000 })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
### Handler Signature
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
fastify.fetch.get(path, [options], handler)
|
|
44
|
+
fastify.fetch.post(path, [options], handler)
|
|
45
|
+
fastify.fetch.put(path, [options], handler)
|
|
46
|
+
fastify.fetch.delete(path, [options], handler)
|
|
47
|
+
fastify.fetch.patch(path, [options], handler)
|
|
48
|
+
fastify.fetch.options(path, [options], handler)
|
|
49
|
+
fastify.fetch.head(path, [options], handler)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The handler receives two arguments:
|
|
53
|
+
|
|
54
|
+
- `request` - Web Standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
|
|
55
|
+
- `ctx` - Context object
|
|
56
|
+
|
|
57
|
+
The handler must return a Web Standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object.
|
|
58
|
+
|
|
59
|
+
### Context Object
|
|
60
|
+
|
|
61
|
+
| Property | Type | Description |
|
|
62
|
+
|----------|------|-------------|
|
|
63
|
+
| `ctx.log` | `FastifyBaseLogger` | Fastify logger instance |
|
|
64
|
+
| `ctx.server` | `FastifyInstance` | Fastify server instance |
|
|
65
|
+
| `ctx.params` | `Record<string, string>` | Route parameters |
|
|
66
|
+
| `ctx.query` | `Record<string, string>` | Query string parameters |
|
|
67
|
+
| `ctx.request` | `FastifyRequest` | Original Fastify request |
|
|
68
|
+
| `ctx.reply` | `FastifyReply` | Original Fastify reply |
|
|
69
|
+
| `ctx.abortController` | `AbortController \| undefined` | AbortController instance when enabled for the route |
|
|
70
|
+
|
|
71
|
+
### Route options
|
|
72
|
+
|
|
73
|
+
The optional `options` object accepts standard Fastify route options plus:
|
|
74
|
+
|
|
75
|
+
| Option | Type | Default | Description |
|
|
76
|
+
|--------|------|---------|-------------|
|
|
77
|
+
| `abortController` | `boolean` | `false` | Create an `AbortController` for the route and expose it as `ctx.abortController`. The Web `Request` signal is aborted when the client aborts the request or when the handler lifecycle ends. |
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
fastify.fetch.get('/long-running', { abortController: true }, async (request, ctx) => {
|
|
81
|
+
const result = await doWork({ signal: request.signal })
|
|
82
|
+
return Response.json(result)
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Hooks
|
|
87
|
+
|
|
88
|
+
Routes registered with `fastify.fetch.*` are standard Fastify routes, so all Fastify hooks are supported:
|
|
89
|
+
|
|
90
|
+
| Hook | Fires |
|
|
91
|
+
|------|-------|
|
|
92
|
+
| `onRequest` | Yes |
|
|
93
|
+
| `preParsing` | Yes |
|
|
94
|
+
| `preValidation` | Yes |
|
|
95
|
+
| `preHandler` | Yes |
|
|
96
|
+
| `onSend` | Yes |
|
|
97
|
+
| `onResponse` | Yes |
|
|
98
|
+
| `onError` | Yes (on errors) |
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
102
|
+
// runs before the fetch handler
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
106
|
+
// runs after the response is sent
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
### JSON Response
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
fastify.fetch.get('/users/:id', async (request, ctx) => {
|
|
116
|
+
const user = await getUser(ctx.params.id)
|
|
117
|
+
return Response.json(user)
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Headers
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
fastify.fetch.get('/data', async (request, ctx) => {
|
|
125
|
+
return new Response('data', {
|
|
126
|
+
headers: {
|
|
127
|
+
'X-Custom-Header': 'value',
|
|
128
|
+
'Cache-Control': 'max-age=3600'
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Custom Status Code
|
|
135
|
+
|
|
136
|
+
```javascript
|
|
137
|
+
fastify.fetch.post('/users', async (request, ctx) => {
|
|
138
|
+
const body = await request.json()
|
|
139
|
+
const user = await createUser(body)
|
|
140
|
+
return Response.json(user, { status: 201 })
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Reading Request Body
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
fastify.fetch.post('/upload', async (request, ctx) => {
|
|
148
|
+
// JSON
|
|
149
|
+
const json = await request.json()
|
|
150
|
+
|
|
151
|
+
// Text
|
|
152
|
+
const text = await request.text()
|
|
153
|
+
|
|
154
|
+
// FormData
|
|
155
|
+
const formData = await request.formData()
|
|
156
|
+
|
|
157
|
+
// ArrayBuffer
|
|
158
|
+
const buffer = await request.arrayBuffer()
|
|
159
|
+
|
|
160
|
+
return new Response('OK')
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Using Query Parameters
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
fastify.fetch.get('/search', async (request, ctx) => {
|
|
168
|
+
const { q, limit } = ctx.query
|
|
169
|
+
const results = await search(q, parseInt(limit))
|
|
170
|
+
return Response.json(results)
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Accessing Request URL
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
fastify.fetch.get('/info', async (request, ctx) => {
|
|
178
|
+
const url = new URL(request.url)
|
|
179
|
+
return Response.json({
|
|
180
|
+
pathname: url.pathname,
|
|
181
|
+
search: url.search
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
Licensed under [MIT](./LICENSE).
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import Fastify from 'fastify'
|
|
2
|
+
import fastifyFetch from '../index.js'
|
|
3
|
+
|
|
4
|
+
const fastify = Fastify({ logger: true })
|
|
5
|
+
|
|
6
|
+
await fastify.register(fastifyFetch)
|
|
7
|
+
|
|
8
|
+
fastify.fetch.get('/hello', async (request, ctx) => {
|
|
9
|
+
ctx.log.info('handling request')
|
|
10
|
+
return new Response('Hello World')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
fastify.fetch.get('/users/:id', async (request, ctx) => {
|
|
14
|
+
return Response.json({ id: ctx.params.id, name: 'John Doe' })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
fastify.fetch.post('/data', async (request, ctx) => {
|
|
18
|
+
const body = await request.json()
|
|
19
|
+
return Response.json({ received: body })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
fastify.fetch.get('/search', async (request, ctx) => {
|
|
23
|
+
return Response.json({ query: ctx.query })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
await fastify.listen({ port: 3000 })
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Test the endpoints using curl:
|
|
30
|
+
*
|
|
31
|
+
* curl http://localhost:3000/hello
|
|
32
|
+
*
|
|
33
|
+
* curl http://localhost:3000/users/42
|
|
34
|
+
*
|
|
35
|
+
* curl -X POST http://localhost:3000/data \
|
|
36
|
+
* -H "Content-Type: application/json" \
|
|
37
|
+
* -d '{"name": "John Doe", "age": 30}'
|
|
38
|
+
*
|
|
39
|
+
* curl "http://localhost:3000/search?q=test&limit=10"
|
|
40
|
+
*/
|
package/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fp = require('fastify-plugin')
|
|
4
|
+
const { Readable } = require('node:stream')
|
|
5
|
+
|
|
6
|
+
const kAbortController = Symbol('fastify-fetch.abortController')
|
|
7
|
+
const requestLifecycleEnded = new DOMException('Request lifecycle ended', 'AbortError')
|
|
8
|
+
|
|
9
|
+
function createWebRequest (fastifyRequest, signal) {
|
|
10
|
+
const url = new URL(fastifyRequest.url, `http://${fastifyRequest.headers.host}`)
|
|
11
|
+
const hasBody = !['GET', 'HEAD'].includes(fastifyRequest.method)
|
|
12
|
+
const body = fastifyRequest.body
|
|
13
|
+
|
|
14
|
+
let webBody
|
|
15
|
+
if (hasBody && body) {
|
|
16
|
+
if (body instanceof Readable) {
|
|
17
|
+
webBody = Readable.toWeb(body)
|
|
18
|
+
} else {
|
|
19
|
+
throw new Error('Request body must be a Readable stream')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (signal === undefined) {
|
|
24
|
+
return new Request(url, {
|
|
25
|
+
method: fastifyRequest.method,
|
|
26
|
+
headers: new Headers(fastifyRequest.headers),
|
|
27
|
+
body: webBody,
|
|
28
|
+
duplex: webBody ? 'half' : undefined
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Request(url, {
|
|
33
|
+
method: fastifyRequest.method,
|
|
34
|
+
headers: new Headers(fastifyRequest.headers),
|
|
35
|
+
body: webBody,
|
|
36
|
+
duplex: webBody ? 'half' : undefined,
|
|
37
|
+
signal
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function sendWebResponse (fastifyReply, webResponse) {
|
|
42
|
+
if (!(webResponse instanceof Response)) {
|
|
43
|
+
throw new Error('Handler must return a Response object')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fastifyReply.status(webResponse.status)
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of webResponse.headers) {
|
|
49
|
+
fastifyReply.header(key, value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const body = await webResponse.arrayBuffer()
|
|
53
|
+
fastifyReply.send(Buffer.from(body))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function abortWebRequest (request) {
|
|
57
|
+
request[kAbortController]?.abort()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function prepareRouteOptions (options, abortControllerEnabled) {
|
|
61
|
+
if (options === undefined) {
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const routeOptions = { ...options }
|
|
66
|
+
delete routeOptions.abortController
|
|
67
|
+
if (!abortControllerEnabled) {
|
|
68
|
+
return routeOptions
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hooks = routeOptions.onRequestAbort
|
|
72
|
+
if (hooks === undefined) {
|
|
73
|
+
routeOptions.onRequestAbort = abortWebRequest
|
|
74
|
+
} else if (Array.isArray(hooks)) {
|
|
75
|
+
routeOptions.onRequestAbort = [abortWebRequest, ...hooks]
|
|
76
|
+
} else {
|
|
77
|
+
routeOptions.onRequestAbort = [abortWebRequest, hooks]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return routeOptions
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function fastifyFetch (fastify, options) {
|
|
84
|
+
fastify.removeAllContentTypeParsers()
|
|
85
|
+
fastify.addContentTypeParser('*', function (request, payload, done) {
|
|
86
|
+
done(null, payload)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const methods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']
|
|
90
|
+
const fetch = {}
|
|
91
|
+
|
|
92
|
+
for (const method of methods) {
|
|
93
|
+
fetch[method] = (path, options, handler) => {
|
|
94
|
+
if (handler === undefined) {
|
|
95
|
+
handler = options
|
|
96
|
+
options = undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const abortControllerEnabled = options?.abortController === true
|
|
100
|
+
const routeOptions = prepareRouteOptions(options, abortControllerEnabled)
|
|
101
|
+
let routeHandler
|
|
102
|
+
|
|
103
|
+
if (abortControllerEnabled) {
|
|
104
|
+
routeHandler = async function handleRequestWithAbortController (request, reply) {
|
|
105
|
+
const abortController = new AbortController()
|
|
106
|
+
request[kAbortController] = abortController
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const webRequest = createWebRequest(request, abortController.signal)
|
|
110
|
+
const ctx = {
|
|
111
|
+
log: request.log,
|
|
112
|
+
server: fastify,
|
|
113
|
+
params: request.params,
|
|
114
|
+
query: request.query,
|
|
115
|
+
request,
|
|
116
|
+
reply,
|
|
117
|
+
abortController
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const webResponse = await handler(webRequest, ctx)
|
|
121
|
+
await sendWebResponse(reply, webResponse)
|
|
122
|
+
} finally {
|
|
123
|
+
delete request[kAbortController]
|
|
124
|
+
abortController.abort(requestLifecycleEnded)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
routeHandler = async function handleRequest (request, reply) {
|
|
129
|
+
const webRequest = createWebRequest(request)
|
|
130
|
+
const ctx = {
|
|
131
|
+
log: request.log,
|
|
132
|
+
server: fastify,
|
|
133
|
+
params: request.params,
|
|
134
|
+
query: request.query,
|
|
135
|
+
request,
|
|
136
|
+
reply
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const webResponse = await handler(webRequest, ctx)
|
|
140
|
+
await sendWebResponse(reply, webResponse)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (routeOptions === undefined) {
|
|
145
|
+
fastify[method](path, routeHandler)
|
|
146
|
+
} else {
|
|
147
|
+
fastify[method](path, routeOptions, routeHandler)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fastify.decorate('fetch', fetch)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = fp(fastifyFetch, {
|
|
156
|
+
fastify: '>=5.0.0',
|
|
157
|
+
name: '@fastify/fetch'
|
|
158
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastify/fetch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Handle requests using the fetch standard",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"types": "types/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"lint": "eslint",
|
|
10
|
+
"lint:fix": "eslint --fix",
|
|
11
|
+
"test": "borp && npm run test:types",
|
|
12
|
+
"test:types": "tstyche"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/fastify/fastify-fetch.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"fastify",
|
|
20
|
+
"fetch",
|
|
21
|
+
"web-standard",
|
|
22
|
+
"request",
|
|
23
|
+
"response"
|
|
24
|
+
],
|
|
25
|
+
"author": "Matteo Collina <hello@matteocollina.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/fastify/fastify-fetch/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/fastify/fastify-fetch",
|
|
31
|
+
"funding": [
|
|
32
|
+
{
|
|
33
|
+
"type": "github",
|
|
34
|
+
"url": "https://github.com/sponsors/fastify"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"type": "opencollective",
|
|
38
|
+
"url": "https://opencollective.com/fastify"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"fastify-plugin": "^6.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^26.0.1",
|
|
46
|
+
"borp": "^1.0.0",
|
|
47
|
+
"fastify": "^5.0.0",
|
|
48
|
+
"neostandard": "^0.13.0",
|
|
49
|
+
"tstyche": "^7.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test, describe } = require('node:test')
|
|
4
|
+
const assert = require('node:assert')
|
|
5
|
+
const Fastify = require('fastify')
|
|
6
|
+
const fastifyFetch = require('../index.js')
|
|
7
|
+
|
|
8
|
+
describe('fastify-fetch', async () => {
|
|
9
|
+
test('GET request returns text response', async () => {
|
|
10
|
+
const fastify = Fastify()
|
|
11
|
+
await fastify.register(fastifyFetch)
|
|
12
|
+
|
|
13
|
+
fastify.fetch.get('/hello', async (request, ctx) => {
|
|
14
|
+
return new Response('Hello World')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const response = await fastify.inject({
|
|
18
|
+
method: 'GET',
|
|
19
|
+
url: '/hello'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
assert.strictEqual(response.statusCode, 200)
|
|
23
|
+
assert.strictEqual(response.body, 'Hello World')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('POST request with JSON body', async () => {
|
|
27
|
+
const fastify = Fastify()
|
|
28
|
+
await fastify.register(fastifyFetch)
|
|
29
|
+
|
|
30
|
+
fastify.fetch.post('/data', async (request, ctx) => {
|
|
31
|
+
const body = await request.json()
|
|
32
|
+
return Response.json({ received: body })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const response = await fastify.inject({
|
|
36
|
+
method: 'POST',
|
|
37
|
+
url: '/data',
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
payload: JSON.stringify({ name: 'test' })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(response.statusCode, 200)
|
|
43
|
+
assert.deepStrictEqual(JSON.parse(response.body), { received: { name: 'test' } })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('Response headers are forwarded', async () => {
|
|
47
|
+
const fastify = Fastify()
|
|
48
|
+
await fastify.register(fastifyFetch)
|
|
49
|
+
|
|
50
|
+
fastify.fetch.get('/headers', async (request, ctx) => {
|
|
51
|
+
return new Response('ok', {
|
|
52
|
+
headers: { 'x-custom-header': 'custom-value' }
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const response = await fastify.inject({
|
|
57
|
+
method: 'GET',
|
|
58
|
+
url: '/headers'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
assert.strictEqual(response.headers['x-custom-header'], 'custom-value')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('Response status codes work', async () => {
|
|
65
|
+
const fastify = Fastify()
|
|
66
|
+
await fastify.register(fastifyFetch)
|
|
67
|
+
|
|
68
|
+
fastify.fetch.get('/created', async (request, ctx) => {
|
|
69
|
+
return new Response('created', { status: 201 })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const response = await fastify.inject({
|
|
73
|
+
method: 'GET',
|
|
74
|
+
url: '/created'
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(response.statusCode, 201)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('Route parameters accessible via ctx.params', async () => {
|
|
81
|
+
const fastify = Fastify()
|
|
82
|
+
await fastify.register(fastifyFetch)
|
|
83
|
+
|
|
84
|
+
fastify.fetch.get('/users/:id', async (request, ctx) => {
|
|
85
|
+
return Response.json({ id: ctx.params.id })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const response = await fastify.inject({
|
|
89
|
+
method: 'GET',
|
|
90
|
+
url: '/users/123'
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
assert.deepStrictEqual(JSON.parse(response.body), { id: '123' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('Query string accessible via ctx.query', async () => {
|
|
97
|
+
const fastify = Fastify()
|
|
98
|
+
await fastify.register(fastifyFetch)
|
|
99
|
+
|
|
100
|
+
fastify.fetch.get('/search', async (request, ctx) => {
|
|
101
|
+
return Response.json({ q: ctx.query.q })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const response = await fastify.inject({
|
|
105
|
+
method: 'GET',
|
|
106
|
+
url: '/search?q=hello'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
assert.deepStrictEqual(JSON.parse(response.body), { q: 'hello' })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('Logger accessible via ctx.log', async () => {
|
|
113
|
+
const fastify = Fastify()
|
|
114
|
+
await fastify.register(fastifyFetch)
|
|
115
|
+
|
|
116
|
+
let logCalled = false
|
|
117
|
+
fastify.fetch.get('/log', async (request, ctx) => {
|
|
118
|
+
assert.ok(ctx.log)
|
|
119
|
+
assert.strictEqual(typeof ctx.log.info, 'function')
|
|
120
|
+
logCalled = true
|
|
121
|
+
return new Response('ok')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await fastify.inject({
|
|
125
|
+
method: 'GET',
|
|
126
|
+
url: '/log'
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
assert.ok(logCalled)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('Server accessible via ctx.server', async () => {
|
|
133
|
+
const fastify = Fastify()
|
|
134
|
+
await fastify.register(fastifyFetch)
|
|
135
|
+
|
|
136
|
+
fastify.fetch.get('/server', async (request, ctx) => {
|
|
137
|
+
assert.strictEqual(ctx.server, fastify)
|
|
138
|
+
return new Response('ok')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
await fastify.inject({
|
|
142
|
+
method: 'GET',
|
|
143
|
+
url: '/server'
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('AbortController is disabled by default', async () => {
|
|
148
|
+
const fastify = Fastify()
|
|
149
|
+
await fastify.register(fastifyFetch)
|
|
150
|
+
|
|
151
|
+
let cachedRequest
|
|
152
|
+
|
|
153
|
+
fastify.fetch.get('/abort-controller-disabled', async (request, ctx) => {
|
|
154
|
+
cachedRequest = request
|
|
155
|
+
assert.strictEqual(ctx.abortController, undefined)
|
|
156
|
+
return new Response('ok')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
await fastify.inject({
|
|
160
|
+
method: 'GET',
|
|
161
|
+
url: '/abort-controller-disabled'
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
assert.strictEqual(cachedRequest.signal.aborted, false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('AbortController accessible via ctx.abortController when enabled', async () => {
|
|
168
|
+
const fastify = Fastify()
|
|
169
|
+
await fastify.register(fastifyFetch)
|
|
170
|
+
|
|
171
|
+
fastify.fetch.get('/abort-controller', { abortController: true }, async (request, ctx) => {
|
|
172
|
+
assert.ok(ctx.abortController instanceof AbortController)
|
|
173
|
+
assert.strictEqual(request.signal, ctx.abortController.signal)
|
|
174
|
+
return new Response('ok')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await fastify.inject({
|
|
178
|
+
method: 'GET',
|
|
179
|
+
url: '/abort-controller'
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('Fastify request accessible via ctx.request', async () => {
|
|
184
|
+
const fastify = Fastify()
|
|
185
|
+
await fastify.register(fastifyFetch)
|
|
186
|
+
|
|
187
|
+
fastify.fetch.get('/fastify-request', async (request, ctx) => {
|
|
188
|
+
assert.ok(ctx.request)
|
|
189
|
+
assert.strictEqual(ctx.request.method, 'GET')
|
|
190
|
+
return new Response('ok')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await fastify.inject({
|
|
194
|
+
method: 'GET',
|
|
195
|
+
url: '/fastify-request'
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('Fastify reply accessible via ctx.reply', async () => {
|
|
200
|
+
const fastify = Fastify()
|
|
201
|
+
await fastify.register(fastifyFetch)
|
|
202
|
+
|
|
203
|
+
fastify.fetch.get('/fastify-reply', async (request, ctx) => {
|
|
204
|
+
assert.ok(ctx.reply)
|
|
205
|
+
assert.strictEqual(typeof ctx.reply.send, 'function')
|
|
206
|
+
return new Response('ok')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
await fastify.inject({
|
|
210
|
+
method: 'GET',
|
|
211
|
+
url: '/fastify-reply'
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('PUT method works', async () => {
|
|
216
|
+
const fastify = Fastify()
|
|
217
|
+
await fastify.register(fastifyFetch)
|
|
218
|
+
|
|
219
|
+
fastify.fetch.put('/update', async (request, ctx) => {
|
|
220
|
+
const body = await request.json()
|
|
221
|
+
return Response.json({ updated: body })
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
const response = await fastify.inject({
|
|
225
|
+
method: 'PUT',
|
|
226
|
+
url: '/update',
|
|
227
|
+
headers: { 'content-type': 'application/json' },
|
|
228
|
+
payload: JSON.stringify({ value: 'new' })
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
assert.deepStrictEqual(JSON.parse(response.body), { updated: { value: 'new' } })
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('DELETE method works', async () => {
|
|
235
|
+
const fastify = Fastify()
|
|
236
|
+
await fastify.register(fastifyFetch)
|
|
237
|
+
|
|
238
|
+
fastify.fetch.delete('/remove/:id', async (request, ctx) => {
|
|
239
|
+
return Response.json({ deleted: ctx.params.id })
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const response = await fastify.inject({
|
|
243
|
+
method: 'DELETE',
|
|
244
|
+
url: '/remove/456'
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
assert.deepStrictEqual(JSON.parse(response.body), { deleted: '456' })
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('PATCH method works', async () => {
|
|
251
|
+
const fastify = Fastify()
|
|
252
|
+
await fastify.register(fastifyFetch)
|
|
253
|
+
|
|
254
|
+
fastify.fetch.patch('/patch', async (request, ctx) => {
|
|
255
|
+
const body = await request.json()
|
|
256
|
+
return Response.json({ patched: body })
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const response = await fastify.inject({
|
|
260
|
+
method: 'PATCH',
|
|
261
|
+
url: '/patch',
|
|
262
|
+
headers: { 'content-type': 'application/json' },
|
|
263
|
+
payload: JSON.stringify({ field: 'patched' })
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
assert.deepStrictEqual(JSON.parse(response.body), { patched: { field: 'patched' } })
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('OPTIONS method works', async () => {
|
|
270
|
+
const fastify = Fastify()
|
|
271
|
+
await fastify.register(fastifyFetch)
|
|
272
|
+
|
|
273
|
+
fastify.fetch.options('/cors', async (request, ctx) => {
|
|
274
|
+
return new Response(null, {
|
|
275
|
+
status: 204,
|
|
276
|
+
headers: { 'access-control-allow-origin': '*' }
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const response = await fastify.inject({
|
|
281
|
+
method: 'OPTIONS',
|
|
282
|
+
url: '/cors'
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
assert.strictEqual(response.statusCode, 204)
|
|
286
|
+
assert.strictEqual(response.headers['access-control-allow-origin'], '*')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('HEAD method works', async () => {
|
|
290
|
+
const fastify = Fastify()
|
|
291
|
+
await fastify.register(fastifyFetch)
|
|
292
|
+
|
|
293
|
+
fastify.fetch.head('/head', async (request, ctx) => {
|
|
294
|
+
return new Response(null, {
|
|
295
|
+
headers: { 'x-custom': 'value' }
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const response = await fastify.inject({
|
|
300
|
+
method: 'HEAD',
|
|
301
|
+
url: '/head'
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
assert.strictEqual(response.statusCode, 200)
|
|
305
|
+
assert.strictEqual(response.headers['x-custom'], 'value')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
test('onRequest hook fires', async () => {
|
|
309
|
+
const fastify = Fastify()
|
|
310
|
+
let hookCalled = false
|
|
311
|
+
|
|
312
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
313
|
+
hookCalled = true
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
await fastify.register(fastifyFetch)
|
|
317
|
+
|
|
318
|
+
fastify.fetch.get('/hook-test', async (request, ctx) => {
|
|
319
|
+
return new Response('ok')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
await fastify.inject({
|
|
323
|
+
method: 'GET',
|
|
324
|
+
url: '/hook-test'
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
assert.strictEqual(hookCalled, true)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('preHandler hook fires', async () => {
|
|
331
|
+
const fastify = Fastify()
|
|
332
|
+
let hookCalled = false
|
|
333
|
+
|
|
334
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
335
|
+
hookCalled = true
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
await fastify.register(fastifyFetch)
|
|
339
|
+
|
|
340
|
+
fastify.fetch.get('/prehandler-test', async (request, ctx) => {
|
|
341
|
+
return new Response('ok')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
await fastify.inject({
|
|
345
|
+
method: 'GET',
|
|
346
|
+
url: '/prehandler-test'
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
assert.strictEqual(hookCalled, true)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test('route options are forwarded', async () => {
|
|
353
|
+
const fastify = Fastify()
|
|
354
|
+
let hookCalled = false
|
|
355
|
+
|
|
356
|
+
await fastify.register(fastifyFetch)
|
|
357
|
+
|
|
358
|
+
fastify.fetch.get('/route-options', {
|
|
359
|
+
preHandler: async (request, reply) => {
|
|
360
|
+
hookCalled = true
|
|
361
|
+
}
|
|
362
|
+
}, async (request, ctx) => {
|
|
363
|
+
return new Response('ok')
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
const response = await fastify.inject({
|
|
367
|
+
method: 'GET',
|
|
368
|
+
url: '/route-options'
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
assert.strictEqual(response.statusCode, 200)
|
|
372
|
+
assert.strictEqual(hookCalled, true)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('onSend hook fires', async () => {
|
|
376
|
+
const fastify = Fastify()
|
|
377
|
+
let hookCalled = false
|
|
378
|
+
|
|
379
|
+
fastify.addHook('onSend', async (request, reply, payload) => {
|
|
380
|
+
hookCalled = true
|
|
381
|
+
return payload
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
await fastify.register(fastifyFetch)
|
|
385
|
+
|
|
386
|
+
fastify.fetch.get('/onsend-test', async (request, ctx) => {
|
|
387
|
+
return new Response('ok')
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
await fastify.inject({
|
|
391
|
+
method: 'GET',
|
|
392
|
+
url: '/onsend-test'
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
assert.strictEqual(hookCalled, true)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('onResponse hook fires', async () => {
|
|
399
|
+
const fastify = Fastify()
|
|
400
|
+
let hookCalled = false
|
|
401
|
+
|
|
402
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
403
|
+
hookCalled = true
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
await fastify.register(fastifyFetch)
|
|
407
|
+
|
|
408
|
+
fastify.fetch.get('/onresponse-test', async (request, ctx) => {
|
|
409
|
+
return new Response('ok')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
await fastify.inject({
|
|
413
|
+
method: 'GET',
|
|
414
|
+
url: '/onresponse-test'
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
assert.strictEqual(hookCalled, true)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
test('onError hook fires on handler error', async () => {
|
|
421
|
+
const fastify = Fastify()
|
|
422
|
+
let hookCalled = false
|
|
423
|
+
|
|
424
|
+
fastify.addHook('onError', async (request, reply, error) => {
|
|
425
|
+
hookCalled = true
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
await fastify.register(fastifyFetch)
|
|
429
|
+
|
|
430
|
+
fastify.fetch.get('/onerror-test', async (request, ctx) => {
|
|
431
|
+
throw new Error('test error')
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
await fastify.inject({
|
|
435
|
+
method: 'GET',
|
|
436
|
+
url: '/onerror-test'
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
assert.strictEqual(hookCalled, true)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
test('handler must return Response object', async () => {
|
|
443
|
+
const fastify = Fastify()
|
|
444
|
+
await fastify.register(fastifyFetch)
|
|
445
|
+
|
|
446
|
+
fastify.fetch.get('/invalid-response', async (request, ctx) => {
|
|
447
|
+
return 'not a response'
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
const response = await fastify.inject({
|
|
451
|
+
method: 'GET',
|
|
452
|
+
url: '/invalid-response'
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
assert.strictEqual(response.statusCode, 500)
|
|
456
|
+
assert.ok(response.body.includes('Handler must return a Response object'))
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('Request must be aborted after the handler completes when enabled', async () => {
|
|
460
|
+
const fastify = Fastify()
|
|
461
|
+
await fastify.register(fastifyFetch)
|
|
462
|
+
|
|
463
|
+
let cachedRequest
|
|
464
|
+
|
|
465
|
+
fastify.fetch.get('/caching-request', { abortController: true }, async (request, ctx) => {
|
|
466
|
+
cachedRequest = request
|
|
467
|
+
return new Response('response')
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
await fastify.inject({
|
|
471
|
+
method: 'GET',
|
|
472
|
+
url: '/caching-request'
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
assert.ok(cachedRequest.signal.aborted)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
test('Request must be aborted after the handler throws an error when enabled', async () => {
|
|
479
|
+
const fastify = Fastify()
|
|
480
|
+
await fastify.register(fastifyFetch)
|
|
481
|
+
|
|
482
|
+
let cachedRequest
|
|
483
|
+
|
|
484
|
+
fastify.fetch.get('/throwing-error', { abortController: true }, async (request, ctx) => {
|
|
485
|
+
cachedRequest = request
|
|
486
|
+
throw new Error('Unexpected exception raised')
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
await fastify.inject({
|
|
490
|
+
method: 'GET',
|
|
491
|
+
url: '/throwing-error'
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
assert.ok(cachedRequest.signal.aborted)
|
|
495
|
+
})
|
|
496
|
+
})
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FastifyPluginAsync, FastifyInstance, FastifyBaseLogger, FastifyRequest, FastifyReply, RouteShorthandOptions } from 'fastify'
|
|
2
|
+
|
|
3
|
+
declare module 'fastify' {
|
|
4
|
+
interface FastifyInstance {
|
|
5
|
+
fetch: fastifyFetch.FetchRoutes
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type FastifyFetchPlugin = FastifyPluginAsync<NonNullable<fastifyFetch.FastifyFetchOptions>>
|
|
10
|
+
|
|
11
|
+
declare namespace fastifyFetch {
|
|
12
|
+
export interface FetchContext {
|
|
13
|
+
log: FastifyBaseLogger
|
|
14
|
+
server: FastifyInstance
|
|
15
|
+
params: Record<string, string>
|
|
16
|
+
query: Record<string, string>
|
|
17
|
+
request: FastifyRequest
|
|
18
|
+
reply: FastifyReply
|
|
19
|
+
abortController?: AbortController
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FetchContextWithAbortController extends FetchContext {
|
|
23
|
+
abortController: AbortController
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FetchRouteOptions extends RouteShorthandOptions {
|
|
27
|
+
abortController?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type FetchHandler = (request: Request, ctx: FetchContext) => Response | Promise<Response>
|
|
31
|
+
export type FetchHandlerWithAbortController = (request: Request, ctx: FetchContextWithAbortController) => Response | Promise<Response>
|
|
32
|
+
|
|
33
|
+
export interface FetchRoute {
|
|
34
|
+
(path: string, handler: FetchHandler): void
|
|
35
|
+
(path: string, options: FetchRouteOptions & { abortController: true }, handler: FetchHandlerWithAbortController): void
|
|
36
|
+
(path: string, options: FetchRouteOptions, handler: FetchHandler): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FetchRoutes {
|
|
40
|
+
get: FetchRoute
|
|
41
|
+
post: FetchRoute
|
|
42
|
+
put: FetchRoute
|
|
43
|
+
delete: FetchRoute
|
|
44
|
+
patch: FetchRoute
|
|
45
|
+
options: FetchRoute
|
|
46
|
+
head: FetchRoute
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FastifyFetchOptions {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const fastifyFetch: FastifyFetchPlugin
|
|
53
|
+
|
|
54
|
+
export { fastifyFetch as default }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare function fastifyFetch (...params: Parameters<FastifyFetchPlugin>): ReturnType<FastifyFetchPlugin>
|
|
58
|
+
|
|
59
|
+
export = fastifyFetch
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fastify, {
|
|
2
|
+
FastifyInstance,
|
|
3
|
+
FastifyPluginAsync,
|
|
4
|
+
FastifyBaseLogger,
|
|
5
|
+
FastifyRequest,
|
|
6
|
+
FastifyReply
|
|
7
|
+
} from 'fastify'
|
|
8
|
+
import { expect } from 'tstyche'
|
|
9
|
+
import * as fastifyFetchStar from '..'
|
|
10
|
+
import fastifyFetch, {
|
|
11
|
+
FetchContext,
|
|
12
|
+
FetchContextWithAbortController,
|
|
13
|
+
FetchHandler,
|
|
14
|
+
FetchHandlerWithAbortController,
|
|
15
|
+
FetchRouteOptions,
|
|
16
|
+
FetchRoutes,
|
|
17
|
+
fastifyFetch as fastifyFetchNamed
|
|
18
|
+
} from '..'
|
|
19
|
+
|
|
20
|
+
import fastifyFetchCjsImport = require('..')
|
|
21
|
+
const fastifyFetchCjs = require('../..')
|
|
22
|
+
|
|
23
|
+
const app: FastifyInstance = fastify()
|
|
24
|
+
|
|
25
|
+
app.register(fastifyFetch)
|
|
26
|
+
app.register(fastifyFetchNamed)
|
|
27
|
+
app.register(fastifyFetchCjs)
|
|
28
|
+
app.register(fastifyFetchCjsImport.default)
|
|
29
|
+
app.register(fastifyFetchCjsImport.fastifyFetch)
|
|
30
|
+
app.register(fastifyFetchStar.default)
|
|
31
|
+
app.register(fastifyFetchStar.fastifyFetch)
|
|
32
|
+
|
|
33
|
+
expect(fastifyFetch).type.toBe<FastifyPluginAsync>()
|
|
34
|
+
expect(fastifyFetchNamed).type.toBe<FastifyPluginAsync>()
|
|
35
|
+
expect(fastifyFetchCjs).type.toBe<any>()
|
|
36
|
+
|
|
37
|
+
app.register(fastifyFetch).after(() => {
|
|
38
|
+
expect(app.fetch).type.toBe<FetchRoutes>()
|
|
39
|
+
|
|
40
|
+
app.fetch.get('/test', async (request, ctx) => {
|
|
41
|
+
expect(request).type.toBe<Request>()
|
|
42
|
+
expect(ctx).type.toBe<FetchContext>()
|
|
43
|
+
expect(ctx.log).type.toBe<FastifyBaseLogger>()
|
|
44
|
+
expect(ctx.server).type.toBe<FastifyInstance>()
|
|
45
|
+
expect(ctx.params).type.toBe<Record<string, string>>()
|
|
46
|
+
expect(ctx.query).type.toBe<Record<string, string>>()
|
|
47
|
+
expect(ctx.request).type.toBe<FastifyRequest>()
|
|
48
|
+
expect(ctx.reply).type.toBe<FastifyReply>()
|
|
49
|
+
expect(ctx.abortController).type.toBe<AbortController | undefined>()
|
|
50
|
+
return new Response('ok')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
app.fetch.get('/abort', { abortController: true }, async (request, ctx) => {
|
|
54
|
+
expect(request).type.toBe<Request>()
|
|
55
|
+
expect(ctx).type.toBe<FetchContextWithAbortController>()
|
|
56
|
+
expect(ctx.abortController).type.toBe<AbortController>()
|
|
57
|
+
return new Response('ok')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
app.fetch.post('/data', async (request, ctx) => {
|
|
61
|
+
return Response.json({ ok: true })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
app.fetch.put('/update', async (request, ctx) => {
|
|
65
|
+
return new Response('updated')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
app.fetch.delete('/remove', async (request, ctx) => {
|
|
69
|
+
return new Response('deleted')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
app.fetch.patch('/patch', async (request, ctx) => {
|
|
73
|
+
return new Response('patched')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
app.fetch.options('/options', async (request, ctx) => {
|
|
77
|
+
return new Response(null, { status: 204 })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
app.fetch.head('/head', async (request, ctx) => {
|
|
81
|
+
return new Response(null)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const handler: FetchHandler = async (request, ctx) => {
|
|
86
|
+
return new Response('handler')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
expect(handler).type.toBe<FetchHandler>()
|
|
90
|
+
|
|
91
|
+
const handlerWithAbortController: FetchHandlerWithAbortController = async (request, ctx) => {
|
|
92
|
+
expect(ctx.abortController).type.toBe<AbortController>()
|
|
93
|
+
return new Response('handler')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
expect(handlerWithAbortController).type.toBe<FetchHandlerWithAbortController>()
|
|
97
|
+
|
|
98
|
+
const options: FetchRouteOptions = {
|
|
99
|
+
abortController: true,
|
|
100
|
+
schema: {
|
|
101
|
+
response: {
|
|
102
|
+
200: {
|
|
103
|
+
type: 'string'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
expect(options).type.toBe<FetchRouteOptions>()
|