@fedify/express 2.4.2-dev.2362 → 2.4.3

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
@@ -6,9 +6,10 @@ function integrateFederation(federation, contextDataFactory) {
6
6
  const body = req.method === "GET" || req.method === "HEAD" ? void 0 : createRequestBody(req);
7
7
  const request = fromERequest(req, body?.stream);
8
8
  const contextData = contextDataFactory(req);
9
- (contextData instanceof Promise ? contextData : Promise.resolve(contextData)).then(async (contextData) => {
10
- let notFound = false;
11
- let notAcceptable = false;
9
+ const contextDataPromise = contextData instanceof Promise ? contextData : Promise.resolve(contextData);
10
+ let notFound = false;
11
+ let notAcceptable = false;
12
+ contextDataPromise.then(async (contextData) => {
12
13
  const response = await federation.fetch(request, {
13
14
  contextData,
14
15
  onNotFound: () => {
@@ -19,6 +20,7 @@ function integrateFederation(federation, contextDataFactory) {
19
20
  },
20
21
  onNotAcceptable: () => {
21
22
  notAcceptable = true;
23
+ installNotAcceptableFallback(req, res);
22
24
  body?.restore();
23
25
  next();
24
26
  return new Response("Not acceptable", {
@@ -30,7 +32,7 @@ function integrateFederation(federation, contextDataFactory) {
30
32
  });
31
33
  }
32
34
  });
33
- if (notFound || notAcceptable && req.route != null) return;
35
+ if (notFound || notAcceptable) return;
34
36
  await setEResponse(res, response);
35
37
  res.end();
36
38
  res.status = () => res;
@@ -39,6 +41,9 @@ function integrateFederation(federation, contextDataFactory) {
39
41
  res.json = () => res;
40
42
  res.removeHeader = () => res;
41
43
  res.setHeader = () => res;
44
+ }).catch((error) => {
45
+ if (notFound || notAcceptable) throw error;
46
+ next(error === "route" || error === "router" ? new Error(error) : error || /* @__PURE__ */ new Error("The federation middleware promise was rejected."));
42
47
  });
43
48
  };
44
49
  }
@@ -153,5 +158,86 @@ function setEResponse(res, response) {
153
158
  });
154
159
  });
155
160
  }
