Repository: solid/community-server (ts)

Issue:
Lock is not released on client hangup
When the client requests a large file, and drops the connection in the middle of transfer, the lock is not released.

```
2021-01-28T20:33:43.073Z [WrappedExpiringResourceLocker] error: Lock expired after 3000ms on http://localhost:3000/joachim-pizza.jpg
2021-01-28T20:33:43.074Z [StreamUtil] warn: Piped stream errored with Lock expired after 3000ms on http://localhost:3000/joachim-pizza.jpg
```

Pull request:
fix: Close unpiped streams
Closes #564 

Patch:
diff --git a/config/presets/http.json b/config/presets/http.json
index cde5b6fcc..028bde8ad 100644
--- a/config/presets/http.json
+++ b/config/presets/http.json
@@ -43,6 +43,17 @@
           ]
         }
       ]
+    },
+    {
+      "@id": "urn:solid-server:default:FaviconHandler",
+      "@type": "StaticAssetHandler",
+      "comment": "Serves the favicon",
+      "StaticAssetHandler:_assets": [
+        {
+          "StaticAssetHandler:_assets_key": "/favicon.ico",
+          "StaticAssetHandler:_assets_value": "./templates/root/favicon.ico"
+        }
+      ]
     }
   ]
 }
diff --git a/config/presets/static.json b/config/presets/static.json
index 34e0d5229..aa253b1ec 100644
--- a/config/presets/static.json
+++ b/config/presets/static.json
@@ -10,17 +10,6 @@
       "@id": "urn:solid-server:default:StaticAssetHandler",
       "@type": "UnsupportedAsyncHandler",
       "comment": "This value can be used to set a custom handler for static assets"
-    },
-    {
-      "@id": "urn:solid-server:default:FaviconHandler",
-      "@type": "StaticAssetHandler",
-      "comment": "Serves the favicon",
-      "StaticAssetHandler:_assets": [
-        {
-          "StaticAssetHandler:_assets_key": "/favicon.ico",
-          "StaticAssetHandler:_assets_value": "./templates/root/favicon.ico"
-        }
-      ]
     }
   ]
 }
diff --git a/src/util/StreamUtil.ts b/src/util/StreamUtil.ts
index 555c27825..c878fc0f7 100644
--- a/src/util/StreamUtil.ts
+++ b/src/util/StreamUtil.ts
@@ -40,6 +40,16 @@ export function pipeSafely<T extends Writable>(readable: NodeJS.ReadableStream,
     // in order to prevent memory leaks."
     destination.destroy(mapError ? mapError(error) : error);
   });
+
+  // Make sure we have no dangling streams in case of unpiping, which can happen if there's an error.
+  // This can also happen if a stream naturally closes so most calls here will not be indication of a problem.
+  // From https://nodejs.org/api/stream.html#stream_errors_while_writing :
+  // "If a Readable stream pipes into a Writable stream when Writable emits an error,
+  // the Readable stream will be unpiped."
+  destination.on('unpipe', (source): void => {
+    source.destroy();
+  });
+
   return guardStream(destination);
 }
 


Test patch:
diff --git a/test/unit/util/StreamUtil.test.ts b/test/unit/util/StreamUtil.test.ts
index a9ce2dc66..70bfeb448 100644
--- a/test/unit/util/StreamUtil.test.ts
+++ b/test/unit/util/StreamUtil.test.ts
@@ -40,6 +40,22 @@ describe('StreamUtil', (): void => {
       const piped = pipeSafely(input, output, (): any => new Error('other error'));
       await expect(readableToString(piped)).rejects.toThrow('other error');
     });
+
+    it('destroys the source stream in case the destinations becomes unpiped.', async(): Promise<void> => {
+      const input = new PassThrough();
+      const output = new PassThrough();
+      const piped = pipeSafely(input, output);
+
+      // Catch errors to prevent problems in test output
+      output.on('error', (): void => {
+        // Empty
+      });
+
+      piped.destroy(new Error('this causes an unpipe!'));
+      // Allow events to propagate
+      await new Promise(setImmediate);
+      expect(input.destroyed).toBe(true);
+    });
   });
 
   describe('#transformSafely', (): void => {