@fedify/fastify 2.0.29-dev.2053 → 2.0.29-dev.2055

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/dist/index.cjs CHANGED
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  //#endregion
27
27
  let fastify_plugin = require("fastify-plugin");
28
28
  fastify_plugin = __toESM(fastify_plugin);
29
- let node_stream = require("node:stream");
29
+ let node_buffer = require("node:buffer");
30
30
  //#region src/index.ts
31
31
  /**
32
32
  * Fastify plugin that integrates with a Federation instance.
@@ -61,7 +61,7 @@ let node_stream = require("node:stream");
61
61
  const fedifyPluginCore = (fastify, options) => {
62
62
  const { federation, contextDataFactory = () => void 0, errorHandlers } = options;
63
63
  fastify.addHook("onRequest", async (request, reply) => {
64
- const webRequest = toWebRequest(request);
64
+ const { request: webRequest, body } = toWebRequest(request);
65
65
  const contextData = await contextDataFactory(request);
66
66
  const response = await federation.fetch(webRequest, {
67
67
  contextData,
@@ -69,7 +69,10 @@ const fedifyPluginCore = (fastify, options) => {
69
69
  onNotFound: () => dummyNotFoundResponse,
70
70
  ...errorHandlers
71
71
  });
72
- if (response === dummyNotFoundResponse) return;
72
+ if (response === dummyNotFoundResponse) {
73
+ body?.restore();
74
+ return;
75
+ }
73
76
  await reply.send(response);
74
77
  });
75
78
  return Promise.resolve();
@@ -94,34 +97,104 @@ function toWebRequest(fastifyReq) {
94
97
  const headers = new Headers();
95
98
  for (const [key, value] of Object.entries(fastifyReq.raw.headers)) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
96
99
  else if (value !== void 0) headers.set(key, String(value));
97
- const body = fastifyReq.method === "GET" || fastifyReq.method === "HEAD" ? void 0 : fastifyReq.body !== void 0 ? typeof fastifyReq.body === "string" ? fastifyReq.body : JSON.stringify(fastifyReq.body) : lazyRequestBody(fastifyReq.raw);
98
- return new Request(url, {
99
- method: fastifyReq.method,
100
- headers,
101
- body,
102
- duplex: "half"
103
- });
100
+ let body;
101
+ let rawBody;
102
+ if (fastifyReq.method === "GET" || fastifyReq.method === "HEAD") body = void 0;
103
+ else if (fastifyReq.body !== void 0) body = typeof fastifyReq.body === "string" ? fastifyReq.body : JSON.stringify(fastifyReq.body);
104
+ else {
105
+ rawBody = createRequestBody(fastifyReq.raw);
106
+ body = rawBody.stream;
107
+ }
108
+ return {
109
+ request: new Request(url, {
110
+ method: fastifyReq.method,
111
+ headers,
112
+ body,
113
+ duplex: "half"
114
+ }),
115
+ body: rawBody
116
+ };
104
117
  }
105
118
  /**
106
119
  * Wraps the raw `IncomingMessage` in a `ReadableStream` that stays inert until
107
- * first read. The `Request` is built in the `onRequest` hook, before Fedify
108
- * decides whether it handles the request; an eager `Readable.toWeb()` would
109
- * start draining the socket and break Fastify's own body parsing of the
110
- * declined requests.
120
+ * first read, and that keeps the chunks it reads so that they can be put back.
121
+ *
122
+ * The `Request` is built before Fedify decides whether it handles the request.
123
+ * Laziness alone is not enough: `Request.clone()` tees the body, and a tee pulls
124
+ * a chunk right away, so a dispatcher calling `ctx.getSignedKey()` starts
125
+ * reading the body even if Fedify then declines the request. Reading in paused
126
+ * mode, rather than through `Readable.toWeb()`, lets `restore()` unshift those
127
+ * chunks back.
111
128
  */
