Unverified Commit a9930cad authored by Revath S Kumar's avatar Revath S Kumar
Browse files

fix(docLoader): handle exception from kvstore and log error message

In case of an exception when writing or reading from kvStore,
fedify logs the below error, which is confusing and not helpful
for the developers.

```
jsonld.InvalidUrl: Dereferencing a URL did not result in a valid JSON-LD
object. Possible causes are a
n inaccessible URL perhaps due to a same-origin policy (ensure the
server uses CORS if you are using client-side JavaScript), too many
redirects, a non-JSON response, or more than one HTTP Link Header was
provided for a remote context.
URL: "https://w3id.org/security/multikey/v1".
```

So, this patch tries to handle the exception from kvStore and log the error message.

Steps to reproduce the problem

* Use `@fedify/postgres` as kvStore.
* Configure invalid database url.
* Try `curl  -H"Accept: application/activity+json" http://localhost:3000/users/demo`
parent 3feb71c5
Loading
Loading
Loading
Loading
+44 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import * as mf from "mock_fetch";
import process from "node:process";
import metadata from "../deno.json" with { type: "json" };
import type { KvKey, KvStore, KvStoreSetOptions } from "../federation/kv.ts";
import { MemoryKvStore } from "../federation/kv.ts";
import { verifyRequest } from "../sig/http.ts";
import { mockDocumentLoader } from "../testing/docloader.ts";
@@ -548,6 +549,49 @@ test("kvCache()", async (t) => {
      "The maximum cache duration is 30 days",
    );
  });

  await t.step("on kv store exception", async () => {
    class KvStoreThrowsException implements KvStore {
      get<T = unknown>(_key: KvKey): Promise<T | undefined> {
        throw new Error("Failed to get key");
      }
      set(
        _key: KvKey,
        _value: unknown,
        _options?: KvStoreSetOptions,
      ): Promise<void> {
        throw new Error("Failed to set key");
      }
      delete(_key: KvKey): Promise<void> {
        throw new Error("Failed to delete key");
      }
    }

    const loader = kvCache({
      kv: new KvStoreThrowsException(),
      loader: mockDocumentLoader,
      rules: [
        ["https://example.org/", Temporal.Duration.from({ days: 1 })],
        [new URL("https://example.net/"), Temporal.Duration.from({ days: 1 })],
        [
          new URLPattern("https://example.com/*"),
          Temporal.Duration.from({ days: 30 }),
        ],
      ],
      prefix: ["_test", "not cached"],
    });
    const result = await loader("https://example.com/object");
    assertEquals(result, {
      contextUrl: null,
      documentUrl: "https://example.com/object",
      document: {
        "@context": "https://www.w3.org/ns/activitystreams",
        id: "https://example.com/object",
        name: "Fetched object",
        type: "Object",
      },
    });
  });
});

test("getUserAgent()", () => {
+17 −2
Original line number Diff line number Diff line
@@ -495,10 +495,25 @@ export function kvCache(
    const match = matchRule(url);
    if (match == null) return await loader(url);
    const key: KvKey = [...keyPrefix, url];
    const cache = await kv.get<RemoteDocument>(key);
    let cache: RemoteDocument | undefined = undefined;
    try {
      cache = await kv.get<RemoteDocument>(key);
    } catch (err) {
      if (err instanceof Error) {
        logger.warn(`Failed to get value from kv cache.`);
        logger.error(err.message);
      }
    }
    if (cache == null) {
      const remoteDoc = await loader(url);
      try {
        await kv.set(key, remoteDoc, { ttl: match });
      } catch (err) {
        if (err instanceof Error) {
          logger.warn(`Failed to get value from kv cache.`);
          logger.error(err.message);
        }
      }
      return remoteDoc;
    }
    return cache;