161
+ /**
162
+ * Waits for downstream handling to finish before replacing an unsent 404.
163
+ * A matched route can still fall through, and next() does not wait for an
164
+ * asynchronous handler. Already committed responses belong to the application.
165
+ */
166
+ function installNotAcceptableFallback(req, res) {
167
+ const end = res.end;
168
+ const send = res.send;
169
+ let pending = true;
170
+ let applicationResponse = false;
171
+ const wrappedSend = function(body) {
172
+ applicationResponse = true;
173
+ return send.call(this, body);
174
+ };
175
+ res.send = wrappedSend;
176
+ const wrapEnd = (end) => function(...args) {
177
+ if (pending) {
178
+ Object.defineProperty(this, "end", {
179
+ configurable: true,
180
+ enumerable: true,
181
+ writable: true,
182
+ value: end
183
+ });
184
+ if (this.send === wrappedSend) this.send = send;
185
+ }
186
+ if (!pending || applicationResponse || this.headersSent || this.statusCode !== 404 || !isDefaultNotFound(req, this, args[0])) {
187
+ pending = false;
188
+ return end.apply(this, args);
189
+ }
190
+ pending = false;
191
+ const callback = args.find((arg) => typeof arg === "function");
192
+ const body = "Not acceptable";
193
+ this.statusCode = 406;
194
+ this.statusMessage = "Not Acceptable";
195
+ for (const header of [
196
+ "Content-Encoding",
197
+ "Content-Language",
198
+ "Content-Range",
199
+ "ETag",
200
+ "Last-Modified",
201
+ "Transfer-Encoding"
202
+ ]) this.removeHeader(header);
203
+ this.setHeader("Content-Type", "text/plain; charset=utf-8");
204
+ this.setHeader("Content-Length", node_buffer.Buffer.byteLength(body));
205
+ this.vary("Accept");
206
+ return end.call(this, req.method === "HEAD" ? void 0 : body, "utf8", callback);
207
+ };
208
+ let wrappedEnd = wrapEnd(end);
209
+ Object.defineProperty(res, "end", {
210
+ configurable: true,
211
+ enumerable: true,
212
+ get: () => wrappedEnd,
213
+ set: (value) => {
214
+ wrappedEnd = wrapEnd(value);
215
+ }
216
+ });
217
+ }
218
+ /**
219
+ * Express offers no public router-exhaustion callback to middleware. Recognize
220
+ * finalhandler's default page, failing safe when its format changes. An app
221
+ * response identical to that default page cannot be distinguished from it.
222
+ */
223
+ function isDefaultNotFound(req, res, chunk) {
224
+ if (res.getHeader("Content-Security-Policy") !== "default-src 'none'" || res.getHeader("X-Content-Type-Options") !== "nosniff" || res.getHeader("Content-Type") !== "text/html; charset=utf-8") return false;
225
+ const encodedPath = (req.originalUrl ?? req.url).split("?")[0].replace(/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g, "$1�$2").replace(/(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g, encodeURI);
226
+ const page = `<!DOCTYPE html>
227
+ <html lang="en">
228
+ <head>
229
+ <meta charset="utf-8">
230
+ <title>Error</title>
231
+ </head>
232
+ <body>
233
+ <pre>${`Cannot ${req.method} ${encodedPath}`.replace(/[&<>"']/g, (character) => ({
234
+ "&": "&amp;",
235
+ "<": "&lt;",
236
+ ">": "&gt;",
237
+ "\"": "&quot;",
238
+ "'": "&#39;"
239
+ })[character])}</pre>\n</body>\n</html>\n`;
240
+ return req.method === "HEAD" ? chunk == null && Number(res.getHeader("Content-Length")) === node_buffer.Buffer.byteLength(page) : chunk === page;
241
+ }
156
242
  //#endregion
157
243
  exports.integrateFederation = integrateFederation;
package/dist/index.js CHANGED
@@ -5,9 +5,10 @@ function integrateFederation(federation, contextDataFactory) {
5
5
  const body = req.method === "GET" || req.method === "HEAD" ? void 0 : createRequestBody(req);
6
6
  const request = fromERequest(req, body?.stream);
7
7
  const contextData = contextDataFactory(req);
8
- (contextData instanceof Promise ? contextData : Promise.resolve(contextData)).then(async (contextData) => {
9
- let notFound = false;
10
- let notAcceptable = false;
8
+ const contextDataPromise = contextData instanceof Promise ? contextData : Promise.resolve(contextData);
9
+ let notFound = false;
10
+ let notAcceptable = false;
11
+ contextDataPromise.then(async (contextData) => {
11
12
  const response = await federation.fetch(request, {
12
13
  contextData,
13
14
  onNotFound: () => {
@@ -18,6 +19,7 @@ function integrateFederation(federation, contextDataFactory) {
18
19
  },
19
20
  onNotAcceptable: () => {
20
21
  notAcceptable = true;
22
+ installNotAcceptableFallback(req, res);
21
23
  body?.restore();
22
24
  next();
23
25
  return new Response("Not acceptable", {
@@ -29,7 +31,7 @@ function integrateFederation(federation, contextDataFactory) {
29
31
  });
30
32
  }
31
33
  });
32
- if (notFound || notAcceptable && req.route != null) return;
34
+ if (notFound || notAcceptable) return;
33
35
  await setEResponse(res, response);
34
36
  res.end();
35
37
  res.status = () => res;
@@ -38,6 +40,9 @@ function integrateFederation(federation, contextDataFactory) {
38
40
  res.json = () => res;
39
41
  res.removeHeader = () => res;
40
42
  res.setHeader = () => res;
43
+ }).catch((error) => {
44
+ if (notFound || notAcceptable) throw error;
45
+ next(error === "route" || error === "router" ? new Error(error) : error || /* @__PURE__ */ new Error("The federation middleware promise was rejected."));
41
46
  });
42
47
  };
43
48
  }
@@ -152,5 +157,86 @@ function setEResponse(res, response) {
152
157
  });
153
158
  });
154
159
  }
160
+ /**
161
+ * Waits for downstream handling to finish before replacing an unsent 404.
162
+ * A matched route can still fall through, and next() does not wait for an
163
+ * asynchronous handler. Already committed responses belong to the application.
164
+ */
165
+ function installNotAcceptableFallback(req, res) {
166
+ const end = res.end;
167
+ const send = res.send;
168
+ let pending = true;
169
+ let applicationResponse = false;
170
+ const wrappedSend = function(body) {
171
+ applicationResponse = true;
172
+ return send.call(this, body);
173
+ };
174
+ res.send = wrappedSend;
175
+ const wrapEnd = (end) => function(...args) {
176
+ if (pending) {
177
+ Object.defineProperty(this, "end", {
178
+ configurable: true,
179
+ enumerable: true,
180
+ writable: true,
181
+ value: end
182
+ });
183
+ if (this.send === wrappedSend) this.send = send;
184
+ }
185
+ if (!pending || applicationResponse || this.headersSent || this.statusCode !== 404 || !isDefaultNotFound(req, this, args[0])) {
186
+ pending = false;
187
+ return end.apply(this, args);
188
+ }
189
+ pending = false;
190
+ const callback = args.find((arg) => typeof arg === "function");
191
+ const body = "Not acceptable";
192
+ this.statusCode = 406;
193
+ this.statusMessage = "Not Acceptable";
194
+ for (const header of [
195
+ "Content-Encoding",
196
+ "Content-Language",
197
+ "Content-Range",
198
+ "ETag",
199
+ "Last-Modified",
200
+ "Transfer-Encoding"
201
+ ]) this.removeHeader(header);
202
+ this.setHeader("Content-Type", "text/plain; charset=utf-8");
203
+ this.setHeader("Content-Length", Buffer.byteLength(body));
204
+ this.vary("Accept");
205
+ return end.call(this, req.method === "HEAD" ? void 0 : body, "utf8", callback);
206
+ };
207
+ let wrappedEnd = wrapEnd(end);
208
+ Object.defineProperty(res, "end", {
209
+ configurable: true,
210
+ enumerable: true,
211
+ get: () => wrappedEnd,
212
+ set: (value) => {
213
+ wrappedEnd = wrapEnd(value);
214
+ }
215
+ });
216
+ }
217
+ /**
218
+ * Express offers no public router-exhaustion callback to middleware. Recognize
219
+ * finalhandler's default page, failing safe when its format changes. An app
220
+ * response identical to that default page cannot be distinguished from it.
221
+ */
222
+ function isDefaultNotFound(req, res, chunk) {
223
+ if (res.getHeader("Content-Security-Policy") !== "default-src 'none'" || res.getHeader("X-Content-Type-Options") !== "nosniff" || res.getHeader("Content-Type") !== "text/html; charset=utf-8") return false;
224
+ const encodedPath = (req.originalUrl ?? req.url).split("?")[0].replace(/(^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF]([^\uDC00-\uDFFF]|$)/g, "$1�$2").replace(/(?:[^\x21\x23-\x3B\x3D\x3F-\x5F\x61-\x7A\x7C\x7E]|%(?:[^0-9A-Fa-f]|[0-9A-Fa-f][^0-9A-Fa-f]|$))+/g, encodeURI);
225
+ const page = `<!DOCTYPE html>
226
+ <html lang="en">
227
+ <head>
228
+ <meta charset="utf-8">
229
+ <title>Error</title>
230
+ </head>
231
+ <body>
232
+ <pre>${`Cannot ${req.method} ${encodedPath}`.replace(/[&<>"']/g, (character) => ({
233
+ "&": "&amp;",
234
+ "<": "&lt;",
235
+ ">": "&gt;",
236
+ "\"": "&quot;",
237
+ "'": "&#39;"
238
+ })[character])}</pre>\n</body>\n</html>\n`;
239
+ return req.method === "HEAD" ? chunk == null && Number(res.getHeader("Content-Length")) === Buffer.byteLength(page) : chunk === page;
240
+ }
155
241
  //#endregion
156
242
  export { integrateFederation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/express",
3
- "version": "2.4.2-dev.2362+46a1084e",
3
+ "version": "2.4.3",
4
4
  "description": "Integrate Fedify with Express",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -46,13 +46,14 @@
46
46
  ],
47
47
  "peerDependencies": {
48
48
  "express": ">=4.0.0",
49
- "@fedify/fedify": "^2.4.2-dev.2362+46a1084e"
49
+ "@fedify/fedify": "^2.4.3"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/express": "^4.0.0",
53
53
  "@types/node": "^24.12.2",
54
54
  "tsdown": "^0.22.0",
55
- "typescript": "^6.0.0"
55
+ "typescript": "^6.0.0",
56
+ "@fedify/fixture": "2.0.0"
56
57
  },
57
58
  "scripts": {
58
59
  "build:self": "tsdown",