@fastify/reply-from 12.3.1 → 12.5.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/.github/workflows/ci.yml +5 -0
- package/CLAUDE.md +66 -0
- package/README.md +36 -10
- package/index.js +4 -2
- package/lib/utils.js +12 -1
- package/package.json +2 -7
- package/test/build-url.test.js +32 -0
- package/test/fix-GHSA-2q7r-29rg-6m5h.test.js +32 -0
- package/test/full-querystring-url.test.js +55 -0
- package/test/http2-te-header-not-stripped-if-trailing.test.js +60 -0
- package/test/multipart-custom-parser.test.js +74 -0
- package/test/text-event-stream-custom-parser.test.js +68 -0
- package/.claude/settings.local.json +0 -9
package/.github/workflows/ci.yml
CHANGED
|
@@ -14,6 +14,11 @@ on:
|
|
|
14
14
|
- 'docs/**'
|
|
15
15
|
- '*.md'
|
|
16
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
|
+
|
|
17
22
|
permissions:
|
|
18
23
|
contents: read
|
|
19
24
|
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Commands
|
|
6
|
+
|
|
7
|
+
### Testing
|
|
8
|
+
- `npm test` - Run all tests (unit + TypeScript)
|
|
9
|
+
- `npm run test:unit` - Run unit tests with coverage using c8 and Node.js test runner
|
|
10
|
+
- `npm run test:typescript` - Run TypeScript definition tests using tsd
|
|
11
|
+
|
|
12
|
+
### Linting
|
|
13
|
+
- `npm run lint` - Run ESLint to check code style
|
|
14
|
+
- `npm run lint:fix` - Run ESLint with automatic fixes
|
|
15
|
+
- ESLint configuration uses neostandard with TypeScript support
|
|
16
|
+
|
|
17
|
+
### Building
|
|
18
|
+
- No build step required - this is a CommonJS library
|
|
19
|
+
|
|
20
|
+
## Architecture Overview
|
|
21
|
+
|
|
22
|
+
This is a Fastify plugin for proxying HTTP requests to upstream servers. The plugin decorates the Fastify reply with a `from()` method.
|
|
23
|
+
|
|
24
|
+
### Key Components
|
|
25
|
+
|
|
26
|
+
**Main Plugin (`index.js`)**
|
|
27
|
+
- Entry point that registers the plugin using fastify-plugin
|
|
28
|
+
- Decorates `reply.from(source, opts)` method
|
|
29
|
+
- Handles request proxying, retries, caching, and error handling
|
|
30
|
+
- Supports HTTP/1.1, HTTP/2, and Undici client strategies
|
|
31
|
+
|
|
32
|
+
**Request Builder (`lib/request.js`)**
|
|
33
|
+
- Factory for creating request handlers based on transport (HTTP/1.1, HTTP/2, Undici)
|
|
34
|
+
- Handles different agent configurations (HTTP, HTTPS, Unix sockets)
|
|
35
|
+
- Manages timeouts, connection pooling, and retry logic
|
|
36
|
+
- Returns unified interface: `{ request, close, retryOnError }`
|
|
37
|
+
|
|
38
|
+
**Utilities (`lib/utils.js`)**
|
|
39
|
+
- `filterPseudoHeaders()` - Removes HTTP/2 pseudo-headers (starting with :)
|
|
40
|
+
- `copyHeaders()` - Copies headers to Fastify reply
|
|
41
|
+
- `stripHttp1ConnectionHeaders()` - Removes HTTP/1 connection-specific headers for HTTP/2
|
|
42
|
+
- `buildURL()` - Constructs destination URLs with base path validation
|
|
43
|
+
|
|
44
|
+
**Error Definitions (`lib/errors.js`)**
|
|
45
|
+
- Standardized error types using @fastify/error
|
|
46
|
+
- Maps different error conditions to appropriate HTTP status codes
|
|
47
|
+
- Includes timeout, connection reset, and gateway errors
|
|
48
|
+
|
|
49
|
+
### Transport Strategies
|
|
50
|
+
|
|
51
|
+
1. **Undici (default)** - High-performance HTTP/1.1 client with connection pooling
|
|
52
|
+
2. **HTTP/2** - Native Node.js HTTP/2 client for modern protocols
|
|
53
|
+
3. **HTTP/1.1** - Node.js built-in http/https modules with custom agents
|
|
54
|
+
|
|
55
|
+
### Key Features
|
|
56
|
+
|
|
57
|
+
- **Caching**: URL parsing cache using toad-cache LruMap (default 100 entries)
|
|
58
|
+
- **Retries**: Configurable retry logic for socket hangups and 503 errors
|
|
59
|
+
- **Headers**: Smart header rewriting for HTTP/1.1 ↔ HTTP/2 compatibility
|
|
60
|
+
- **Body handling**: Supports streams, JSON encoding, and custom content types
|
|
61
|
+
- **Unix sockets**: Special handling for unix+http: and unix+https: protocols
|
|
62
|
+
- **Timeouts**: Per-request and session-level timeout configuration
|
|
63
|
+
|
|
64
|
+
### Configuration
|
|
65
|
+
|
|
66
|
+
The plugin accepts extensive configuration for different transport methods, retry behavior, caching, and request/response transformation hooks. See README.md for full option documentation.
|
package/README.md
CHANGED
|
@@ -13,9 +13,6 @@ HTTP2 to HTTP is supported too.
|
|
|
13
13
|
npm i @fastify/reply-from
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
## Compatibility with @fastify/multipart
|
|
17
|
-
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins that have a parent-child relationship.`<br>` The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.`<br>` This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.`<br>`
|
|
18
|
-
However, the two plugins may be used within the same fastify instance, at the condition that they belong to disjoint branches of the fastify plugins hierarchy tree.
|
|
19
16
|
|
|
20
17
|
## Usage
|
|
21
18
|
|
|
@@ -518,14 +515,43 @@ This library has:
|
|
|
518
515
|
When a timeout happens, [`504 Gateway Timeout`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)
|
|
519
516
|
will be returned to the client.
|
|
520
517
|
|
|
521
|
-
##
|
|
518
|
+
## Compatibility with @fastify/multipart
|
|
519
|
+
|
|
520
|
+
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins that have a parent-child relationship.
|
|
521
|
+
|
|
522
|
+
The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.
|
|
523
|
+
This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.
|
|
524
|
+
|
|
525
|
+
However, the two plugins may be used within the same fastify instance, at the condition that they belong to disjoint branches of the fastify plugins hierarchy tree.
|
|
526
|
+
|
|
527
|
+
### Proxying specific content types without parsing
|
|
528
|
+
|
|
529
|
+
If you need to proxy certain content types (like `multipart/form-data` or `text/event-stream`) without parsing them, you can use custom content type parsers:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
// Register custom content type parsers that pass raw body through
|
|
533
|
+
fastify.addContentTypeParser('multipart/form-data', function (req, body, done) {
|
|
534
|
+
done(null, body)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
fastify.addContentTypeParser('text/event-stream', function (req, body, done) {
|
|
538
|
+
done(null, body)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
fastify.register(require('@fastify/reply-from'))
|
|
542
|
+
|
|
543
|
+
fastify.post('/upload', (request, reply) => {
|
|
544
|
+
// The multipart data will be proxied as-is to the upstream server
|
|
545
|
+
reply.from('http://upstream-server.com/upload')
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
fastify.post('/events', (request, reply) => {
|
|
549
|
+
// The SSE data will be proxied as-is to the upstream server
|
|
550
|
+
reply.from('http://upstream-server.com/events')
|
|
551
|
+
})
|
|
552
|
+
```
|
|
522
553
|
|
|
523
|
-
|
|
524
|
-
* [ ] forward the request id to the other peer might require some
|
|
525
|
-
refactoring because we have to make the `req.id` unique
|
|
526
|
-
(see [hyperid](https://npm.im/hyperid)).
|
|
527
|
-
* [ ] Support origin HTTP2 push
|
|
528
|
-
* [X] benchmarks
|
|
554
|
+
This approach allows these content types to be proxied correctly while avoiding parsing that would consume the request body.
|
|
529
555
|
|
|
530
556
|
## License
|
|
531
557
|
|
package/index.js
CHANGED
|
@@ -71,7 +71,9 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
|
|
|
71
71
|
const retryDelay = opts.retryDelay || undefined
|
|
72
72
|
|
|
73
73
|
if (!source) {
|
|
74
|
-
|
|
74
|
+
const requestUrl = req.url
|
|
75
|
+
const queryIndex = requestUrl.indexOf('?')
|
|
76
|
+
source = queryIndex >= 0 ? requestUrl.substring(0, queryIndex) : requestUrl
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
// we leverage caching to avoid parsing the destination URL
|
|
@@ -285,7 +287,7 @@ function onErrorDefault (reply, { error }) {
|
|
|
285
287
|
}
|
|
286
288
|
|
|
287
289
|
function isFastifyMultipartRegistered (fastify) {
|
|
288
|
-
return fastify.
|
|
290
|
+
return fastify.hasPlugin('@fastify/multipart')
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
function createRequestRetry (requestImpl, reply, retryHandler) {
|
package/lib/utils.js
CHANGED
|
@@ -42,12 +42,17 @@ function stripHttp1ConnectionHeaders (headers) {
|
|
|
42
42
|
case 'connection':
|
|
43
43
|
case 'upgrade':
|
|
44
44
|
case 'http2-settings':
|
|
45
|
-
case 'te':
|
|
46
45
|
case 'transfer-encoding':
|
|
47
46
|
case 'proxy-connection':
|
|
48
47
|
case 'keep-alive':
|
|
49
48
|
case 'host':
|
|
50
49
|
break
|
|
50
|
+
case 'te':
|
|
51
|
+
// see illegal connection specific header handling in Node.js
|
|
52
|
+
if (headers['te'] === 'trailers') {
|
|
53
|
+
dest[header] = headers[header]
|
|
54
|
+
}
|
|
55
|
+
break
|
|
51
56
|
default:
|
|
52
57
|
dest[header] = headers[header]
|
|
53
58
|
break
|
|
@@ -58,6 +63,12 @@ function stripHttp1ConnectionHeaders (headers) {
|
|
|
58
63
|
|
|
59
64
|
// issue ref: https://github.com/fastify/fast-proxy/issues/42
|
|
60
65
|
function buildURL (source, reqBase) {
|
|
66
|
+
if (decodeURIComponent(source).includes('..')) {
|
|
67
|
+
const err = new Error('source/request contain invalid characters')
|
|
68
|
+
err.statusCode = 400
|
|
69
|
+
throw err
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
if (Array.isArray(reqBase)) reqBase = reqBase[0]
|
|
62
73
|
let baseOrigin = reqBase ? new URL(reqBase).href : undefined
|
|
63
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastify/reply-from",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.5.0",
|
|
4
4
|
"description": "forward your HTTP request to another server, for fastify",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -60,7 +60,6 @@
|
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@fastify/formbody": "^8.0.0",
|
|
62
62
|
"@fastify/multipart": "^9.0.0",
|
|
63
|
-
"@fastify/pre-commit": "^2.1.0",
|
|
64
63
|
"@sinonjs/fake-timers": "^14.0.0",
|
|
65
64
|
"@types/node": "^24.0.8",
|
|
66
65
|
"@types/tap": "^18.0.0",
|
|
@@ -74,7 +73,7 @@
|
|
|
74
73
|
"proxy": "^2.1.1",
|
|
75
74
|
"proxyquire": "^2.1.3",
|
|
76
75
|
"split2": "^4.2.0",
|
|
77
|
-
"tsd": "^0.
|
|
76
|
+
"tsd": "^0.33.0"
|
|
78
77
|
},
|
|
79
78
|
"dependencies": {
|
|
80
79
|
"@fastify/error": "^4.0.0",
|
|
@@ -85,10 +84,6 @@
|
|
|
85
84
|
"toad-cache": "^3.7.0",
|
|
86
85
|
"undici": "^7.0.0"
|
|
87
86
|
},
|
|
88
|
-
"pre-commit": [
|
|
89
|
-
"lint",
|
|
90
|
-
"test"
|
|
91
|
-
],
|
|
92
87
|
"publishConfig": {
|
|
93
88
|
"access": "public"
|
|
94
89
|
}
|
package/test/build-url.test.js
CHANGED
|
@@ -76,3 +76,35 @@ test('should throw when trying to override base', async (t) => {
|
|
|
76
76
|
|
|
77
77
|
await Promise.all(promises)
|
|
78
78
|
})
|
|
79
|
+
|
|
80
|
+
test('should throw on path traversal attempts', (t) => {
|
|
81
|
+
t.assert.throws(
|
|
82
|
+
() => buildURL('/foo/bar/../', 'http://localhost'),
|
|
83
|
+
new Error('source/request contain invalid characters')
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
t.assert.throws(
|
|
87
|
+
() => buildURL('/foo/bar/..', 'http://localhost'),
|
|
88
|
+
new Error('source/request contain invalid characters')
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
t.assert.throws(
|
|
92
|
+
() => buildURL('/foo/bar/%2e%2e/', 'http://localhost'),
|
|
93
|
+
new Error('source/request contain invalid characters')
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
t.assert.throws(
|
|
97
|
+
() => buildURL('/foo/bar/%2E%2E/', 'http://localhost'),
|
|
98
|
+
new Error('source/request contain invalid characters')
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
t.assert.throws(
|
|
102
|
+
() => buildURL('/foo/bar/..%2f', 'http://localhost'),
|
|
103
|
+
new Error('source/request contain invalid characters')
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
t.assert.throws(
|
|
107
|
+
() => buildURL('/foo/bar/%2e%2e%2f', 'http://localhost'),
|
|
108
|
+
new Error('source/request contain invalid characters')
|
|
109
|
+
)
|
|
110
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const { request } = require('undici')
|
|
6
|
+
const From = require('..')
|
|
7
|
+
const http = require('node:http')
|
|
8
|
+
|
|
9
|
+
const instance = Fastify()
|
|
10
|
+
|
|
11
|
+
t.test('fix for GHSA-2q7r-29rg-6m5h vulnerability', async (t) => {
|
|
12
|
+
t.plan(2)
|
|
13
|
+
|
|
14
|
+
const target = http.createServer((_, res) => {
|
|
15
|
+
res.statusCode = 205
|
|
16
|
+
res.end('hi')
|
|
17
|
+
})
|
|
18
|
+
await target.listen({ port: 0 })
|
|
19
|
+
t.after(() => target.close())
|
|
20
|
+
|
|
21
|
+
instance.get('/', (_request, reply) => { reply.from('/ho/%2E%2E/hi') })
|
|
22
|
+
instance.register(From, {
|
|
23
|
+
base: `http://localhost:${target.address().port}/hi/`,
|
|
24
|
+
undici: true
|
|
25
|
+
})
|
|
26
|
+
await instance.listen({ port: 0 })
|
|
27
|
+
t.after(() => instance.close())
|
|
28
|
+
|
|
29
|
+
const { statusCode, body } = await request(`http://localhost:${instance.server.address().port}`)
|
|
30
|
+
t.assert.strictEqual(statusCode, 400)
|
|
31
|
+
t.assert.strictEqual(await body.text(), '{"statusCode":400,"error":"Bad Request","message":"source/request contain invalid characters"}')
|
|
32
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const { request } = require('undici')
|
|
6
|
+
const From = require('..')
|
|
7
|
+
const http = require('node:http')
|
|
8
|
+
|
|
9
|
+
const instance = Fastify()
|
|
10
|
+
|
|
11
|
+
t.test('full querystring url', async (t) => {
|
|
12
|
+
const target = http.createServer((req, res) => {
|
|
13
|
+
t.assert.ok('request proxied')
|
|
14
|
+
t.assert.strictEqual(req.method, 'GET')
|
|
15
|
+
t.assert.strictEqual(req.url, '/hi?a=/ho/%2E%2E/hi')
|
|
16
|
+
res.statusCode = 205
|
|
17
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
18
|
+
res.setHeader('x-my-header', 'hi!')
|
|
19
|
+
res.end('hi')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
await target.listen({ port: 0 })
|
|
23
|
+
t.after(() => target.close())
|
|
24
|
+
|
|
25
|
+
await instance.register(From, {
|
|
26
|
+
base: `http://localhost:${target.address().port}`
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
instance.get('/hi', (_request, reply) => {
|
|
30
|
+
reply.from()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
instance.get('/foo', (_request, reply) => {
|
|
34
|
+
reply.from('/hi')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await instance.listen({ port: 0 })
|
|
38
|
+
t.after(() => instance.close())
|
|
39
|
+
|
|
40
|
+
{
|
|
41
|
+
const result = await request(`http://localhost:${instance.server.address().port}/hi?a=/ho/%2E%2E/hi`)
|
|
42
|
+
t.assert.strictEqual(result.headers['content-type'], 'text/plain')
|
|
43
|
+
t.assert.strictEqual(result.headers['x-my-header'], 'hi!')
|
|
44
|
+
t.assert.strictEqual(result.statusCode, 205)
|
|
45
|
+
t.assert.strictEqual(await result.body.text(), 'hi')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
const result = await request(`http://localhost:${instance.server.address().port}/foo?a=/ho/%2E%2E/hi`)
|
|
50
|
+
t.assert.strictEqual(result.headers['content-type'], 'text/plain')
|
|
51
|
+
t.assert.strictEqual(result.headers['x-my-header'], 'hi!')
|
|
52
|
+
t.assert.strictEqual(result.statusCode, 205)
|
|
53
|
+
t.assert.strictEqual(await result.body.text(), 'hi')
|
|
54
|
+
}
|
|
55
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const h2url = require('h2url')
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const From = require('..')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
const path = require('node:path')
|
|
8
|
+
const certs = {
|
|
9
|
+
key: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.key')),
|
|
10
|
+
cert: fs.readFileSync(path.join(__dirname, 'fixtures', 'fastify.cert'))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
t.test('do not strip te header if set to trailing', async (t) => {
|
|
14
|
+
const instance = Fastify({
|
|
15
|
+
http2: true,
|
|
16
|
+
https: certs
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
t.after(() => instance.close())
|
|
20
|
+
|
|
21
|
+
const target = Fastify({
|
|
22
|
+
http2: true
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
target.get('/', (request, reply) => {
|
|
26
|
+
t.assert.strictEqual(request.headers['te'], 'trailers')
|
|
27
|
+
|
|
28
|
+
reply.send({
|
|
29
|
+
hello: 'world'
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
instance.get('/', (_request, reply) => {
|
|
34
|
+
reply.from()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
t.after(() => target.close())
|
|
38
|
+
|
|
39
|
+
await target.listen({ port: 0 })
|
|
40
|
+
|
|
41
|
+
instance.register(From, {
|
|
42
|
+
base: `http://localhost:${target.server.address().port}`,
|
|
43
|
+
http2: true,
|
|
44
|
+
rejectUnauthorized: false
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
await instance.listen({ port: 0 })
|
|
48
|
+
|
|
49
|
+
const { headers } = await h2url.concat({
|
|
50
|
+
url: `https://localhost:${instance.server.address().port}`,
|
|
51
|
+
headers: {
|
|
52
|
+
te: 'trailers'
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
t.assert.strictEqual(headers[':status'], 200)
|
|
57
|
+
|
|
58
|
+
instance.close()
|
|
59
|
+
target.close()
|
|
60
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const t = require('node:test')
|
|
6
|
+
const Fastify = require('fastify')
|
|
7
|
+
const { request } = require('undici')
|
|
8
|
+
const From = require('..')
|
|
9
|
+
const http = require('node:http')
|
|
10
|
+
const FormData = require('form-data')
|
|
11
|
+
|
|
12
|
+
t.test('multipart/form-data proxying with custom content type parser', async (t) => {
|
|
13
|
+
t.plan(7)
|
|
14
|
+
|
|
15
|
+
const filetPath = path.join(__dirname, 'fixtures', 'file.txt')
|
|
16
|
+
|
|
17
|
+
// Target server that expects multipart/form-data
|
|
18
|
+
const target = http.createServer((req, res) => {
|
|
19
|
+
t.assert.ok('request proxied')
|
|
20
|
+
t.assert.strictEqual(req.method, 'POST')
|
|
21
|
+
t.assert.match(req.headers['content-type'], /^multipart\/form-data/)
|
|
22
|
+
|
|
23
|
+
let data = ''
|
|
24
|
+
req.setEncoding('utf8')
|
|
25
|
+
req.on('data', (chunk) => {
|
|
26
|
+
data += chunk
|
|
27
|
+
})
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
// Verify the multipart data contains our form fields
|
|
30
|
+
t.assert.match(data, /Content-Disposition: form-data; name="key"/)
|
|
31
|
+
t.assert.match(data, /value/)
|
|
32
|
+
t.assert.match(data, /Content-Disposition: form-data; name="file"/)
|
|
33
|
+
|
|
34
|
+
res.setHeader('content-type', 'application/json')
|
|
35
|
+
res.statusCode = 200
|
|
36
|
+
res.end(JSON.stringify({ received: 'multipart data' }))
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Fastify instance with custom multipart parser (not @fastify/multipart)
|
|
41
|
+
const fastify = Fastify()
|
|
42
|
+
|
|
43
|
+
// Register custom content type parser for multipart/form-data
|
|
44
|
+
// This allows the raw body to be passed through without parsing
|
|
45
|
+
fastify.addContentTypeParser('multipart/form-data', function (req, body, done) {
|
|
46
|
+
done(null, body)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
fastify.register(From)
|
|
50
|
+
|
|
51
|
+
fastify.post('/', (request, reply) => {
|
|
52
|
+
reply.from(`http://localhost:${target.address().port}`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
t.after(() => fastify.close())
|
|
56
|
+
t.after(() => target.close())
|
|
57
|
+
|
|
58
|
+
await new Promise(resolve => fastify.listen({ port: 0 }, resolve))
|
|
59
|
+
await new Promise(resolve => target.listen({ port: 0 }, resolve))
|
|
60
|
+
|
|
61
|
+
// Create multipart form data
|
|
62
|
+
const form = new FormData()
|
|
63
|
+
form.append('key', 'value')
|
|
64
|
+
form.append('file', fs.createReadStream(filetPath, { encoding: 'utf-8' }))
|
|
65
|
+
|
|
66
|
+
// Send request with multipart data
|
|
67
|
+
const result = await request(`http://localhost:${fastify.server.address().port}`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: form.getHeaders(),
|
|
70
|
+
body: form
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
t.assert.deepStrictEqual(await result.body.json(), { received: 'multipart data' })
|
|
74
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const t = require('node:test')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const { request } = require('undici')
|
|
6
|
+
const From = require('..')
|
|
7
|
+
const http = require('node:http')
|
|
8
|
+
|
|
9
|
+
t.test('text/event-stream proxying with custom content type parser', async (t) => {
|
|
10
|
+
t.plan(6)
|
|
11
|
+
|
|
12
|
+
// Target server that sends SSE data
|
|
13
|
+
const target = http.createServer((req, res) => {
|
|
14
|
+
t.assert.ok('request proxied')
|
|
15
|
+
t.assert.strictEqual(req.method, 'POST')
|
|
16
|
+
t.assert.match(req.headers['content-type'], /^text\/event-stream/)
|
|
17
|
+
|
|
18
|
+
let data = ''
|
|
19
|
+
req.setEncoding('utf8')
|
|
20
|
+
req.on('data', (chunk) => {
|
|
21
|
+
data += chunk
|
|
22
|
+
})
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
// Verify the SSE data is received
|
|
25
|
+
t.assert.match(data, /data: test message/)
|
|
26
|
+
t.assert.match(data, /event: custom/)
|
|
27
|
+
|
|
28
|
+
res.setHeader('content-type', 'application/json')
|
|
29
|
+
res.statusCode = 200
|
|
30
|
+
res.end(JSON.stringify({ received: 'sse data' }))
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Fastify instance with custom text/event-stream parser
|
|
35
|
+
const fastify = Fastify()
|
|
36
|
+
|
|
37
|
+
// Register custom content type parser for text/event-stream
|
|
38
|
+
// This allows the raw body to be passed through without parsing
|
|
39
|
+
fastify.addContentTypeParser('text/event-stream', function (req, body, done) {
|
|
40
|
+
done(null, body)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
fastify.register(From)
|
|
44
|
+
|
|
45
|
+
fastify.post('/', (request, reply) => {
|
|
46
|
+
reply.from(`http://localhost:${target.address().port}`)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
t.after(() => fastify.close())
|
|
50
|
+
t.after(() => target.close())
|
|
51
|
+
|
|
52
|
+
await fastify.listen({ port: 0 })
|
|
53
|
+
await target.listen({ port: 0 })
|
|
54
|
+
|
|
55
|
+
// Create SSE-like data
|
|
56
|
+
const sseData = 'data: test message\nevent: custom\ndata: another line\n\n'
|
|
57
|
+
|
|
58
|
+
// Send request with SSE data
|
|
59
|
+
const result = await request(`http://localhost:${fastify.server.address().port}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'content-type': 'text/event-stream'
|
|
63
|
+
},
|
|
64
|
+
body: sseData
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
t.assert.deepStrictEqual(await result.body.json(), { received: 'sse data' })
|
|
68
|
+
})
|