Repository: solid/community-server
Pull request #514 (2021-01-14 23:14:37)

Eliminate repeated existence checks with containsResource function


Patch:
diff --git a/src/authorization/AclManager.ts b/src/authorization/AclManager.ts
index 274a5c5cb..7947a2aac 100644
--- a/src/authorization/AclManager.ts
+++ b/src/authorization/AclManager.ts
@@ -2,6 +2,10 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie
 
 /**
  * Handles where acl resources are stored.
+ *
+ * Solid, §4.3.3: "A given Solid resource MUST NOT be directly associated with more than one ACL auxiliary resource.
+ * A given ACL auxiliary resource MUST NOT be directly associated with more than one Solid resource."
+ * https://solid.github.io/specification/protocol#auxiliary-resources-reserved
  */
 export interface AclManager {
   /**
diff --git a/src/authorization/WebAclAuthorizer.ts b/src/authorization/WebAclAuthorizer.ts
index 66565e2bd..f01fe04cf 100644
--- a/src/authorization/WebAclAuthorizer.ts
+++ b/src/authorization/WebAclAuthorizer.ts
@@ -8,7 +8,6 @@ import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
 import { INTERNAL_QUADS } from '../util/ContentTypes';
 import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
-import { InternalServerError } from '../util/errors/InternalServerError';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { UnauthorizedHttpError } from '../util/errors/UnauthorizedHttpError';
 import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
@@ -44,6 +43,9 @@ export class WebAclAuthorizer extends Authorizer {
   public async handle(input: AuthorizerArgs): Promise<void> {
     const store = await this.getAclRecursive(input.identifier);
     if (await this.aclManager.isAclDocument(input.identifier)) {
+      // Solid, §4.3.3: "To discover, read, create, or modify an ACL auxiliary resource, an acl:agent MUST
+      // have acl:Control privileges per the ACL inheritance algorithm on the resource directly associated with it."
+      // https://solid.github.io/specification/protocol#auxiliary-resources-reserved
       this.checkPermission(input.credentials, store, 'control');
     } else {
       (Object.keys(input.permissions) as (keyof PermissionSet)[]).forEach((key): void => {
@@ -77,6 +79,9 @@ export class WebAclAuthorizer extends Authorizer {
         this.logger.warn(`Agent ${agent.webId} has no ${mode} permissions`);
         throw new ForbiddenHttpError();
       } else {
+        // Solid, §2.1: "When a client does not provide valid credentials when requesting a resource that requires it,
+        // the data pod MUST send a response with a 401 status code (unless 404 is preferred for security reasons)."
+        // https://solid.github.io/specification/protocol#http-server
         this.logger.warn(`Unauthenticated agent has no ${mode} permissions`);
         throw new UnauthorizedHttpError();
       }
@@ -154,7 +159,9 @@ export class WebAclAuthorizer extends Authorizer {
     this.logger.debug(`Traversing to the parent of ${id.path}`);
     if (this.identifierStrategy.isRootContainer(id)) {
       this.logger.error(`No ACL document found for root container ${id.path}`);
-      throw new InternalServerError('No ACL document found for root container');
+      // Solid, §10.1: "In the event that a server can’t apply an ACL to a resource, it MUST deny access."
+      // https://solid.github.io/specification/protocol#web-access-control
+      throw new ForbiddenHttpError('No ACL document found for root container');
     }
     const parent = this.identifierStrategy.getParentContainer(id);
     return this.getAclRecursive(parent, true);
diff --git a/src/init/AclInitializer.ts b/src/init/AclInitializer.ts
index a9c2f7961..fe8632f5f 100644
--- a/src/init/AclInitializer.ts
+++ b/src/init/AclInitializer.ts
@@ -3,8 +3,8 @@ import { BasicRepresentation } from '../ldp/representation/BasicRepresentation';
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
+import { containsResource } from '../storage/StoreUtil';
 import { TEXT_TURTLE } from '../util/ContentTypes';
-import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { ensureTrailingSlash } from '../util/PathUtil';
 import { Initializer } from './Initializer';
 
@@ -30,27 +30,19 @@ export class AclInitializer extends Initializer {
 
   public async handle(): Promise<void> {
     const rootAcl = await this.aclManager.getAclDocument({ path: this.baseUrl });
-    if (!await this.hasRootAclDocument(rootAcl)) {
+    if (!await containsResource(this.store, rootAcl)) {
       await this.setRootAclDocument(rootAcl);
-    }
-  }
-
-  protected async hasRootAclDocument(rootAcl: ResourceIdentifier): Promise<boolean> {
-    try {
-      const result = await this.store.getRepresentation(rootAcl, {});
+    } else {
       this.logger.debug(`Existing root ACL document found at ${rootAcl.path}`);
-      result.data.destroy();
-      return true;
-    } catch (error: unknown) {
-      if (error instanceof NotFoundHttpError) {
-        return false;
-      }
-      throw error;
     }
   }
 
   // Set up ACL so everything can still be done by default
   // Note that this will need to be adapted to go through all the correct channels later on
+  //
+  // Solid, §4.1: "The root container (pim:Storage) MUST have an ACL auxiliary resource directly associated to it.
+  // The associated ACL document MUST include an authorization policy with acl:Control access privilege."
+  // https://solid.github.io/specification/protocol#storage
   protected async setRootAclDocument(rootAcl: ResourceIdentifier): Promise<void> {
     const acl = `@prefix   acl:  <http://www.w3.org/ns/auth/acl#>.
 @prefix  foaf:  <http://xmlns.com/foaf/0.1/>.
diff --git a/src/init/RootContainerInitializer.ts b/src/init/RootContainerInitializer.ts
index ef83094cf..c24bd048f 100644
--- a/src/init/RootContainerInitializer.ts
+++ b/src/init/RootContainerInitializer.ts
@@ -4,8 +4,8 @@ import { RepresentationMetadata } from '../ldp/representation/RepresentationMeta
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
+import { containsResource } from '../storage/StoreUtil';
 import { TEXT_TURTLE } from '../util/ContentTypes';
-import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { ensureTrailingSlash } from '../util/PathUtil';
 import { generateResourceQuads } from '../util/ResourceUtil';
 import { PIM, RDF } from '../util/Vocabularies';
@@ -14,6 +14,10 @@ import namedNode = DataFactory.namedNode;
 
 /**
  * Initializes ResourceStores by creating a root container if it didn't exist yet.
+ *
+ * Solid, §4.1: "When a server supports a data pod, it MUST provide one or more storages (pim:Storage) –
+ * a space of URIs in which data can be accessed. A storage is the root container for all of its contained resources."
+ * https://solid.github.io/specification/protocol#storage
  */
 export class RootContainerInitializer extends Initializer {
   protected readonly logger = getLoggerFor(this);
@@ -27,27 +31,12 @@ export class RootContainerInitializer extends Initializer {
   }
 
   public async handle(): Promise<void> {
-    if (!await this.hasRootContainer()) {
+    this.logger.debug(`Checking for root container at ${this.baseId.path}`);
+    if (!await containsResource(this.store, this.baseId)) {
       await this.createRootContainer();
-    }
-  }
-
-  /**
-   * Verify if a root container already exists in a ResourceStore.
-   */
-  protected async hasRootContainer(): Promise<boolean> {
-    try {
-      this.logger.debug(`Checking for root container at ${this.baseId.path}`);
-      const result = await this.store.getRepresentation(this.baseId, {});
+    } else {
       this.logger.debug(`Existing root container found at ${this.baseId.path}`);
-      result.data.destroy();
-      return true;
-    } catch (error: unknown) {
-      if (!(error instanceof NotFoundHttpError)) {
-        throw error;
-      }
     }
-    return false;
   }
 
   /**
@@ -59,6 +48,9 @@ export class RootContainerInitializer extends Initializer {
 
     // Make sure the root container is a pim:Storage
     // This prevents deletion of the root container as storage root containers can not be deleted
+    // Solid, §4.1: "Servers exposing the storage resource MUST advertise by including the HTTP Link header
+    // with rel="type" targeting http://www.w3.org/ns/pim/space#Storage when responding to storage’s request URI."
+    // https://solid.github.io/specification/protocol#storage
     metadata.add(RDF.type, PIM.terms.Storage);
 
     this.logger.debug(`Creating root container at ${this.baseId.path}`);
diff --git a/src/ldp/operations/PatchOperationHandler.ts b/src/ldp/operations/PatchOperationHandler.ts
index a4a1ee954..2b047483c 100644
--- a/src/ldp/operations/PatchOperationHandler.ts
+++ b/src/ldp/operations/PatchOperationHandler.ts
@@ -1,4 +1,6 @@
+import { getLoggerFor } from '../../logging/LogUtil';
 import type { ResourceStore } from '../../storage/ResourceStore';
+import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
 import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
 import type { Patch } from '../http/Patch';
 import { ResetResponseDescription } from '../http/response/ResetResponseDescription';
@@ -6,7 +8,13 @@ import type { ResponseDescription } from '../http/response/ResponseDescription';
 import type { Operation } from './Operation';
 import { OperationHandler } from './OperationHandler';
 
+/**
+ * Handles PATCH {@link Operation}s.
+ * Calls the modifyResource function from a {@link ResourceStore}.
+ */
 export class PatchOperationHandler extends OperationHandler {
+  protected readonly logger = getLoggerFor(this);
+
   private readonly store: ResourceStore;
 
   public constructor(store: ResourceStore) {
@@ -21,6 +29,13 @@ export class PatchOperationHandler extends OperationHandler {
   }
 
   public async handle(input: Operation): Promise<ResponseDescription> {
+    // Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
+    // without the Content-Type header with a status code of 400."
+    // https://solid.github.io/specification/protocol#http-server
+    if (!input.body?.metadata.contentType) {
+      this.logger.warn('No Content-Type header specified on PATCH request');
+      throw new BadRequestHttpError('No Content-Type header specified on PATCH request');
+    }
     await this.store.modifyResource(input.target, input.body as Patch);
     return new ResetResponseDescription();
   }
diff --git a/src/ldp/operations/PostOperationHandler.ts b/src/ldp/operations/PostOperationHandler.ts
index 9b9de16f3..1687849cd 100644
--- a/src/ldp/operations/PostOperationHandler.ts
+++ b/src/ldp/operations/PostOperationHandler.ts
@@ -28,9 +28,12 @@ export class PostOperationHandler extends OperationHandler {
   }
 
   public async handle(input: Operation): Promise<ResponseDescription> {
-    if (!input.body) {
-      this.logger.warn('POST operations require a body');
-      throw new BadRequestHttpError('POST operations require a body');
+    // Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
+    // without the Content-Type header with a status code of 400."
+    // https://solid.github.io/specification/protocol#http-server
+    if (!input.body?.metadata.contentType) {
+      this.logger.warn('No Content-Type header specified on POST request');
+      throw new BadRequestHttpError('No Content-Type header specified on POST request');
     }
     const identifier = await this.store.addResource(input.target, input.body);
     return new CreatedResponseDescription(identifier);
diff --git a/src/ldp/operations/PutOperationHandler.ts b/src/ldp/operations/PutOperationHandler.ts
index 7cdede505..94ec7a469 100644
--- a/src/ldp/operations/PutOperationHandler.ts
+++ b/src/ldp/operations/PutOperationHandler.ts
@@ -28,9 +28,12 @@ export class PutOperationHandler extends OperationHandler {
   }
 
   public async handle(input: Operation): Promise<ResponseDescription> {
-    if (typeof input.body !== 'object') {
-      this.logger.warn('No body specified on PUT request');
-      throw new BadRequestHttpError('PUT operations require a body');
+    // Solid, §2.1: "A Solid server MUST reject PUT, POST and PATCH requests
+    // without the Content-Type header with a status code of 400."
+    // https://solid.github.io/specification/protocol#http-server
+    if (!input.body?.metadata.contentType) {
+      this.logger.warn('No Content-Type header specified on PUT request');
+      throw new BadRequestHttpError('No Content-Type header specified on PUT request');
     }
     await this.store.setRepresentation(input.target, input.body);
     return new ResetResponseDescription();
diff --git a/src/pods/GeneratedPodManager.ts b/src/pods/GeneratedPodManager.ts
index 72e188762..616a8a805 100644
--- a/src/pods/GeneratedPodManager.ts
+++ b/src/pods/GeneratedPodManager.ts
@@ -1,8 +1,8 @@
 import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
 import { getLoggerFor } from '../logging/LogUtil';
 import type { ResourceStore } from '../storage/ResourceStore';
+import { containsResource } from '../storage/StoreUtil';
 import { ConflictHttpError } from '../util/errors/ConflictHttpError';
-import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import type { Agent } from './agent/Agent';
 import type { IdentifierGenerator } from './generate/IdentifierGenerator';
 import type { ResourcesGenerator } from './generate/ResourcesGenerator';
@@ -33,15 +33,8 @@ export class GeneratedPodManager implements PodManager {
   public async createPod(agent: Agent): Promise<ResourceIdentifier> {
     const podIdentifier = this.idGenerator.generate(agent.login);
     this.logger.info(`Creating pod ${podIdentifier.path}`);
-    try {
-      const result = await this.store.getRepresentation(podIdentifier, {});
-      result.data.destroy();
+    if (await containsResource(this.store, podIdentifier)) {
       throw new ConflictHttpError(`There already is a resource at ${podIdentifier.path}`);
-    } catch (error: unknown) {
-      // We want the identifier to not exist
-      if (!(error instanceof NotFoundHttpError)) {
-        throw error;
-      }
     }
 
     const resources = this.resourcesGenerator.generate(podIdentifier, agent);
diff --git a/src/server/middleware/CorsHandler.ts b/src/server/middleware/CorsHandler.ts
index 1c66f6c57..23365a695 100644
--- a/src/server/middleware/CorsHandler.ts
+++ b/src/server/middleware/CorsHandler.ts
@@ -22,6 +22,11 @@ interface SimpleCorsOptions {
 
 /**
  * Handler that sets CORS options on the response.
+ *
+ * Solid, §7.1: "A data pod MUST implement the CORS protocol [FETCH] such that, to the extent possible,
+ * the browser allows Solid apps to send any request and combination of request headers to the data pod,
+ * and the Solid app can read any response and response headers received from the data pod."
+ * Full details: https://solid.github.io/specification/protocol#cors-server
  */
 export class CorsHandler extends HttpHandler {
   private readonly corsHandler: RequestHandler;
diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts
index fafc07484..c2705a946 100644
--- a/src/storage/DataAccessorBasedStore.ts
+++ b/src/storage/DataAccessorBasedStore.ts
@@ -9,7 +9,7 @@ import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifie
 import { INTERNAL_QUADS } from '../util/ContentTypes';
 import { BadRequestHttpError } from '../util/errors/BadRequestHttpError';
 import { ConflictHttpError } from '../util/errors/ConflictHttpError';
-import { InternalServerError } from '../util/errors/InternalServerError';
+import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
 import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
 import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
@@ -65,7 +65,7 @@ export class DataAccessorBasedStore implements ResourceStore {
     const metadata = await this.accessor.getMetadata(identifier);
     let representation: Representation;
 
-    if (this.isExistingContainer(metadata)) {
+    if (isContainerPath(metadata.identifier.value)) {
       // Generate a container representation from the metadata
       const data = metadata.quads();
       metadata.addQuad(DC.terms.namespace, VANN.terms.preferredNamespacePrefix, 'dc');
@@ -86,23 +86,31 @@ export class DataAccessorBasedStore implements ResourceStore {
     // Ensure the representation is supported by the accessor
     await this.accessor.canHandle(representation);
 
-    // Using the parent metadata as we can also use that later to check if the nested containers maybe need to be made
     const parentMetadata = await this.getSafeNormalizedMetadata(container);
 
-    // When a POST method request targets a resource without an existing representation,
-    // the server MUST respond with the 404 status code.
+    // Solid, §5.3: "When a POST method request targets a resource without an existing representation,
+    // the server MUST respond with the 404 status code."
+    // https://solid.github.io/specification/protocol#writing-resources
     if (!parentMetadata) {
       throw new NotFoundHttpError();
     }
 
-    if (parentMetadata && !this.isExistingContainer(parentMetadata)) {
+    // Not using `container` since `getSafeNormalizedMetadata` might return metadata for a different identifier.
+    // Solid, §5: "Servers MUST respond with the 405 status code to requests using HTTP methods
+    // that are not supported by the target resource."
+    // https://solid.github.io/specification/protocol#reading-writing-resources
+    if (parentMetadata && !isContainerPath(parentMetadata.identifier.value)) {
       throw new MethodNotAllowedHttpError('The given path is not a container.');
     }
 
+    // Solid, §5.1: "Servers MAY allow clients to suggest the URI of a resource created through POST,
+    // using the HTTP Slug header as defined in [RFC5023].
+    // Clients who want the server to assign a URI of a resource, MUST use the POST request."
+    // https://solid.github.io/specification/protocol#resource-type-heuristics
     const newID = this.createSafeUri(container, representation.metadata, parentMetadata);
 
-    // Write the data. New containers will need to be created if there is no parent.
-    await this.writeData(newID, representation, isContainerIdentifier(newID), !parentMetadata);
+    // Write the data. New containers should never be made for a POST request.
+    await this.writeData(newID, representation, isContainerIdentifier(newID), false);
 
     return newID;
   }
@@ -116,16 +124,19 @@ export class DataAccessorBasedStore implements ResourceStore {
     // Check if the resource already exists
     const oldMetadata = await this.getSafeNormalizedMetadata(identifier);
 
-    // Might want to redirect in the future
+    // Might want to redirect in the future.
+    // See #480
+    // Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with
+    // one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to
+    // requests for the latter URI with a 301 redirect to the former."
+    // https://solid.github.io/specification/protocol#uri-slash-semantics
     if (oldMetadata && oldMetadata.identifier.value !== identifier.path) {
-      throw new ConflictHttpError(`${identifier.path} conflicts with existing path ${oldMetadata.identifier.value}`);
+      throw new ForbiddenHttpError(`${identifier.path} conflicts with existing path ${oldMetadata.identifier.value}`);
     }
 
-    // If we already have a resource for the given identifier, make sure they match resource types
     const isContainer = this.isNewContainer(representation.metadata, identifier.path);
-    if (oldMetadata && isContainer !== this.isExistingContainer(oldMetadata)) {
-      throw new ConflictHttpError('Input resource type does not match existing resource type.');
-    }
+    // Solid, §3.1: "Paths ending with a slash denote a container resource."
+    // https://solid.github.io/specification/protocol#uri-slash-semantics
     if (isContainer !== isContainerIdentifier(identifier)) {
       throw new BadRequestHttpError('Containers should have a `/` at the end of their path, resources should not.');
     }
@@ -141,12 +152,16 @@ export class DataAccessorBasedStore implements ResourceStore {
   public async deleteResource(identifier: ResourceIdentifier): Promise<void> {
     this.validateIdentifier(identifier);
     const metadata = await this.accessor.getMetadata(identifier);
-    // "When a DELETE request targets storage’s root container or its associated ACL resource,
-    //  the server MUST respond with the 405 status code."
-    // https://solid.github.io/specification/#deleting-resources
+    // Solid, §5.4: "When a DELETE request targets storage’s root container or its associated ACL resource,
+    // the server MUST respond with the 405 status code."
+    // https://solid.github.io/specification/protocol#deleting-resources
     if (this.isRootStorage(metadata)) {
       throw new MethodNotAllowedHttpError('Cannot delete a root storage container.');
     }
+    // Solid, §5.4: "When a DELETE request is made to a container, the server MUST delete the container
+    // if it contains no resources. If the container contains resources,
+    // the server MUST respond with the 409 status code and response body describing the error."
+    // https://solid.github.io/specification/protocol#deleting-resources
     if (metadata.getAll(LDP.contains).length > 0) {
       throw new ConflictHttpError('Can only delete empty containers.');
     }
@@ -164,9 +179,11 @@ export class DataAccessorBasedStore implements ResourceStore {
 
   /**
    * Returns the metadata matching the identifier, ignoring the presence of a trailing slash or not.
-   * This is used to support the following part of the spec:
-   * "If two URIs differ only in the trailing slash, and the server has associated a resource with one of them,
-   *  then the other URI MUST NOT correspond to another resource."
+   *
+   * Solid, §3.1: "If two URIs differ only in the trailing slash,
+   * and the server has associated a resource with one of them,
+   * then the other URI MUST NOT correspond to another resource."
+   * https://solid.github.io/specification/protocol#uri-slash-semantics
    *
    * First the identifier gets requested and if no result is found
    * the identifier with differing trailing slash is requested.
@@ -177,11 +194,13 @@ export class DataAccessorBasedStore implements ResourceStore {
     try {
       return await this.accessor.getMetadata(identifier);
     } catch (error: unknown) {
-      // Trimming the trailing slash of a root container is undefined as there is no parent container
-      if (error instanceof NotFoundHttpError && !this.identifierStrategy.isRootContainer(identifier)) {
-        return this.accessor.getMetadata(
-          { path: hasSlash ? trimTrailingSlashes(identifier.path) : ensureTrailingSlash(identifier.path) },
-        );
+      if (error instanceof NotFoundHttpError) {
+        const otherIdentifier =
+          { path: hasSlash ? trimTrailingSlashes(identifier.path) : ensureTrailingSlash(identifier.path) };
+
+        // Only try to access other identifier if it is valid in the scope of the DataAccessor
+        this.validateIdentifier(otherIdentifier);
+        return this.accessor.getMetadata(otherIdentifier);
       }
       throw error;
     }
@@ -222,6 +241,9 @@ export class DataAccessorBasedStore implements ResourceStore {
     }
 
     // Root container should not have a parent container
+    // Solid, §5.3: "Servers MUST create intermediate containers and include corresponding containment triples
+    // in container representations derived from the URI path component of PUT and PATCH requests."
+    // https://solid.github.io/specification/protocol#writing-resources
     if (createContainers && !this.identifierStrategy.isRootContainer(identifier)) {
       await this.createRecursiveContainers(this.identifierStrategy.getParentContainer(identifier));
     }
@@ -254,11 +276,11 @@ export class DataAccessorBasedStore implements ResourceStore {
       throw error;
     }
 
-    // Make sure there are no containment triples in the body
-    for (const quad of quads) {
-      if (quad.predicate.value === LDP.contains) {
-        throw new ConflictHttpError('Container bodies are not allowed to have containment triples.');
-      }
+    // Solid, §5.3: "Servers MUST NOT allow HTTP POST, PUT and PATCH to update a container’s containment triples;
+    // if the server receives such a request, it MUST respond with a 409 status code."
+    // https://solid.github.io/specification/protocol#writing-resources
+    if (quads.some((quad): boolean => quad.predicate.value === LDP.contains)) {
+      throw new ConflictHttpError('Container bodies are not allowed to have containment triples.');
     }
 
     // Input content type doesn't matter anymore
@@ -270,6 +292,13 @@ export class DataAccessorBasedStore implements ResourceStore {
 
   /**
    * Generates a new URI for a resource in the given container, potentially using the given slug.
+   *
+   * Solid, §5.3: "Servers MUST allow creating new resources with a POST request to URI path ending `/`.
+   * Servers MUST create a resource with URI path ending `/{id}` in container `/`.
+   * Servers MUST create a container with URI path ending `/{id}/` in container `/` for requests
+   * including the HTTP Link header with rel="type" targeting a valid LDP container type."
+   * https://solid.github.io/specification/protocol#writing-resources
+   *
    * @param container - Parent container of the new URI.
    * @param isContainer - Does the new URI represent a container?
    * @param slug - Slug to use for the new URI.
@@ -315,8 +344,6 @@ export class DataAccessorBasedStore implements ResourceStore {
    * @param suffix - Suffix of the URI. Can be the full URI, but only the last part is required.
    */
   protected isNewContainer(metadata: RepresentationMetadata, suffix?: string): boolean {
-    // Should not use `isExistingContainer` since the metadata might contain unrelated type triples
-    // It's not because there is no container type triple that the new resource is not a container
     if (this.hasContainerType(metadata.getAll(RDF.type))) {
       return true;
     }
@@ -324,19 +351,6 @@ export class DataAccessorBasedStore implements ResourceStore {
     return Boolean(slug && isContainerPath(slug));
   }
 
-  /**
-   * Checks if the given metadata represents a container, purely based on metadata type triples.
-   * Since type metadata always gets generated when writing resources this should never fail on stored resources.
-   * @param metadata - Metadata to check.
-   */
-  protected isExistingContainer(metadata: RepresentationMetadata): boolean {
-    const types = metadata.getAll(RDF.type);
-    if (!types.some((type): boolean => type.value === LDP.Resource)) {
-      throw new InternalServerError('Unknown resource type.');
-    }
-    return this.hasContainerType(types);
-  }
-
   /**
    * Checks in a list of types if any of them match a Container type.
    */
@@ -359,8 +373,13 @@ export class DataAccessorBasedStore implements ResourceStore {
   protected async createRecursiveContainers(container: ResourceIdentifier): Promise<void> {
     try {
       const metadata = await this.getNormalizedMetadata(container);
-      if (!this.isExistingContainer(metadata)) {
-        throw new ConflictHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
+      // See #480
+      // Solid, §3.1: "If two URIs differ only in the trailing slash, and the server has associated a resource with
+      // one of them, then the other URI MUST NOT correspond to another resource. Instead, the server MAY respond to
+      // requests for the latter URI with a 301 redirect to the former."
+      // https://solid.github.io/specification/protocol#uri-slash-semantics
+      if (!isContainerPath(metadata.identifier.value)) {
+        throw new ForbiddenHttpError(`Creating container ${container.path} conflicts with an existing resource.`);
       }
     } catch (error: unknown) {
       if (error instanceof NotFoundHttpError) {
diff --git a/src/storage/StoreUtil.ts b/src/storage/StoreUtil.ts
new file mode 100644
index 000000000..93440c24f
--- /dev/null
+++ b/src/storage/StoreUtil.ts
@@ -0,0 +1,16 @@
+import type { ResourceIdentifier } from '../ldp/representation/ResourceIdentifier';
+import type { ResourceStore } from '../storage/ResourceStore';
+import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
+
+export async function containsResource(store: ResourceStore, identifier: ResourceIdentifier): Promise<boolean> {
+  try {
+    const result = await store.getRepresentation(identifier, {});
+    result.data.destroy();
+    return true;
+  } catch (error: unknown) {
+    if (error instanceof NotFoundHttpError) {
+      return false;
+    }
+    throw error;
+  }
+}
diff --git a/src/storage/accessors/DataAccessor.ts b/src/storage/accessors/DataAccessor.ts
index ef793603c..616fb2482 100644
--- a/src/storage/accessors/DataAccessor.ts
+++ b/src/storage/accessors/DataAccessor.ts
@@ -57,6 +57,11 @@ export interface DataAccessor {
 
   /**
    * Deletes the resource and its corresponding metadata.
+   *
+   * Solid, §5.4: "When a contained resource is deleted, the server MUST also remove the corresponding containment
+   * triple, which has the effect of removing the deleted resource from the containing container."
+   * https://solid.github.io/specification/protocol#deleting-resources
+   *
    * @param identifier - Resource to delete.
    */
   deleteResource: (identifier: ResourceIdentifier) => Promise<void>;
diff --git a/src/storage/patch/SparqlUpdatePatchHandler.ts b/src/storage/patch/SparqlUpdatePatchHandler.ts
index beb563493..d9c8529a3 100644
--- a/src/storage/patch/SparqlUpdatePatchHandler.ts
+++ b/src/storage/patch/SparqlUpdatePatchHandler.ts
@@ -92,7 +92,8 @@ export class SparqlUpdatePatchHandler extends PatchHandler {
       });
       this.logger.debug(`${store.size} quads in ${identifier.path}.`);
     } catch (error: unknown) {
-      // In case the resource does not exist yet we want to create it
+      // Solid, §5.1: "Clients who want to assign a URI to a resource, MUST use PUT and PATCH requests."
+      // https://solid.github.io/specification/protocol#resource-type-heuristics
       if (!(error instanceof NotFoundHttpError)) {
         throw error;
       }
diff --git a/src/storage/routing/ConvertingRouterRule.ts b/src/storage/routing/ConvertingRouterRule.ts
index 6aff25975..1ee17c7c2 100644
--- a/src/storage/routing/ConvertingRouterRule.ts
+++ b/src/storage/routing/ConvertingRouterRule.ts
@@ -1,6 +1,6 @@
 import type { Representation } from '../../ldp/representation/Representation';
 import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
-import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
+import { containsResource } from '../../storage/StoreUtil';
 import type { ResourceStore } from '../ResourceStore';
 import type { PreferenceSupport } from './PreferenceSupport';
 import { RouterRule } from './RouterRule';
@@ -41,27 +41,11 @@ export class ConvertingRouterRule extends RouterRule {
     } else {
       // No content-type given so we can only check if one of the stores has data for the identifier
       store = await this.findStore(async(entry): Promise<boolean> =>
-        this.hasResource(entry.store, input.identifier));
+        containsResource(entry.store, input.identifier));
     }
     return store;
   }
 
-  /**
-   * Helper function that checks if the given store contains the given identifier or not.
-   */
-  private async hasResource(store: ResourceStore, identifier: ResourceIdentifier): Promise<boolean> {
-    try {
-      const response = await store.getRepresentation(identifier, {});
-      response.data.destroy();
-      return true;
-    } catch (error: unknown) {
-      if (error instanceof NotFoundHttpError) {
-        return false;
-      }
-      throw error;
-    }
-  }
-
   /**
    * Helper function that runs the given callback function for all the stores
    * and returns the first one that does not throw an error.
diff --git a/src/util/PathUtil.ts b/src/util/PathUtil.ts
index 41f182e19..4329505db 100644
--- a/src/util/PathUtil.ts
+++ b/src/util/PathUtil.ts
@@ -88,6 +88,8 @@ export function encodeUriPathComponents(path: string): string {
  * @param path - Path to check.
  */
 export function isContainerPath(path: string): boolean {
+  // Solid, §3.1: "Paths ending with a slash denote a container resource."
+  // https://solid.github.io/specification/protocol#uri-slash-semantics
   return path.endsWith('/');
 }
 


Test patch:
diff --git a/test/unit/authorization/WebAclAuthorizer.test.ts b/test/unit/authorization/WebAclAuthorizer.test.ts
index 79ee7460e..cbc6728ce 100644
--- a/test/unit/authorization/WebAclAuthorizer.test.ts
+++ b/test/unit/authorization/WebAclAuthorizer.test.ts
@@ -8,7 +8,6 @@ import type { Representation } from '../../../src/ldp/representation/Representat
 import type { ResourceIdentifier } from '../../../src/ldp/representation/ResourceIdentifier';
 import type { ResourceStore } from '../../../src/storage/ResourceStore';
 import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
-import { InternalServerError } from '../../../src/util/errors/InternalServerError';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { UnauthorizedHttpError } from '../../../src/util/errors/UnauthorizedHttpError';
 import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
@@ -153,7 +152,7 @@ describe('A WebAclAuthorizer', (): void => {
     };
     const promise = authorizer.handle({ identifier, permissions, credentials });
     await expect(promise).rejects.toThrow('No ACL document found for root container');
-    await expect(promise).rejects.toThrow(InternalServerError);
+    await expect(promise).rejects.toThrow(ForbiddenHttpError);
   });
 
   it('allows an agent to append if they have write access.', async(): Promise<void> => {
diff --git a/test/unit/ldp/operations/PatchOperationHandler.test.ts b/test/unit/ldp/operations/PatchOperationHandler.test.ts
index b1362845d..6767d4218 100644
--- a/test/unit/ldp/operations/PatchOperationHandler.test.ts
+++ b/test/unit/ldp/operations/PatchOperationHandler.test.ts
@@ -1,6 +1,8 @@
 import type { Operation } from '../../../../src/ldp/operations/Operation';
 import { PatchOperationHandler } from '../../../../src/ldp/operations/PatchOperationHandler';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import type { ResourceStore } from '../../../../src/storage/ResourceStore';
+import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
 
 describe('A PatchOperationHandler', (): void => {
@@ -15,10 +17,17 @@ describe('A PatchOperationHandler', (): void => {
     await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(NotImplementedHttpError);
   });
 
+  it('errors if there is no body or content-type.', async(): Promise<void> => {
+    await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
+    await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
+      .rejects.toThrow(BadRequestHttpError);
+  });
+
   it('deletes the resource from the store and returns the correct response.', async(): Promise<void> => {
-    const result = await handler.handle({ target: { path: 'url' }, body: { binary: false }} as Operation);
+    const metadata = new RepresentationMetadata('text/turtle');
+    const result = await handler.handle({ target: { path: 'url' }, body: { metadata }} as Operation);
     expect(store.modifyResource).toHaveBeenCalledTimes(1);
-    expect(store.modifyResource).toHaveBeenLastCalledWith({ path: 'url' }, { binary: false });
+    expect(store.modifyResource).toHaveBeenLastCalledWith({ path: 'url' }, { metadata });
     expect(result.statusCode).toBe(205);
     expect(result.metadata).toBeUndefined();
     expect(result.data).toBeUndefined();
diff --git a/test/unit/ldp/operations/PostOperationHandler.test.ts b/test/unit/ldp/operations/PostOperationHandler.test.ts
index 380b51d76..89d0e84ea 100644
--- a/test/unit/ldp/operations/PostOperationHandler.test.ts
+++ b/test/unit/ldp/operations/PostOperationHandler.test.ts
@@ -20,12 +20,15 @@ describe('A PostOperationHandler', (): void => {
       .rejects.toThrow(NotImplementedHttpError);
   });
 
-  it('errors if there is no body.', async(): Promise<void> => {
-    await expect(handler.handle({ method: 'POST' } as Operation)).rejects.toThrow(BadRequestHttpError);
+  it('errors if there is no body or content-type.', async(): Promise<void> => {
+    await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
+    await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
+      .rejects.toThrow(BadRequestHttpError);
   });
 
   it('adds the given representation to the store and returns the correct response.', async(): Promise<void> => {
-    const result = await handler.handle({ method: 'POST', body: { }} as Operation);
+    const metadata = new RepresentationMetadata('text/turtle');
+    const result = await handler.handle({ method: 'POST', body: { metadata }} as Operation);
     expect(result.statusCode).toBe(201);
     expect(result.metadata).toBeInstanceOf(RepresentationMetadata);
     expect(result.metadata?.get(HTTP.location)?.value).toBe('newPath');
diff --git a/test/unit/ldp/operations/PutOperationHandler.test.ts b/test/unit/ldp/operations/PutOperationHandler.test.ts
index b23af1960..f667ce9f3 100644
--- a/test/unit/ldp/operations/PutOperationHandler.test.ts
+++ b/test/unit/ldp/operations/PutOperationHandler.test.ts
@@ -1,5 +1,6 @@
 import type { Operation } from '../../../../src/ldp/operations/Operation';
 import { PutOperationHandler } from '../../../../src/ldp/operations/PutOperationHandler';
+import { RepresentationMetadata } from '../../../../src/ldp/representation/RepresentationMetadata';
 import type { ResourceStore } from '../../../../src/storage/ResourceStore';
 import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
 import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
@@ -17,16 +18,19 @@ describe('A PutOperationHandler', (): void => {
     await expect(handler.canHandle({ method: 'PUT' } as Operation)).resolves.toBeUndefined();
   });
 
+  it('errors if there is no body or content-type.', async(): Promise<void> => {
+    await expect(handler.handle({ } as Operation)).rejects.toThrow(BadRequestHttpError);
+    await expect(handler.handle({ body: { metadata: new RepresentationMetadata() }} as Operation))
+      .rejects.toThrow(BadRequestHttpError);
+  });
+
   it('sets the representation in the store and returns the correct response.', async(): Promise<void> => {
-    const result = await handler.handle({ target: { path: 'url' }, body: {}} as Operation);
+    const metadata = new RepresentationMetadata('text/turtle');
+    const result = await handler.handle({ target: { path: 'url' }, body: { metadata }} as Operation);
     expect(store.setRepresentation).toHaveBeenCalledTimes(1);
-    expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, {});
+    expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { metadata });
     expect(result.statusCode).toBe(205);
     expect(result.metadata).toBeUndefined();
     expect(result.data).toBeUndefined();
   });
-
-  it('errors when there is no body.', async(): Promise<void> => {
-    await expect(handler.handle({ method: 'PUT' } as Operation)).rejects.toThrow(BadRequestHttpError);
-  });
 });
diff --git a/test/unit/storage/DataAccessorBasedStore.test.ts b/test/unit/storage/DataAccessorBasedStore.test.ts
index ba463be00..5e55d81fb 100644
--- a/test/unit/storage/DataAccessorBasedStore.test.ts
+++ b/test/unit/storage/DataAccessorBasedStore.test.ts
@@ -11,7 +11,7 @@ import { DataAccessorBasedStore } from '../../../src/storage/DataAccessorBasedSt
 import { INTERNAL_QUADS } from '../../../src/util/ContentTypes';
 import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
 import { ConflictHttpError } from '../../../src/util/errors/ConflictHttpError';
-import { InternalServerError } from '../../../src/util/errors/InternalServerError';
+import { ForbiddenHttpError } from '../../../src/util/errors/ForbiddenHttpError';
 import { MethodNotAllowedHttpError } from '../../../src/util/errors/MethodNotAllowedHttpError';
 import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
 import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
@@ -90,7 +90,9 @@ describe('A DataAccessorBasedStore', (): void => {
         DataFactory.namedNode(LDP.BasicContainer),
       ]},
     );
-    accessor.data[root] = { metadata: containerMetadata } as Representation;
+    const rootMetadata = new RepresentationMetadata(containerMetadata);
+    rootMetadata.identifier = namedNode(root);
+    accessor.data[root] = { metadata: rootMetadata } as Representation;
 
     representation = {
       binary: true,
@@ -117,6 +119,7 @@ describe('A DataAccessorBasedStore', (): void => {
 
     it('will return a data stream that matches the metadata for containers.', async(): Promise<void> => {
       const resourceID = { path: `${root}container/` };
+      containerMetadata.identifier = namedNode(resourceID.path);
       accessor.data[resourceID.path] = { metadata: containerMetadata } as Representation;
       const metaQuads = containerMetadata.quads();
       const result = await store.getRepresentation(resourceID);
@@ -243,8 +246,9 @@ describe('A DataAccessorBasedStore', (): void => {
       const resourceID = { path: `${root}resource` };
       accessor.data[`${resourceID.path}/`] = representation;
       representation.metadata.identifier = DataFactory.namedNode(`${resourceID.path}/`);
-      await expect(store.setRepresentation(resourceID, representation))
-        .rejects.toThrow(`${resourceID.path} conflicts with existing path ${resourceID.path}/`);
+      const prom = store.setRepresentation(resourceID, representation);
+      await expect(prom).rejects.toThrow(`${resourceID.path} conflicts with existing path ${resourceID.path}/`);
+      await expect(prom).rejects.toThrow(ForbiddenHttpError);
     });
 
     // As discussed in #475, trimming the trailing slash of a root container in getNormalizedMetadata
@@ -268,17 +272,6 @@ describe('A DataAccessorBasedStore', (): void => {
       mock.mockRestore();
     });
 
-    it('will error if the target has a different resource type.', async(): Promise<void> => {
-      const resourceID = { path: `${root}resource` };
-      accessor.data[resourceID.path] = representation;
-      representation.metadata.identifier = DataFactory.namedNode(resourceID.path);
-      const newRepresentation = { ...representation };
-      newRepresentation.metadata = new RepresentationMetadata(representation.metadata);
-      newRepresentation.metadata.add(RDF.type, LDP.terms.Container);
-      await expect(store.setRepresentation(resourceID, newRepresentation))
-        .rejects.toThrow(new ConflictHttpError('Input resource type does not match existing resource type.'));
-    });
-
     it('will error if the ending slash does not match its resource type.', async(): Promise<void> => {
       const resourceID = { path: `${root}resource` };
       representation.metadata.add(RDF.type, LDP.terms.Container);
@@ -287,16 +280,6 @@ describe('A DataAccessorBasedStore', (): void => {
       );
     });
 
-    it('will error if the DataAccessor did not store the required type triples.', async(): Promise<void> => {
-      const resourceID = { path: `${root}resource` };
-      accessor.data[resourceID.path] = representation;
-      representation.metadata.identifier = namedNode(resourceID.path);
-      representation.metadata.removeAll(RDF.type);
-      await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
-        new InternalServerError('Unknown resource type.'),
-      );
-    });
-
     it('errors when trying to create a container with non-RDF data.', async(): Promise<void> => {
       const resourceID = { path: `${root}container/` };
       representation.metadata.add(RDF.type, LDP.terms.Container);
@@ -360,9 +343,9 @@ describe('A DataAccessorBasedStore', (): void => {
     it('errors when a recursive container overlaps with an existing resource.', async(): Promise<void> => {
       const resourceID = { path: `${root}a/b/resource` };
       accessor.data[`${root}a`] = representation;
-      await expect(store.setRepresentation(resourceID, representation)).rejects.toThrow(
-        new ConflictHttpError(`Creating container ${root}a/ conflicts with an existing resource.`),
-      );
+      const prom = store.setRepresentation(resourceID, representation);
+      await expect(prom).rejects.toThrow(`Creating container ${root}a/ conflicts with an existing resource.`);
+      await expect(prom).rejects.toThrow(ForbiddenHttpError);
     });
 
     it('can write to root if it does not exist.', async(): Promise<void> => {