Repository: solid/community-server (ts)

Issue:
IDP BadRequestHttpError when using sparql endpoint
Hi, I don't know if this is actually a bug or just a problem in my configuration...
When I use a sparql endpoint as a storage medium (`config/sparql-endpoint.json`), if I try to open IDP pages in the browser (`idp/login` or `idp/register`) I always get a  response like this:

**500** Internal Server Error
**Content-Type:** text/plain; charset=utf-8
**Body:**
> BadRequestHttpError: Missing context link header for media type application/json on http://localhost:3000/idp/data/L2lkcC9qd2tz

I'm using the latest version of CSS.

Pull request:
fix: Use memory key/value storage for sparql backends
Closes #840 .

Due to all the work on storage I realized what the problem was and how to easily fix it.

The problem is that the sparql store was using the ResourceStore for the internal storage. But in that case the store only accepts RDF while the internal storage uses JSON. This explains the error message in the issue above since the converter tried to conver the JSON to JSON-LD and was missing the context. This also means #864 completely broke the sparql config since there we made it so the locker also used the ResourceStore for internal storage.

The solution is to just use the memory storage instead since that one does accept everything. It is partially a band-aid solution though since this means all IDP data (e.g. registrations) will be lost on server restart.

A more comprehensive solution would be to either set up a new file store specifically for the internal storage, or store the JSON documents in the sparql store as strings.

Patch:
diff --git a/config/http/static/default.json b/config/http/static/default.json
index f3b8bbe00..27b3eb457 100644
--- a/config/http/static/default.json
+++ b/config/http/static/default.json
@@ -5,6 +5,7 @@
       "comment": "Servers static files on fixed URLs.",
       "@id": "urn:solid-server:default:StaticAssetHandler",
       "@type": "StaticAssetHandler",
+      "options_expires": 86400,
       "assets": [
         {
           "StaticAssetHandler:_assets_key": "/favicon.ico",
diff --git a/config/path-routing.json b/config/path-routing.json
index 1da9fc335..67e3df917 100644
--- a/config/path-routing.json
+++ b/config/path-routing.json
@@ -19,7 +19,7 @@
     "files-scs:config/ldp/metadata-writer/default.json",
     "files-scs:config/ldp/permissions/acl.json",
     "files-scs:config/storage/backend/regex.json",
-    "files-scs:config/storage/key-value/resource-store.json",
+    "files-scs:config/storage/key-value/memory.json",
     "files-scs:config/storage/middleware/default.json",
     "files-scs:config/util/auxiliary/acl.json",
     "files-scs:config/util/identifiers/suffix.json",
diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json
index 5966d1c05..0cba57d31 100644
--- a/config/sparql-endpoint.json
+++ b/config/sparql-endpoint.json
@@ -19,7 +19,7 @@
     "files-scs:config/ldp/metadata-writer/default.json",
     "files-scs:config/ldp/permissions/acl.json",
     "files-scs:config/storage/backend/sparql.json",
-    "files-scs:config/storage/key-value/resource-store.json",
+    "files-scs:config/storage/key-value/memory.json",
     "files-scs:config/storage/middleware/default.json",
     "files-scs:config/util/auxiliary/acl.json",
     "files-scs:config/util/identifiers/suffix.json",
@@ -31,7 +31,10 @@
   ],
   "@graph": [
     {
-      "comment": "A single-pod server that stores its resources in a SPARQL endpoint."
+      "comment": [
+        "A single-pod server that stores its resources in a SPARQL endpoint.",
+        "This server only supports RDF data. For this reason it can not use its resource store for internal key/value storage."
+      ]
     }
   ]
 }
diff --git a/src/server/middleware/StaticAssetHandler.ts b/src/server/middleware/StaticAssetHandler.ts
index b91d31c93..61091453f 100644
--- a/src/server/middleware/StaticAssetHandler.ts
+++ b/src/server/middleware/StaticAssetHandler.ts
@@ -20,20 +20,23 @@ import type { HttpRequest } from '../HttpRequest';
 export class StaticAssetHandler extends HttpHandler {
   private readonly mappings: Record<string, string>;
   private readonly pathMatcher: RegExp;
+  private readonly expires: number;
   private readonly logger = getLoggerFor(this);
 
   /**
    * Creates a handler for the provided static resources.
    * @param assets - A mapping from URL paths to paths,
    *  where URL paths ending in a slash are interpreted as entire folders.
+   * @param options - Cache expiration time in seconds.
    */
-  public constructor(assets: Record<string, string>) {
+  public constructor(assets: Record<string, string>, options: { expires?: number } = {}) {
     super();
     this.mappings = {};
     for (const [ url, path ] of Object.entries(assets)) {
       this.mappings[url] = resolveAssetPath(path);
     }
     this.pathMatcher = this.createPathMatcher(assets);
+    this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires!) : 0;
   }
 
   /**
@@ -90,7 +93,10 @@ export class StaticAssetHandler extends HttpHandler {
       // Write a 200 response when the asset becomes readable
       asset.once('readable', (): void => {
         const contentType = mime.lookup(filePath) || APPLICATION_OCTET_STREAM;
-        response.writeHead(200, { 'content-type': contentType });
+        response.writeHead(200, {
+          'content-type': contentType,
+          ...this.getCacheHeaders(),
+        });
 
         // With HEAD, only write the headers
         if (request.method === 'HEAD') {
@@ -120,4 +126,13 @@ export class StaticAssetHandler extends HttpHandler {
       });
     });
   }
+
+  private getCacheHeaders(): Record<string, string> {
+    return this.expires <= 0 ?
+      {} :
+      {
+        'cache-control': `max-age=${this.expires}`,
+        expires: new Date(Date.now() + (this.expires * 1000)).toUTCString(),
+      };
+  }
 }


Test patch:
diff --git a/test/unit/server/middleware/StaticAssetHandler.test.ts b/test/unit/server/middleware/StaticAssetHandler.test.ts
index 131951d94..bc7dbd7c7 100644
--- a/test/unit/server/middleware/StaticAssetHandler.test.ts
+++ b/test/unit/server/middleware/StaticAssetHandler.test.ts
@@ -213,4 +213,21 @@ describe('A StaticAssetHandler', (): void => {
     const response = createResponse({ eventEmitter: EventEmitter });
     await expect(handler.canHandle({ request, response } as any)).rejects.toThrow();
   });
+
+  it('caches responses when the expires option is set.', async(): Promise<void> => {
+    jest.spyOn(Date, 'now').mockReturnValue(0);
+    const cachedHandler = new StaticAssetHandler({
+      '/foo/bar/style': '/assets/styles/bar.css',
+    }, {
+      expires: 86400,
+    });
+    const request = { method: 'GET', url: '/foo/bar/style' };
+    const response = createResponse();
+    await cachedHandler.handleSafe({ request, response } as any);
+    jest.restoreAllMocks();
+
+    expect(response.statusCode).toBe(200);
+    expect(response.getHeaders()).toHaveProperty('cache-control', 'max-age=86400');
+    expect(response.getHeaders()).toHaveProperty('expires', 'Fri, 02 Jan 1970 00:00:00 GMT');
+  });
 });