@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.
- package/.claude/rules/configurable-features.md +1 -1
- package/.claude/rules/testing.md +3 -3
- package/FRAMEWORK-API.md +1 -1
- package/dist/core/modules/tus/core-tus.controller.js +2 -2
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +1 -0
- package/dist/core/modules/tus/core-tus.service.js +39 -0
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.42.1-to-11.42.2.md +53 -0
- package/package.json +1 -1
- package/src/core/modules/tus/README.md +15 -0
- package/src/core/modules/tus/core-tus.controller.ts +4 -2
- package/src/core/modules/tus/core-tus.service.ts +60 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|