@lenne.tech/nest-server 11.42.1 → 11.42.2

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.
@@ -0,0 +1,53 @@
1
+ # Migration Guide: 11.42.1 → 11.42.2
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ | -------------------- | ------------------------------------------------------------------------ |
7
+ | **Bugfix** | Finished TUS uploads answer 204 behind the `compression` middleware (§1) |
8
+ | **Migration Effort** | None — `pnpm update` |
9
+
10
+ ## Quick Migration
11
+
12
+ ```bash
13
+ pnpm update @lenne.tech/nest-server
14
+ ```
15
+
16
+ Vendor-mode projects pick up the core update; nothing else to change.
17
+
18
+ ## 1. A finished TUS upload never got an answer although the file was stored
19
+
20
+ `@tus/server` finishes its responses with `res.end(callback)`, a documented Node signature. The
21
+ `compression` middleware patches `res.end(chunk, encoding)` and treats the first argument as a chunk,
22
+ so the callback reached `Buffer.byteLength()` and threw:
23
+
24
+ ```
25
+ TypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer … Received function
26
+ ```
27
+
28
+ The starter's `main.ts` — and therefore every generated project — registers `compression` with
29
+ `filter: () => true` and `threshold: 0`, which removes the two checks that would otherwise skip a
30
+ bodyless 204. The failure hit the final `PATCH`, AFTER the upload had been migrated into the file
31
+ store: the request hung — the headers were already sent, so no error response could follow — the
32
+ client never saw a 2xx, `tus-js-client` never fired `onSuccess`, and no log named `compression`.
33
+
34
+ Since 11.42.2 `CoreTusService` normalises `res.end(callback)` before the tus server writes
35
+ (`normalizeEndCallback()`, wrapped around `getServer().handle()`), so it holds for the core controller
36
+ and for any project controller that calls the tus server. No change to `main.ts` is needed; a project
37
+ that worked around it by excluding `/tus` from the compression filter can keep or drop that filter.
38
+
39
+ ## 2. TUS request errors are logged with their stack
40
+
41
+ `CoreTusController` logged only `error.message` for a failed TUS request. A `TypeError` thrown by a
42
+ middleware's patched `res.end` then read like a tus failure, with nothing pointing at the middleware —
43
+ the defect in §1 took several rounds to find for exactly that reason. The stack is now logged too.
44
+
45
+ ## Troubleshooting
46
+
47
+ | Symptom | Cause | Fix |
48
+ | -------------------------------------------------------------------------------------- | ------------------------------------- | ----------------- |
49
+ | File stored, final PATCH never answers (request hangs), `Received function` in the log | 11.42.1 or older behind `compression` | Update to 11.42.2 |
50
+
51
+ ## Module Documentation
52
+
53
+ - [TUS README](../src/core/modules/tus/README.md) — Troubleshooting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.42.1",
3
+ "version": "11.42.2",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -526,6 +526,21 @@ from elsewhere (an API key, a signed form token), or to let a support role resum
526
526
  2. Verify MongoDB connection is established
527
527
  3. Check server logs for initialization errors
528
528
 
529
+ ### Upload is stored but the client never sees success (fixed in 11.42.2)
530
+
531
+ **Symptom:** the file appears in the file store (`Upload … migrated to …` in the log), but the final
532
+ `PATCH` never answers (the request hangs: the headers are already sent, so no error response can
533
+ follow), `tus-js-client` never fires `onSuccess`, and the log shows
534
+ `ERR_INVALID_ARG_TYPE … Received function`.
535
+
536
+ **Cause:** `@tus/server` finishes its responses with `res.end(callback)`, and the `compression`
537
+ middleware patches `res.end(chunk, encoding)` and reads the callback as a chunk. The starter registers
538
+ `compression` with `filter: () => true` and `threshold: 0`, so it also handles the bodyless 204.
539
+
540
+ **Solution:** update to 11.42.2 — `CoreTusService` normalises `res.end(callback)` before the tus
541
+ server writes (`normalizeEndCallback()`), for the core controller and any project controller that
542
+ calls `getServer().handle()`. No change to `main.ts` is needed.
543
+
529
544
  ### Upload stalls or fails to resume
530
545
 
531
546
  **Cause:** Upload expired or server restarted
