Repository: solid/community-server
Pull request #118 (2020-09-02 15:34:25)

Support all PUT operations, error later
When executing a bodyless `PUT` request, I would get:

```
UnsupportedHttpError: No handler supports the given input: [This handler only supports DELETE operations., This handler only supports GET operations., This handler only supports PATCH operations., This handler only supports POST operations., PUT operations require a body.].
    at CompositeAsyncHandler.findHandler (/Users/ruben/Documents/UGent/Solid/solid-community-server/src/util/CompositeAsyncHandler.js:77:15)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
```

Clearly, the `PUT` handler is the (only) right one. The fact that it errors is because the request is invalid, and the `PUT` handler is the only one who knows that.

So the right error is “no body specified”, all the rest is noise. This PR brings it to:

```
UnsupportedHttpError: PUT operations require a body.
```

Apart from fixing this case, I also want to draw attention to the generic mechanism. `canHandle` answers the question: “is this the right actor to be handling this?’, not: “will handling succeed?”. In this case, the `PUT` actor _can_ handle; it's just that the correct handling is to error. This means two things in particular:
- `canHandle` methods should only check for applicability, not avoid any other errors via preconditions
- calls to `handleSafe` are not guaranteed to not error; only to have that error thrown by the appropriate actor

I have created https://github.com/solid/community-server/issues/119 to ensure this is applied consistently.


Patch:
diff --git a/src/ldp/operations/PutOperationHandler.ts b/src/ldp/operations/PutOperationHandler.ts
index 841939234..7aceacc1e 100644
--- a/src/ldp/operations/PutOperationHandler.ts
+++ b/src/ldp/operations/PutOperationHandler.ts
@@ -20,13 +20,13 @@ export class PutOperationHandler extends OperationHandler {
     if (input.method !== 'PUT') {
       throw new UnsupportedHttpError('This handler only supports PUT operations.');
     }
-    if (typeof input.body !== 'object') {
-      throw new UnsupportedHttpError('PUT operations require a body.');
-    }
   }
 
   public async handle(input: Operation): Promise<ResponseDescription> {
-    await this.store.setRepresentation(input.target, input.body!);
+    if (typeof input.body !== 'object') {
+      throw new UnsupportedHttpError('PUT operations require a body.');
+    }
+    await this.store.setRepresentation(input.target, input.body);
     return { identifier: input.target };
   }
 }


Test patch:
diff --git a/test/unit/ldp/operations/PutOperationHandler.test.ts b/test/unit/ldp/operations/PutOperationHandler.test.ts
index 68ff8ef66..6f9abea01 100644
--- a/test/unit/ldp/operations/PutOperationHandler.test.ts
+++ b/test/unit/ldp/operations/PutOperationHandler.test.ts
@@ -11,10 +11,9 @@ describe('A PutOperationHandler', (): void => {
     store.setRepresentation = jest.fn(async(): Promise<void> => {});
   });
 
-  it('only supports PUT operations with a body.', async(): Promise<void> => {
-    await expect(handler.canHandle({ method: 'PUT' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+  it('only supports PUT operations.', async(): Promise<void> => {
     await expect(handler.canHandle({ method: 'GET' } as Operation)).rejects.toThrow(UnsupportedHttpError);
-    await expect(handler.canHandle({ method: 'PUT', body: { dataType: 'test' }} as Operation)).resolves.toBeUndefined();
+    await expect(handler.canHandle({ method: 'PUT' } as Operation)).resolves.toBeUndefined();
   });
 
   it('sets the representation in the store and returns its identifier.', async(): Promise<void> => {
@@ -23,4 +22,8 @@ describe('A PutOperationHandler', (): void => {
     expect(store.setRepresentation).toHaveBeenCalledTimes(1);
     expect(store.setRepresentation).toHaveBeenLastCalledWith({ path: 'url' }, { dataType: 'test' });
   });
+
+  it('errors when there is no body.', async(): Promise<void> => {
+    await expect(handler.handle({ method: 'PUT' } as Operation)).rejects.toThrow(UnsupportedHttpError);
+  });
 });