mppx 0.5.9 → 0.5.11
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/CHANGELOG.md +12 -0
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +27 -6
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/server/NodeListener.d.ts +4 -4
- package/dist/server/NodeListener.d.ts.map +1 -1
- package/dist/server/NodeListener.js +38 -4
- package/dist/server/NodeListener.js.map +1 -1
- package/dist/server/Request.d.ts +7 -7
- package/dist/server/Request.d.ts.map +1 -1
- package/dist/server/Request.js +93 -8
- package/dist/server/Request.js.map +1 -1
- package/package.json +2 -4
- package/src/proxy/Proxy.test.ts +22 -0
- package/src/proxy/Proxy.ts +32 -7
- package/src/server/Mppx.test.ts +69 -0
- package/src/server/NodeListener.test.ts +78 -0
- package/src/server/NodeListener.ts +40 -4
- package/src/server/Request.test.ts +26 -0
- package/src/server/Request.ts +111 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# mppx
|
|
2
2
|
|
|
3
|
+
## 0.5.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2aff2c0: Handled malformed Host headers in the Node request listener instead of letting them crash the process.
|
|
8
|
+
|
|
9
|
+
## 0.5.10
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- d95c01c: Pruned internal dependencies.
|
|
14
|
+
|
|
3
15
|
## 0.5.9
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/Proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"Proxy.d.ts","sourceRoot":"","sources":["../../src/proxy/Proxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAA;AAOtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAEvC,kFAAkF;AAClF,MAAM,MAAM,KAAK,GAAG;IAClB,sFAAsF;IACtF,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC9C,uFAAuF;IACvF,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,CAAA;CACxE,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,KAAK,CAyHnD;AAED,MAAM,CAAC,OAAO,WAAW,MAAM,CAAC;IAC9B,KAAY,MAAM,GAAG;QACnB,sEAAsE;QACtE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC7B,wDAAwD;QACxD,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;QACjC,0DAA0D;QAC1D,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAChC,kEAAkE;QAClE,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,GAAG,SAAS,CAAA;QAC/B,qEAAqE;QACrE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,GAAG,SAAS,CAAA;QAC3C,qEAAqE;QACrE,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;QAC3B,8DAA8D;QAC9D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC1B,4DAA4D;QAC5D,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAC7B,CAAA;CACF"}
|
package/dist/proxy/Proxy.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createFetchProxy } from '@remix-run/fetch-proxy';
|
|
2
1
|
import * as Credential from '../Credential.js';
|
|
3
2
|
import { generateProxy } from '../discovery/OpenApi.js';
|
|
4
3
|
import * as Request from '../server/Request.js';
|
|
@@ -34,11 +33,7 @@ import * as Service from './Service.js';
|
|
|
34
33
|
export function create(config) {
|
|
35
34
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
36
35
|
const services = new Map(config.services.map((s) => {
|
|
37
|
-
const proxy = createFetchProxy(s.baseUrl, {
|
|
38
|
-
fetch: fetchImpl,
|
|
39
|
-
rewriteCookieDomain: false,
|
|
40
|
-
rewriteCookiePath: false,
|
|
41
|
-
});
|
|
36
|
+
const proxy = createFetchProxy(s.baseUrl, { fetch: fetchImpl });
|
|
42
37
|
return [s.id, { service: s, proxy }];
|
|
43
38
|
}));
|
|
44
39
|
// Pre-generate static discovery responses once at startup.
|
|
@@ -214,4 +209,30 @@ function matchesPaymentBinding(endpoint, binding) {
|
|
|
214
209
|
return true;
|
|
215
210
|
return payment.method === binding.method && payment.intent === binding.intent;
|
|
216
211
|
}
|
|
212
|
+
function createFetchProxy(target, options) {
|
|
213
|
+
const localFetch = options?.fetch ?? globalThis.fetch;
|
|
214
|
+
const targetUrl = new URL(target);
|
|
215
|
+
if (targetUrl.pathname.endsWith('/'))
|
|
216
|
+
targetUrl.pathname = targetUrl.pathname.replace(/\/+$/, '');
|
|
217
|
+
return async (input, init) => {
|
|
218
|
+
const request = new globalThis.Request(input, init);
|
|
219
|
+
const url = new URL(request.url);
|
|
220
|
+
const proxyUrl = new URL(url.search, targetUrl);
|
|
221
|
+
if (url.pathname !== '/')
|
|
222
|
+
proxyUrl.pathname =
|
|
223
|
+
proxyUrl.pathname === '/' ? url.pathname : proxyUrl.pathname + url.pathname;
|
|
224
|
+
const proxyInit = {
|
|
225
|
+
method: request.method,
|
|
226
|
+
headers: new globalThis.Headers(request.headers),
|
|
227
|
+
signal: request.signal,
|
|
228
|
+
redirect: request.redirect,
|
|
229
|
+
...init,
|
|
230
|
+
};
|
|
231
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
232
|
+
proxyInit.body = request.body;
|
|
233
|
+
proxyInit.duplex = 'half';
|
|
234
|
+
}
|
|
235
|
+
return localFetch(proxyUrl, proxyInit);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
217
238
|
//# sourceMappingURL=Proxy.js.map
|
package/dist/proxy/Proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../src/proxy/Proxy.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"Proxy.js","sourceRoot":"","sources":["../../src/proxy/Proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import type * as http from 'node:http';
|
|
2
|
+
import type * as http2 from 'node:http2';
|
|
2
3
|
/**
|
|
3
4
|
* Writes a Fetch API `Response` to a Node.js `ServerResponse`.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* Fetch API handlers with Node.js HTTP servers.
|
|
6
|
+
* Useful when bridging Fetch API handlers with Node.js HTTP servers.
|
|
7
7
|
*/
|
|
8
|
-
export declare
|
|
8
|
+
export declare function sendResponse(res: http.ServerResponse | http2.Http2ServerResponse, response: Response): Promise<void>;
|
|
9
9
|
//# sourceMappingURL=NodeListener.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeListener.d.ts","sourceRoot":"","sources":["../../src/server/NodeListener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,
|
|
1
|
+
{"version":3,"file":"NodeListener.d.ts","sourceRoot":"","sources":["../../src/server/NodeListener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAA;AACtC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAA;AAExC;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,mBAAmB,EACpD,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -1,9 +1,43 @@
|
|
|
1
|
-
import * as FetchServer from '@remix-run/node-fetch-server';
|
|
2
1
|
/**
|
|
3
2
|
* Writes a Fetch API `Response` to a Node.js `ServerResponse`.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* Fetch API handlers with Node.js HTTP servers.
|
|
4
|
+
* Useful when bridging Fetch API handlers with Node.js HTTP servers.
|
|
7
5
|
*/
|
|
8
|
-
export
|
|
6
|
+
export async function sendResponse(res, response) {
|
|
7
|
+
const headers = {};
|
|
8
|
+
for (const [key, value] of response.headers) {
|
|
9
|
+
if (key in headers) {
|
|
10
|
+
const existing = headers[key];
|
|
11
|
+
if (Array.isArray(existing))
|
|
12
|
+
existing.push(value);
|
|
13
|
+
else
|
|
14
|
+
headers[key] = [existing, value];
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
headers[key] = value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if ('req' in res && res.req?.httpVersionMajor === 1)
|
|
21
|
+
res.writeHead(response.status, response.statusText, headers);
|
|
22
|
+
else
|
|
23
|
+
res.writeHead(response.status, headers);
|
|
24
|
+
if (response.body != null && res.req?.method !== 'HEAD') {
|
|
25
|
+
const reader = response.body.getReader();
|
|
26
|
+
try {
|
|
27
|
+
while (true) {
|
|
28
|
+
const { done, value } = await reader.read();
|
|
29
|
+
if (done)
|
|
30
|
+
break;
|
|
31
|
+
if (res.write(value) === false)
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
res.once('drain', resolve);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
reader.releaseLock();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
res.end();
|
|
42
|
+
}
|
|
9
43
|
//# sourceMappingURL=NodeListener.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeListener.js","sourceRoot":"","sources":["../../src/server/NodeListener.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"NodeListener.js","sourceRoot":"","sources":["../../src/server/NodeListener.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAoD,EACpD,QAAkB;IAElB,MAAM,OAAO,GAAsC,EAAE,CAAA;IACrD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;QAC5C,IAAI,GAAG,IAAI,OAAO,EAAE,CAAC;YACnB,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;YAC7B,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;;gBAC5C,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,QAAS,EAAE,KAAK,CAAC,CAAA;QACxC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QACtB,CAAC;IACH,CAAC;IAED,IAAI,KAAK,IAAI,GAAG,IAAK,GAA2B,CAAC,GAAG,EAAE,gBAAgB,KAAK,CAAC;QACzE,GAA2B,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;;QACjF,GAAiC,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAE3E,IAAI,QAAQ,CAAC,IAAI,IAAI,IAAI,IAAK,GAA2B,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;QACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAA;QACxC,IAAI,CAAC;YACH,OAAO,IAAI,EAAE,CAAC;gBACZ,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAA;gBAC3C,IAAI,IAAI;oBAAE,MAAK;gBACf,IAAK,GAA2B,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,KAAK;oBACrD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;wBAClC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;oBAC5B,CAAC,CAAC,CAAA;YACN,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,WAAW,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAED,GAAG,CAAC,GAAG,EAAE,CAAA;AACX,CAAC"}
|
package/dist/server/Request.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http';
|
|
2
|
-
import * as FetchServer from '@remix-run/node-fetch-server';
|
|
3
2
|
export type FetchHandler = (request: Request) => Promise<Response> | Response;
|
|
3
|
+
export type RequestListenerOptions = {
|
|
4
|
+
host?: string | undefined;
|
|
5
|
+
onError?: ((error: unknown) => void | Response | Promise<void | Response>) | undefined;
|
|
6
|
+
protocol?: string | undefined;
|
|
7
|
+
};
|
|
4
8
|
/**
|
|
5
9
|
* Converts a Fetch API handler into a Node.js HTTP request listener.
|
|
6
10
|
*
|
|
7
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
8
|
-
*
|
|
9
11
|
* @param handler - A Fetch API handler: `(request: Request) => Response`.
|
|
10
12
|
* @param options - Optional error handler.
|
|
11
13
|
* @returns A Node.js `(req, res)` listener.
|
|
12
14
|
*/
|
|
13
|
-
export declare function toNodeListener(handler: FetchHandler, options?:
|
|
15
|
+
export declare function toNodeListener(handler: FetchHandler, options?: RequestListenerOptions | undefined): RequestListener;
|
|
14
16
|
/**
|
|
15
17
|
* Converts a Node.js `IncomingMessage`/`ServerResponse` pair to a Fetch API `Request`.
|
|
16
18
|
*
|
|
17
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
18
|
-
*
|
|
19
19
|
* @param req - The Node.js IncomingMessage.
|
|
20
20
|
* @param res - The Node.js ServerResponse (used for abort signal lifecycle).
|
|
21
21
|
* @returns A Fetch API Request.
|
|
22
22
|
*/
|
|
23
|
-
export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse): Request;
|
|
23
|
+
export declare function fromNodeListener(req: IncomingMessage, res: ServerResponse, options?: RequestListenerOptions | undefined): Request;
|
|
24
24
|
//# sourceMappingURL=Request.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Request.d.ts","sourceRoot":"","sources":["../../src/server/Request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"Request.d.ts","sourceRoot":"","sources":["../../src/server/Request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAIjF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAA;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,OAAO,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC,GAAG,SAAS,CAAA;IACtF,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,sBAAsB,GAAG,SAAS,GAC3C,eAAe,CAkCjB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE,sBAAsB,GAAG,SAAS,GAC3C,OAAO,CA0CT"}
|
package/dist/server/Request.js
CHANGED
|
@@ -1,26 +1,111 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as NodeListener from './NodeListener.js';
|
|
2
2
|
/**
|
|
3
3
|
* Converts a Fetch API handler into a Node.js HTTP request listener.
|
|
4
4
|
*
|
|
5
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
6
|
-
*
|
|
7
5
|
* @param handler - A Fetch API handler: `(request: Request) => Response`.
|
|
8
6
|
* @param options - Optional error handler.
|
|
9
7
|
* @returns A Node.js `(req, res)` listener.
|
|
10
8
|
*/
|
|
11
9
|
export function toNodeListener(handler, options) {
|
|
12
|
-
|
|
10
|
+
const onError = options?.onError ??
|
|
11
|
+
((error) => {
|
|
12
|
+
console.error(error);
|
|
13
|
+
return new Response('Internal Server Error', {
|
|
14
|
+
status: 500,
|
|
15
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
return (async (req, res) => {
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
const request = fromNodeListener(req, res, options);
|
|
22
|
+
response = await handler(request);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
try {
|
|
26
|
+
response =
|
|
27
|
+
(await onError(error)) ??
|
|
28
|
+
new Response('Internal Server Error', {
|
|
29
|
+
status: 500,
|
|
30
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (innerError) {
|
|
34
|
+
console.error(`There was an error in the error handler: ${innerError}`);
|
|
35
|
+
response = new Response('Internal Server Error', {
|
|
36
|
+
status: 500,
|
|
37
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await NodeListener.sendResponse(res, response);
|
|
42
|
+
});
|
|
13
43
|
}
|
|
14
44
|
/**
|
|
15
45
|
* Converts a Node.js `IncomingMessage`/`ServerResponse` pair to a Fetch API `Request`.
|
|
16
46
|
*
|
|
17
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
18
|
-
*
|
|
19
47
|
* @param req - The Node.js IncomingMessage.
|
|
20
48
|
* @param res - The Node.js ServerResponse (used for abort signal lifecycle).
|
|
21
49
|
* @returns A Fetch API Request.
|
|
22
50
|
*/
|
|
23
|
-
export function fromNodeListener(req, res) {
|
|
24
|
-
|
|
51
|
+
export function fromNodeListener(req, res, options) {
|
|
52
|
+
let controller = new AbortController();
|
|
53
|
+
res.once('close', () => controller?.abort());
|
|
54
|
+
res.once('finish', () => {
|
|
55
|
+
controller = null;
|
|
56
|
+
});
|
|
57
|
+
const method = req.method ?? 'GET';
|
|
58
|
+
const headers = createHeaders(req);
|
|
59
|
+
const protocol = options?.protocol ??
|
|
60
|
+
('encrypted' in req.socket && req.socket.encrypted
|
|
61
|
+
? 'https:'
|
|
62
|
+
: 'http:');
|
|
63
|
+
const host = options?.host ??
|
|
64
|
+
headers.get('Host') ??
|
|
65
|
+
req.headers[':authority'] ??
|
|
66
|
+
'localhost';
|
|
67
|
+
const url = new URL(normalizeRequestTarget(req.url), `${protocol}//${host}`);
|
|
68
|
+
const init = {
|
|
69
|
+
method,
|
|
70
|
+
headers,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
};
|
|
73
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
74
|
+
init.body = new ReadableStream({
|
|
75
|
+
start(c) {
|
|
76
|
+
req.on('data', (chunk) => {
|
|
77
|
+
c.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
|
|
78
|
+
});
|
|
79
|
+
req.on('end', () => {
|
|
80
|
+
c.close();
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
init.duplex = 'half';
|
|
85
|
+
}
|
|
86
|
+
return new Request(url, init);
|
|
87
|
+
}
|
|
88
|
+
function normalizeRequestTarget(url) {
|
|
89
|
+
if (!url)
|
|
90
|
+
return '/';
|
|
91
|
+
try {
|
|
92
|
+
const absoluteUrl = new URL(url);
|
|
93
|
+
// Absolute-form request targets can carry a different origin than the socket host.
|
|
94
|
+
// Keep only path+query so realm detection stays bound to Host/:authority.
|
|
95
|
+
if (absoluteUrl.protocol === 'http:' || absoluteUrl.protocol === 'https:')
|
|
96
|
+
return `${absoluteUrl.pathname}${absoluteUrl.search}`;
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
function createHeaders(req) {
|
|
102
|
+
const headers = new Headers();
|
|
103
|
+
const raw = req.rawHeaders;
|
|
104
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
105
|
+
if (raw[i].startsWith(':'))
|
|
106
|
+
continue;
|
|
107
|
+
headers.append(raw[i], raw[i + 1]);
|
|
108
|
+
}
|
|
109
|
+
return headers;
|
|
25
110
|
}
|
|
26
111
|
//# sourceMappingURL=Request.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Request.js","sourceRoot":"","sources":["../../src/server/Request.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"Request.js","sourceRoot":"","sources":["../../src/server/Request.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mppx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.11",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@modelcontextprotocol/sdk": ">=1.25.0",
|
|
106
106
|
"elysia": ">=1",
|
|
107
107
|
"express": ">=5",
|
|
108
|
-
"hono": ">=4",
|
|
108
|
+
"hono": ">=4.12.12",
|
|
109
109
|
"viem": ">=2.47.5"
|
|
110
110
|
},
|
|
111
111
|
"peerDependenciesMeta": {
|
|
@@ -123,8 +123,6 @@
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
"dependencies": {
|
|
126
|
-
"@remix-run/fetch-proxy": "^0.7.1",
|
|
127
|
-
"@remix-run/node-fetch-server": "^0.13.0",
|
|
128
126
|
"incur": "^0.3.23",
|
|
129
127
|
"ox": "0.14.7",
|
|
130
128
|
"zod": "^4.3.6"
|
package/src/proxy/Proxy.test.ts
CHANGED
|
@@ -250,6 +250,28 @@ describe('create', () => {
|
|
|
250
250
|
expect(await res.json()).toEqual({ path: '/v1/status' })
|
|
251
251
|
})
|
|
252
252
|
|
|
253
|
+
test('behavior: joins upstream base paths with request paths', async () => {
|
|
254
|
+
upstream = await createUpstream((req) =>
|
|
255
|
+
Response.json({
|
|
256
|
+
path: new URL(req.url).pathname,
|
|
257
|
+
search: new URL(req.url).search,
|
|
258
|
+
}),
|
|
259
|
+
)
|
|
260
|
+
const proxy = ApiProxy.create({
|
|
261
|
+
services: [
|
|
262
|
+
Service.from('api', {
|
|
263
|
+
baseUrl: `${upstream.url}/prefix/`,
|
|
264
|
+
routes: { 'GET /v1/status': true },
|
|
265
|
+
}),
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
proxyServer = await Http.createServer(proxy.listener)
|
|
269
|
+
|
|
270
|
+
const res = await fetch(`${proxyServer.url}/api/v1/status?q=ok`)
|
|
271
|
+
expect(res.status).toBe(200)
|
|
272
|
+
expect(await res.json()).toEqual({ path: '/prefix/v1/status', search: '?q=ok' })
|
|
273
|
+
})
|
|
274
|
+
|
|
253
275
|
test('behavior: returns 402 when no credential', async () => {
|
|
254
276
|
upstream = await createUpstream(() => Response.json({ result: 'ok' }))
|
|
255
277
|
const proxy = ApiProxy.create({
|
package/src/proxy/Proxy.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type * as http from 'node:http'
|
|
2
2
|
|
|
3
|
-
import { createFetchProxy } from '@remix-run/fetch-proxy'
|
|
4
|
-
|
|
5
3
|
import * as Credential from '../Credential.js'
|
|
6
4
|
import { generateProxy } from '../discovery/OpenApi.js'
|
|
7
5
|
import * as Request from '../server/Request.js'
|
|
@@ -48,11 +46,7 @@ export function create(config: create.Config): Proxy {
|
|
|
48
46
|
|
|
49
47
|
const services = new Map(
|
|
50
48
|
config.services.map((s) => {
|
|
51
|
-
const proxy = createFetchProxy(s.baseUrl, {
|
|
52
|
-
fetch: fetchImpl,
|
|
53
|
-
rewriteCookieDomain: false,
|
|
54
|
-
rewriteCookiePath: false,
|
|
55
|
-
})
|
|
49
|
+
const proxy = createFetchProxy(s.baseUrl, { fetch: fetchImpl })
|
|
56
50
|
return [s.id, { service: s, proxy }] as const
|
|
57
51
|
}),
|
|
58
52
|
)
|
|
@@ -295,3 +289,34 @@ function matchesPaymentBinding(endpoint: unknown, binding: PaymentBinding | null
|
|
|
295
289
|
if (!payment) return true
|
|
296
290
|
return payment.method === binding.method && payment.intent === binding.intent
|
|
297
291
|
}
|
|
292
|
+
|
|
293
|
+
function createFetchProxy(
|
|
294
|
+
target: string | URL,
|
|
295
|
+
options?: { fetch?: typeof globalThis.fetch },
|
|
296
|
+
): (input: URL | RequestInfo, init?: RequestInit) => Promise<Response> {
|
|
297
|
+
const localFetch = options?.fetch ?? globalThis.fetch
|
|
298
|
+
const targetUrl = new URL(target)
|
|
299
|
+
if (targetUrl.pathname.endsWith('/')) targetUrl.pathname = targetUrl.pathname.replace(/\/+$/, '')
|
|
300
|
+
|
|
301
|
+
return async (input, init) => {
|
|
302
|
+
const request = new globalThis.Request(input, init)
|
|
303
|
+
const url = new URL(request.url)
|
|
304
|
+
const proxyUrl = new URL(url.search, targetUrl)
|
|
305
|
+
if (url.pathname !== '/')
|
|
306
|
+
proxyUrl.pathname =
|
|
307
|
+
proxyUrl.pathname === '/' ? url.pathname : proxyUrl.pathname + url.pathname
|
|
308
|
+
|
|
309
|
+
const proxyInit: RequestInit & { duplex?: 'half' } = {
|
|
310
|
+
method: request.method,
|
|
311
|
+
headers: new globalThis.Headers(request.headers),
|
|
312
|
+
signal: request.signal,
|
|
313
|
+
redirect: request.redirect,
|
|
314
|
+
...init,
|
|
315
|
+
}
|
|
316
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
317
|
+
proxyInit.body = request.body
|
|
318
|
+
proxyInit.duplex = 'half'
|
|
319
|
+
}
|
|
320
|
+
return localFetch(proxyUrl, proxyInit)
|
|
321
|
+
}
|
|
322
|
+
}
|
package/src/server/Mppx.test.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as http from 'node:http'
|
|
2
|
+
|
|
1
3
|
import { Challenge, Credential, Method, z } from 'mppx'
|
|
2
4
|
import { Mppx, Transport, tempo } from 'mppx/server'
|
|
3
5
|
import { describe, expect, test } from 'vp/test'
|
|
@@ -2422,6 +2424,73 @@ describe('realm auto-detection', () => {
|
|
|
2422
2424
|
expect(challenge.realm).toBe(expected)
|
|
2423
2425
|
})
|
|
2424
2426
|
|
|
2427
|
+
test('ignores absolute-form request targets when deriving realm in node listeners', async () => {
|
|
2428
|
+
const handler = Mppx.create({ methods: [mockMethod], secretKey })
|
|
2429
|
+
const server = await Http.createServer(async (req, res) => {
|
|
2430
|
+
const result = await Mppx.toNodeListener(
|
|
2431
|
+
handler.charge({
|
|
2432
|
+
amount: '100',
|
|
2433
|
+
currency: '0x0000000000000000000000000000000000000001',
|
|
2434
|
+
recipient: '0x0000000000000000000000000000000000000002',
|
|
2435
|
+
}),
|
|
2436
|
+
)(req, res)
|
|
2437
|
+
|
|
2438
|
+
if (result.status !== 402) res.end('OK')
|
|
2439
|
+
})
|
|
2440
|
+
|
|
2441
|
+
try {
|
|
2442
|
+
const rawResponse = await new Promise<{
|
|
2443
|
+
body: string
|
|
2444
|
+
headers: http.IncomingHttpHeaders
|
|
2445
|
+
statusCode: number
|
|
2446
|
+
}>((resolve, reject) => {
|
|
2447
|
+
const request = http.request(
|
|
2448
|
+
{
|
|
2449
|
+
host: '127.0.0.1',
|
|
2450
|
+
port: server.port,
|
|
2451
|
+
method: 'GET',
|
|
2452
|
+
path: 'http://unexpected.example/resource',
|
|
2453
|
+
headers: { Host: `localhost:${server.port}` },
|
|
2454
|
+
},
|
|
2455
|
+
(response) => {
|
|
2456
|
+
const chunks: Buffer[] = []
|
|
2457
|
+
response.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
|
|
2458
|
+
response.on('end', () => {
|
|
2459
|
+
resolve({
|
|
2460
|
+
body: Buffer.concat(chunks).toString('utf8'),
|
|
2461
|
+
headers: response.headers,
|
|
2462
|
+
statusCode: response.statusCode ?? 0,
|
|
2463
|
+
})
|
|
2464
|
+
})
|
|
2465
|
+
},
|
|
2466
|
+
)
|
|
2467
|
+
|
|
2468
|
+
request.on('error', reject)
|
|
2469
|
+
request.end()
|
|
2470
|
+
})
|
|
2471
|
+
|
|
2472
|
+
const headers = new Headers()
|
|
2473
|
+
for (const [name, value] of Object.entries(rawResponse.headers)) {
|
|
2474
|
+
if (Array.isArray(value)) {
|
|
2475
|
+
for (const item of value) headers.append(name, item)
|
|
2476
|
+
} else if (value !== undefined) {
|
|
2477
|
+
headers.append(name, value)
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
const challenge = Challenge.fromResponse(
|
|
2482
|
+
new Response(rawResponse.body, {
|
|
2483
|
+
status: rawResponse.statusCode,
|
|
2484
|
+
headers,
|
|
2485
|
+
}),
|
|
2486
|
+
)
|
|
2487
|
+
|
|
2488
|
+
expect(challenge.realm).toBe('localhost')
|
|
2489
|
+
} finally {
|
|
2490
|
+
server.close()
|
|
2491
|
+
}
|
|
2492
|
+
})
|
|
2493
|
+
|
|
2425
2494
|
test('credential verifies across different casing of same host', async () => {
|
|
2426
2495
|
const handler = Mppx.create({ methods: [mockMethod], secretKey })
|
|
2427
2496
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
3
|
+
|
|
1
4
|
import { NodeListener, Request } from 'mppx/server'
|
|
2
5
|
import { afterEach, describe, expect, test } from 'vp/test'
|
|
3
6
|
import * as Http from '~test/Http.js'
|
|
@@ -6,6 +9,63 @@ let server: Awaited<ReturnType<typeof Http.createServer>> | undefined
|
|
|
6
9
|
|
|
7
10
|
afterEach(() => server?.close())
|
|
8
11
|
|
|
12
|
+
function createMockRequest(options: {
|
|
13
|
+
method?: string
|
|
14
|
+
url?: string
|
|
15
|
+
rawHeaders?: string[]
|
|
16
|
+
}): [
|
|
17
|
+
IncomingMessage,
|
|
18
|
+
ServerResponse & { body: Buffer[]; headers?: Record<string, string | string[]> },
|
|
19
|
+
] {
|
|
20
|
+
type MockResponse = ServerResponse & {
|
|
21
|
+
body: Buffer[]
|
|
22
|
+
headers?: Record<string, string | string[]>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rawHeaders = options.rawHeaders ?? []
|
|
26
|
+
const headers = Object.fromEntries(
|
|
27
|
+
rawHeaders.reduce<[string, string][]>((acc, value, index, values) => {
|
|
28
|
+
if (index % 2 === 0 && values[index + 1]) acc.push([value.toLowerCase(), values[index + 1]!])
|
|
29
|
+
return acc
|
|
30
|
+
}, []),
|
|
31
|
+
)
|
|
32
|
+
const req = Object.assign(new EventEmitter(), {
|
|
33
|
+
method: options.method ?? 'GET',
|
|
34
|
+
url: options.url ?? '/',
|
|
35
|
+
headers,
|
|
36
|
+
rawHeaders,
|
|
37
|
+
socket: {},
|
|
38
|
+
}) as unknown as IncomingMessage
|
|
39
|
+
|
|
40
|
+
const res = Object.assign(new EventEmitter(), {
|
|
41
|
+
req,
|
|
42
|
+
body: [] as Buffer[],
|
|
43
|
+
writeHead(
|
|
44
|
+
this: MockResponse,
|
|
45
|
+
_statusCode: number,
|
|
46
|
+
statusMessageOrHeaders?: string | Record<string, string | string[]>,
|
|
47
|
+
headersMaybe?: Record<string, string | string[]>,
|
|
48
|
+
) {
|
|
49
|
+
this.headers =
|
|
50
|
+
typeof statusMessageOrHeaders === 'string'
|
|
51
|
+
? (headersMaybe ?? {})
|
|
52
|
+
: (statusMessageOrHeaders ?? {})
|
|
53
|
+
return this
|
|
54
|
+
},
|
|
55
|
+
write(this: MockResponse, chunk: Uint8Array | string) {
|
|
56
|
+
this.body.push(Buffer.from(chunk))
|
|
57
|
+
return true
|
|
58
|
+
},
|
|
59
|
+
end(this: MockResponse, chunk?: Uint8Array | string) {
|
|
60
|
+
if (chunk) this.body.push(Buffer.from(chunk))
|
|
61
|
+
this.emit('finish')
|
|
62
|
+
return this
|
|
63
|
+
},
|
|
64
|
+
}) as unknown as MockResponse
|
|
65
|
+
|
|
66
|
+
return [req, res]
|
|
67
|
+
}
|
|
68
|
+
|
|
9
69
|
describe('sendResponse', () => {
|
|
10
70
|
test('writes status and headers', async () => {
|
|
11
71
|
server = await Http.createServer(async (_, res) => {
|
|
@@ -185,4 +245,22 @@ describe('toNodeListener', () => {
|
|
|
185
245
|
const response = await fetch(server.url)
|
|
186
246
|
expect(await response.text()).toBe('abc')
|
|
187
247
|
})
|
|
248
|
+
|
|
249
|
+
test('routes malformed host header URL parsing errors through onError', async () => {
|
|
250
|
+
const [req, res] = createMockRequest({
|
|
251
|
+
rawHeaders: ['Host', 'a, b'],
|
|
252
|
+
})
|
|
253
|
+
const onError = vi.fn((error: unknown) => {
|
|
254
|
+
return new Response(error instanceof TypeError ? 'bad host' : 'unexpected', {
|
|
255
|
+
status: 500,
|
|
256
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
const handler = Request.toNodeListener(async () => new Response('ok'), { onError })
|
|
260
|
+
|
|
261
|
+
await expect(Promise.resolve(handler(req, res))).resolves.toBeUndefined()
|
|
262
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
263
|
+
expect(onError.mock.calls[0]![0]).toBeInstanceOf(TypeError)
|
|
264
|
+
expect(Buffer.concat(res.body).toString()).toBe('bad host')
|
|
265
|
+
})
|
|
188
266
|
})
|
|
@@ -1,9 +1,45 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import type * as http from 'node:http'
|
|
2
|
+
import type * as http2 from 'node:http2'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Writes a Fetch API `Response` to a Node.js `ServerResponse`.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* Fetch API handlers with Node.js HTTP servers.
|
|
7
|
+
* Useful when bridging Fetch API handlers with Node.js HTTP servers.
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
9
|
+
export async function sendResponse(
|
|
10
|
+
res: http.ServerResponse | http2.Http2ServerResponse,
|
|
11
|
+
response: Response,
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const headers: Record<string, string | string[]> = {}
|
|
14
|
+
for (const [key, value] of response.headers) {
|
|
15
|
+
if (key in headers) {
|
|
16
|
+
const existing = headers[key]
|
|
17
|
+
if (Array.isArray(existing)) existing.push(value)
|
|
18
|
+
else headers[key] = [existing!, value]
|
|
19
|
+
} else {
|
|
20
|
+
headers[key] = value
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if ('req' in res && (res as http.ServerResponse).req?.httpVersionMajor === 1)
|
|
25
|
+
(res as http.ServerResponse).writeHead(response.status, response.statusText, headers)
|
|
26
|
+
else (res as http2.Http2ServerResponse).writeHead(response.status, headers)
|
|
27
|
+
|
|
28
|
+
if (response.body != null && (res as http.ServerResponse).req?.method !== 'HEAD') {
|
|
29
|
+
const reader = response.body.getReader()
|
|
30
|
+
try {
|
|
31
|
+
while (true) {
|
|
32
|
+
const { done, value } = await reader.read()
|
|
33
|
+
if (done) break
|
|
34
|
+
if ((res as http.ServerResponse).write(value) === false)
|
|
35
|
+
await new Promise<void>((resolve) => {
|
|
36
|
+
res.once('drain', resolve)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
reader.releaseLock()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.end()
|
|
45
|
+
}
|
|
@@ -62,6 +62,32 @@ describe('fromNodeListener', () => {
|
|
|
62
62
|
expect(request.url).toBe('http://localhost/')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
+
test('normalizes absolute-form request targets to the host header', () => {
|
|
66
|
+
const [req, res] = createMockRequest({
|
|
67
|
+
url: 'http://unexpected.example/api/resource?q=1',
|
|
68
|
+
rawHeaders: ['Host', 'example.com'],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const request = Request.fromNodeListener(req, res)
|
|
72
|
+
|
|
73
|
+
expect(request.url).toBe('http://example.com/api/resource?q=1')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('uses explicit protocol and host overrides', () => {
|
|
77
|
+
const [req, res] = createMockRequest({
|
|
78
|
+
url: '/api/resource',
|
|
79
|
+
rawHeaders: ['Host', 'internal.local'],
|
|
80
|
+
socket: { encrypted: false },
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const request = Request.fromNodeListener(req, res, {
|
|
84
|
+
host: 'api.example.com',
|
|
85
|
+
protocol: 'https:',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(request.url).toBe('https://api.example.com/api/resource')
|
|
89
|
+
})
|
|
90
|
+
|
|
65
91
|
test('preserves multi-value headers via append', () => {
|
|
66
92
|
const [req, res] = createMockRequest({
|
|
67
93
|
rawHeaders: ['Host', 'example.com', 'Set-Cookie', 'a=1', 'Set-Cookie', 'b=2'],
|
package/src/server/Request.ts
CHANGED
|
@@ -1,34 +1,136 @@
|
|
|
1
1
|
import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http'
|
|
2
2
|
|
|
3
|
-
import * as
|
|
3
|
+
import * as NodeListener from './NodeListener.js'
|
|
4
4
|
|
|
5
5
|
export type FetchHandler = (request: Request) => Promise<Response> | Response
|
|
6
6
|
|
|
7
|
+
export type RequestListenerOptions = {
|
|
8
|
+
host?: string | undefined
|
|
9
|
+
onError?: ((error: unknown) => void | Response | Promise<void | Response>) | undefined
|
|
10
|
+
protocol?: string | undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Converts a Fetch API handler into a Node.js HTTP request listener.
|
|
9
15
|
*
|
|
10
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
11
|
-
*
|
|
12
16
|
* @param handler - A Fetch API handler: `(request: Request) => Response`.
|
|
13
17
|
* @param options - Optional error handler.
|
|
14
18
|
* @returns A Node.js `(req, res)` listener.
|
|
15
19
|
*/
|
|
16
20
|
export function toNodeListener(
|
|
17
21
|
handler: FetchHandler,
|
|
18
|
-
options?:
|
|
22
|
+
options?: RequestListenerOptions | undefined,
|
|
19
23
|
): RequestListener {
|
|
20
|
-
|
|
24
|
+
const onError =
|
|
25
|
+
options?.onError ??
|
|
26
|
+
((error: unknown) => {
|
|
27
|
+
console.error(error)
|
|
28
|
+
return new Response('Internal Server Error', {
|
|
29
|
+
status: 500,
|
|
30
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return (async (req: IncomingMessage, res: ServerResponse) => {
|
|
35
|
+
let response: Response
|
|
36
|
+
try {
|
|
37
|
+
const request = fromNodeListener(req, res, options)
|
|
38
|
+
response = await handler(request)
|
|
39
|
+
} catch (error) {
|
|
40
|
+
try {
|
|
41
|
+
response =
|
|
42
|
+
(await onError(error)) ??
|
|
43
|
+
new Response('Internal Server Error', {
|
|
44
|
+
status: 500,
|
|
45
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
46
|
+
})
|
|
47
|
+
} catch (innerError) {
|
|
48
|
+
console.error(`There was an error in the error handler: ${innerError}`)
|
|
49
|
+
response = new Response('Internal Server Error', {
|
|
50
|
+
status: 500,
|
|
51
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await NodeListener.sendResponse(res, response)
|
|
56
|
+
}) as RequestListener
|
|
21
57
|
}
|
|
22
58
|
|
|
23
59
|
/**
|
|
24
60
|
* Converts a Node.js `IncomingMessage`/`ServerResponse` pair to a Fetch API `Request`.
|
|
25
61
|
*
|
|
26
|
-
* Uses [`@remix-run/node-fetch-server`](https://github.com/remix-run/remix/blob/main/packages/node-fetch-server/src/lib/request-listener.ts).
|
|
27
|
-
*
|
|
28
62
|
* @param req - The Node.js IncomingMessage.
|
|
29
63
|
* @param res - The Node.js ServerResponse (used for abort signal lifecycle).
|
|
30
64
|
* @returns A Fetch API Request.
|
|
31
65
|
*/
|
|
32
|
-
export function fromNodeListener(
|
|
33
|
-
|
|
66
|
+
export function fromNodeListener(
|
|
67
|
+
req: IncomingMessage,
|
|
68
|
+
res: ServerResponse,
|
|
69
|
+
options?: RequestListenerOptions | undefined,
|
|
70
|
+
): Request {
|
|
71
|
+
let controller: AbortController | null = new AbortController()
|
|
72
|
+
res.once('close', () => controller?.abort())
|
|
73
|
+
res.once('finish', () => {
|
|
74
|
+
controller = null
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const method = req.method ?? 'GET'
|
|
78
|
+
const headers = createHeaders(req)
|
|
79
|
+
const protocol =
|
|
80
|
+
options?.protocol ??
|
|
81
|
+
('encrypted' in req.socket && (req.socket as { encrypted?: boolean }).encrypted
|
|
82
|
+
? 'https:'
|
|
83
|
+
: 'http:')
|
|
84
|
+
const host =
|
|
85
|
+
options?.host ??
|
|
86
|
+
headers.get('Host') ??
|
|
87
|
+
(req.headers as Record<string, string>)[':authority'] ??
|
|
88
|
+
'localhost'
|
|
89
|
+
const url = new URL(normalizeRequestTarget(req.url), `${protocol}//${host}`)
|
|
90
|
+
|
|
91
|
+
const init: RequestInit & { duplex?: string } = {
|
|
92
|
+
method,
|
|
93
|
+
headers,
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
98
|
+
init.body = new ReadableStream({
|
|
99
|
+
start(c) {
|
|
100
|
+
req.on('data', (chunk: Buffer) => {
|
|
101
|
+
c.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))
|
|
102
|
+
})
|
|
103
|
+
req.on('end', () => {
|
|
104
|
+
c.close()
|
|
105
|
+
})
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
init.duplex = 'half'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return new Request(url, init)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeRequestTarget(url: string | undefined): string {
|
|
115
|
+
if (!url) return '/'
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const absoluteUrl = new URL(url)
|
|
119
|
+
// Absolute-form request targets can carry a different origin than the socket host.
|
|
120
|
+
// Keep only path+query so realm detection stays bound to Host/:authority.
|
|
121
|
+
if (absoluteUrl.protocol === 'http:' || absoluteUrl.protocol === 'https:')
|
|
122
|
+
return `${absoluteUrl.pathname}${absoluteUrl.search}`
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
return url
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createHeaders(req: IncomingMessage): Headers {
|
|
129
|
+
const headers = new Headers()
|
|
130
|
+
const raw = req.rawHeaders
|
|
131
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
132
|
+
if (raw[i]!.startsWith(':')) continue
|
|
133
|
+
headers.append(raw[i]!, raw[i + 1]!)
|
|
134
|
+
}
|
|
135
|
+
return headers
|
|
34
136
|
}
|