@@ -105,7 +105,9 @@ export class CoreTusController {
105
105
  try {
106
106
  await server.handle(req, res);
107
107
  } catch (error) {
108
- this.logger.error(`TUS request error: ${error.message}`);
108
+ // The stack, not just the message: a TypeError from a middleware's patched `res.end` reads
109
+ // like a tus failure without it, and that cost a project several rounds of diagnosis (11.42.2).
110
+ this.logger.error(`TUS request error: ${error.message}`, error?.stack);
109
111
  if (!res.headersSent) {
110
112
  res.status(500).json({ message: 'Upload error' });
111
113
  }
@@ -131,7 +133,7 @@ export class CoreTusController {
131
133
  try {
132
134
  await server.handle(req, res);
133
135
  } catch (error) {
134
- this.logger.error(`TUS request error for upload ${req.params.id}: ${error.message}`);
136
+ this.logger.error(`TUS request error for upload ${req.params.id}: ${error.message}`, error?.stack);
135
137
  if (!res.headersSent) {
136
138
  res.status(500).json({ message: 'Upload error' });
137
139
  }
@@ -54,6 +54,9 @@ export interface CoreTusServiceOptions {
54
54
  s3Service?: CoreS3Service;
55
55
  }
56
56
 
57
+ /** Marks a response whose `end()` was already normalised (see `normalizeEndCallback()`). */
58
+ const END_CALLBACK_NORMALIZED = Symbol('tusEndCallbackNormalized');
59
+
57
60
  /**
58
61
  * Core TUS Service
59
62
  *
@@ -794,9 +797,66 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
794
797
  respectForwardedHeaders: true,
795
798
  });
796
799
 
800
+ // Every caller of getServer().handle() — the core controller and any project controller — gets
801
+ // the response normalised before @tus/server writes to it. See normalizeEndCallback().
802
+ const handle = server.handle.bind(server);
803
+ server.handle = (req, res) => {
804
+ this.normalizeEndCallback(res);
805
+ return handle(req, res);
806
+ };
807
+
797
808
  return server;
798
809
  }
799
810
 
811
+ /**
812
+ * Makes `res.end(callback)` safe for middlewares that patch `res.end` with the old two-argument form.
813
+ *
814
+ * @tus/server finishes its responses through srvx with `res.end(callback)` — a documented Node
815
+ * signature. `compression` (and any middleware written like it) patches `res.end(chunk, encoding)`
816
+ * and treats the first argument as a chunk unconditionally, so the callback reaches
817
+ * `Buffer.byteLength()` and throws `ERR_INVALID_ARG_TYPE ... Received function`. The starter
818
+ * registers `compression` with `filter: () => true, threshold: 0`, which removes the checks that
819
+ * would otherwise skip a bodyless 204, so every finished upload failed AFTER the file had been
820
+ * stored: the client never saw a 2xx and retried an upload that was already complete.
821
+ *
822
+ * The wrapper moves a function argument into the callback slot and runs it on `finish`, which is
823
+ * what Node itself does with it — or on `close`, so a connection dropped after `end()` resolves srvx
824
+ * too. A response destroyed before `end()` stays unresolved, exactly as with plain Node.
825
+ * Idempotent per response. Protected so a project can widen it.
826
+ */
827
+ protected normalizeEndCallback(res: any): void {
828
+ if (!res || typeof res.end !== 'function' || res[END_CALLBACK_NORMALIZED]) {
829
+ return;
830
+ }
831
+ const end = res.end;
832
+ res.end = function normalizedEnd(this: any, chunk?: unknown, encoding?: unknown, callback?: unknown) {
833
+ if (typeof chunk === 'function') {
834
+ callback = chunk;
835
+ chunk = undefined;
836
+ encoding = undefined;
837
+ } else if (typeof encoding === 'function') {
838
+ callback = encoding;
839
+ encoding = undefined;
840
+ }
841
+ if (typeof callback === 'function') {
842
+ let called = false;
843
+ const done = () => {
844
+ if (!called) {
845
+ called = true;
846
+ (callback as () => void)();
847
+ }
848
+ };
849
+ this.once('finish', done);
850
+ this.once('close', done);
851
+ }
852
+ if (chunk === undefined) {
853
+ return end.call(this);
854
+ }
855
+ return encoding === undefined ? end.call(this, chunk) : end.call(this, chunk, encoding);
856
+ };
857
+ res[END_CALLBACK_NORMALIZED] = true;
858
+ }
859
+
800
860
  /**
801
861
  * Parse TUS metadata into object
802
862
  * @tus/server v2 already parses metadata into an object