112
- function lazyRequestBody(message) {
113
- let reader;
114
- return new ReadableStream({
115
- async pull(controller) {
116
- reader ??= node_stream.Readable.toWeb(message).getReader();
117
- const { done, value } = await reader.read();
118
- if (done) controller.close();
119
- else controller.enqueue(value);
120
- },
121
- cancel(reason) {
122
- return reader?.cancel(reason);
129
+ function createRequestBody(message) {
130
+ const consumed = [];
131
+ const restoring = new AbortController();
132
+ let started = false;
133
+ let restored = false;
134
+ return {
135
+ stream: new ReadableStream({
136
+ async pull(controller) {
137
+ started = true;
138
+ while (!restored) {
139
+ const size = message.readableLength;
140
+ const chunk = size > 0 ? message.read(size) : null;
141
+ if (chunk != null) {
142
+ const bytes = typeof chunk === "string" ? node_buffer.Buffer.from(chunk) : chunk;
143
+ consumed.push(bytes);
144
+ controller.enqueue(bytes);
145
+ return;
146
+ }
147
+ if (isEnded(message)) break;
148
+ if (message.destroyed) throw new Error("The request was closed before its body ended.");
149
+ await waitForReadable(message, restoring.signal);
150
+ }
151
+ controller.close();
152
+ },
153
+ cancel(reason) {
154
+ if (started && !restored) message.destroy(reason instanceof Error ? reason : void 0);
155
+ }
156
+ }, { highWaterMark: 0 }),
157
+ restore() {
158
+ restored = true;
159
+ restoring.abort();
160
+ for (let i = consumed.length - 1; i >= 0; i--) message.unshift(consumed[i]);
161
+ consumed.length = 0;
123
162
  }
124
- }, { highWaterMark: 0 });
163
+ };
164
+ }
165
+ /**
166
+ * Tells whether the stream has received all of its data, even if it has not
167
+ * emitted `"end"` yet. No public API exposes this, so it reads the internal
168
+ * state, which Node.js, Deno, and Bun all provide, and falls back to
169
+ * `readableEnded` elsewhere.
170
+ */
171
+ function isEnded(message) {
172
+ return message._readableState?.ended ?? message.readableEnded;
173
+ }
174
+ function waitForReadable(message, signal) {
175
+ return new Promise((resolve, reject) => {
176
+ if (signal.aborted) return resolve();
177
+ const cleanup = () => {
178
+ message.off("readable", onReady);
179
+ message.off("end", onReady);
180
+ message.off("close", onReady);
181
+ message.off("error", onError);
182
+ signal.removeEventListener("abort", onReady);
183
+ };
184
+ const onReady = () => {
185
+ cleanup();
186
+ resolve();
187
+ };
188
+ const onError = (error) => {
189
+ cleanup();
190
+ reject(error);
191
+ };
192
+ message.on("readable", onReady);
193
+ message.on("end", onReady);
194
+ message.on("close", onReady);
195
+ message.on("error", onError);
196
+ signal.addEventListener("abort", onReady);
197
+ });
125
198
  }
126
199
  //#endregion
127
200
  exports.default = fedifyPlugin;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import fp from "fastify-plugin";
2
- import { Readable } from "node:stream";
2
+ import { Buffer } from "node:buffer";
3
3
  //#region src/index.ts
4
4
  /**
5
5
  * Fastify plugin that integrates with a Federation instance.
@@ -34,7 +34,7 @@ import { Readable } from "node:stream";
34
34
  const fedifyPluginCore = (fastify, options) => {
35
35
  const { federation, contextDataFactory = () => void 0, errorHandlers } = options;
36
36
  fastify.addHook("onRequest", async (request, reply) => {
37
- const webRequest = toWebRequest(request);
37
+ const { request: webRequest, body } = toWebRequest(request);
38
38
  const contextData = await contextDataFactory(request);
39
39
  const response = await federation.fetch(webRequest, {
40
40
  contextData,
@@ -42,7 +42,10 @@ const fedifyPluginCore = (fastify, options) => {
42
42
  onNotFound: () => dummyNotFoundResponse,
43
43
  ...errorHandlers
44
44
  });
45
- if (response === dummyNotFoundResponse) return;
45
+ if (response === dummyNotFoundResponse) {
46
+ body?.restore();
47
+ return;
48
+ }
46
49
  await reply.send(response);
47
50
  });
48
51
  return Promise.resolve();
@@ -67,34 +70,104 @@ function toWebRequest(fastifyReq) {
67
70
  const headers = new Headers();
68
71
  for (const [key, value] of Object.entries(fastifyReq.raw.headers)) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
69
72
  else if (value !== void 0) headers.set(key, String(value));
70
- const body = fastifyReq.method === "GET" || fastifyReq.method === "HEAD" ? void 0 : fastifyReq.body !== void 0 ? typeof fastifyReq.body === "string" ? fastifyReq.body : JSON.stringify(fastifyReq.body) : lazyRequestBody(fastifyReq.raw);
71
- return new Request(url, {
72
- method: fastifyReq.method,
73
- headers,
74
- body,
75
- duplex: "half"
76
- });
73
+ let body;
74
+ let rawBody;
75
+ if (fastifyReq.method === "GET" || fastifyReq.method === "HEAD") body = void 0;
76
+ else if (fastifyReq.body !== void 0) body = typeof fastifyReq.body === "string" ? fastifyReq.body : JSON.stringify(fastifyReq.body);
77
+ else {
78
+ rawBody = createRequestBody(fastifyReq.raw);
79
+ body = rawBody.stream;
80
+ }
81
+ return {
82
+ request: new Request(url, {
83
+ method: fastifyReq.method,
84
+ headers,
85
+ body,
86
+ duplex: "half"
87
+ }),
88
+ body: rawBody
89
+ };
77
90
  }
78
91
  /**
79
92
  * Wraps the raw `IncomingMessage` in a `ReadableStream` that stays inert until
80
- * first read. The `Request` is built in the `onRequest` hook, before Fedify
81
- * decides whether it handles the request; an eager `Readable.toWeb()` would
82
- * start draining the socket and break Fastify's own body parsing of the
83
- * declined requests.
93
+ * first read, and that keeps the chunks it reads so that they can be put back.
94
+ *
95
+ * The `Request` is built before Fedify decides whether it handles the request.
96
+ * Laziness alone is not enough: `Request.clone()` tees the body, and a tee pulls
97
+ * a chunk right away, so a dispatcher calling `ctx.getSignedKey()` starts
98
+ * reading the body even if Fedify then declines the request. Reading in paused
99
+ * mode, rather than through `Readable.toWeb()`, lets `restore()` unshift those
100
+ * chunks back.
84
101
  */
85
- function lazyRequestBody(message) {
86
- let reader;
87
- return new ReadableStream({
88
- async pull(controller) {
89
- reader ??= Readable.toWeb(message).getReader();
90
- const { done, value } = await reader.read();
91
- if (done) controller.close();
92
- else controller.enqueue(value);
93
- },
94
- cancel(reason) {
95
- return reader?.cancel(reason);
102
+ function createRequestBody(message) {
103
+ const consumed = [];
104
+ const restoring = new AbortController();
105
+ let started = false;
106
+ let restored = false;
107
+ return {
108
+ stream: new ReadableStream({
109
+ async pull(controller) {
110
+ started = true;
111
+ while (!restored) {
112
+ const size = message.readableLength;
113
+ const chunk = size > 0 ? message.read(size) : null;
114
+ if (chunk != null) {
115
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
116
+ consumed.push(bytes);
117
+ controller.enqueue(bytes);
118
+ return;
119
+ }
120
+ if (isEnded(message)) break;
121
+ if (message.destroyed) throw new Error("The request was closed before its body ended.");
122
+ await waitForReadable(message, restoring.signal);
123
+ }
124
+ controller.close();
125
+ },
126
+ cancel(reason) {
127
+ if (started && !restored) message.destroy(reason instanceof Error ? reason : void 0);
128
+ }
129
+ }, { highWaterMark: 0 }),
130
+ restore() {
131
+ restored = true;
132
+ restoring.abort();
133
+ for (let i = consumed.length - 1; i >= 0; i--) message.unshift(consumed[i]);
134
+ consumed.length = 0;
96
135
  }
97
- }, { highWaterMark: 0 });
136
+ };
137
+ }
138
+ /**
139
+ * Tells whether the stream has received all of its data, even if it has not
140
+ * emitted `"end"` yet. No public API exposes this, so it reads the internal
141
+ * state, which Node.js, Deno, and Bun all provide, and falls back to
142
+ * `readableEnded` elsewhere.
143
+ */
144
+ function isEnded(message) {
145
+ return message._readableState?.ended ?? message.readableEnded;
146
+ }
147
+ function waitForReadable(message, signal) {
148
+ return new Promise((resolve, reject) => {
149
+ if (signal.aborted) return resolve();
150
+ const cleanup = () => {
151
+ message.off("readable", onReady);
152
+ message.off("end", onReady);
153
+ message.off("close", onReady);
154
+ message.off("error", onError);
155
+ signal.removeEventListener("abort", onReady);
156
+ };
157
+ const onReady = () => {
158
+ cleanup();
159
+ resolve();
160
+ };
161
+ const onError = (error) => {
162
+ cleanup();
163
+ reject(error);
164
+ };
165
+ message.on("readable", onReady);
166
+ message.on("end", onReady);
167
+ message.on("close", onReady);
168
+ message.on("error", onError);
169
+ signal.addEventListener("abort", onReady);
170
+ });
98
171
  }
99
172
  //#endregion
100
173
  export { fedifyPlugin as default, fedifyPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/fastify",
3
- "version": "2.0.29-dev.2053+58b638b4",
3
+ "version": "2.0.29-dev.2055+5e21857f",
4
4
  "description": "Integrate Fedify with Fastify",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "peerDependencies": {
50
50
  "fastify": "^5.2.0",
51
- "@fedify/fedify": "^2.0.29-dev.2053+58b638b4"
51
+ "@fedify/fedify": "^2.0.29-dev.2055+5e21857f"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.17.0",