@fedify/express 2.2.17 → 2.2.18

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
@@ -20,6 +20,7 @@ function integrateFederation(federation, contextDataFactory) {
20
20
  },
21
21
  onNotAcceptable: () => {
22
22
  notAcceptable = true;
23
+ installNotAcceptableFallback(req, res);
23
24
  body?.restore();
24
25
  next();
25
26
  return new Response("Not acceptable", {
@@ -31,7 +32,7 @@ function integrateFederation(federation, contextDataFactory) {
31
32
  });
32
33
  }
33
34
  });
34
- if (notFound || notAcceptable && req.route != null) return;
35
+ if (notFound || notAcceptable) return;
35
36
  await setEResponse(res, response);
36
37
  res.end();
37
38
  res.status = () => res;
@@ -157,5 +158,86 @@ function setEResponse(res, response) {
157
158
  });
158
159
  });
159
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
+ }
160
242
  //#endregion
161
243
  exports.integrateFederation = integrateFederation;
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ function integrateFederation(federation, contextDataFactory) {
19
19
  },
20
20
  onNotAcceptable: () => {
21
21
  notAcceptable = true;
22
+ installNotAcceptableFallback(req, res);
22
23
  body?.restore();
23
24
  next();
24
25
  return new Response("Not acceptable", {
@@ -30,7 +31,7 @@ function integrateFederation(federation, contextDataFactory) {
30
31
  });
31
32
  }
32
33
  });
33
- if (notFound || notAcceptable && req.route != null) return;
34
+ if (notFound || notAcceptable) return;
34
35
  await setEResponse(res, response);
35
36
  res.end();
36
37
  res.status = () => res;
@@ -156,5 +157,86 @@ function setEResponse(res, response) {
156
157
  });
157
158
  });
158
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
+ }
159
241
  //#endregion
160
242
  export { integrateFederation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/express",
3
- "version": "2.2.17",
3
+ "version": "2.2.18",
4
4
  "description": "Integrate Fedify with Express",
5
5
  "keywords": [
6
6
  "Fedify",
@@ -49,13 +49,14 @@
49
49
  ],
50
50
  "peerDependencies": {
51
51
  "express": ">=4.0.0",
52
- "@fedify/fedify": "^2.2.17"
52
+ "@fedify/fedify": "^2.2.18"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/express": "^4.0.0",
56
56
  "@types/node": "^22.17.0",
57
57
  "tsdown": "^0.21.6",
58
- "typescript": "^6.0.0"
58
+ "typescript": "^6.0.0",
59
+ "@fedify/fixture": "2.0.0"
59
60
  },
60
61
  "scripts": {
61
62
  "build:self": "tsdown",