From effc20a270baa2b9305eb11a7858fa2353ab4434 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Fri, 25 Mar 2022 11:34:19 +0100 Subject: [PATCH] feat: Store methods in MethodNotAllowedHttpError --- .../interaction/BaseInteractionHandler.ts | 4 +-- src/identity/interaction/HtmlViewHandler.ts | 2 +- src/init/setup/SetupHttpHandler.ts | 2 +- src/server/util/RouterHandler.ts | 2 +- src/storage/DataAccessorBasedStore.ts | 7 +++-- src/util/Vocabularies.ts | 1 + src/util/errors/MethodNotAllowedHttpError.ts | 30 +++++++++++++++---- test/unit/util/errors/HttpError.test.ts | 29 +++++++++++++++++- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/identity/interaction/BaseInteractionHandler.ts b/src/identity/interaction/BaseInteractionHandler.ts index e81afe7a2..1f192c10a 100644 --- a/src/identity/interaction/BaseInteractionHandler.ts +++ b/src/identity/interaction/BaseInteractionHandler.ts @@ -22,7 +22,7 @@ export abstract class BaseInteractionHandler extends InteractionHandler { await super.canHandle(input); const { method } = input.operation; if (method !== 'GET' && method !== 'POST') { - throw new MethodNotAllowedHttpError('Only GET/POST requests are supported.'); + throw new MethodNotAllowedHttpError([ method ], 'Only GET/POST requests are supported.'); } } @@ -30,7 +30,7 @@ export abstract class BaseInteractionHandler extends InteractionHandler { switch (input.operation.method) { case 'GET': return this.handleGet(input); case 'POST': return this.handlePost(input); - default: throw new MethodNotAllowedHttpError(); + default: throw new MethodNotAllowedHttpError([ input.operation.method ]); } } diff --git a/src/identity/interaction/HtmlViewHandler.ts b/src/identity/interaction/HtmlViewHandler.ts index 7861f9fcf..9a42d7235 100644 --- a/src/identity/interaction/HtmlViewHandler.ts +++ b/src/identity/interaction/HtmlViewHandler.ts @@ -39,7 +39,7 @@ export class HtmlViewHandler extends InteractionHandler { public async canHandle({ operation }: InteractionHandlerInput): Promise { if (operation.method !== 'GET') { - throw new MethodNotAllowedHttpError(); + throw new MethodNotAllowedHttpError([ operation.method ]); } if (!this.templates[operation.target.path]) { throw new NotFoundHttpError(); diff --git a/src/init/setup/SetupHttpHandler.ts b/src/init/setup/SetupHttpHandler.ts index ebc7730d9..0544628ff 100644 --- a/src/init/setup/SetupHttpHandler.ts +++ b/src/init/setup/SetupHttpHandler.ts @@ -68,7 +68,7 @@ export class SetupHttpHandler extends OperationHttpHandler { switch (operation.method) { case 'GET': return this.handleGet(operation); case 'POST': return this.handlePost(operation); - default: throw new MethodNotAllowedHttpError(); + default: throw new MethodNotAllowedHttpError([ operation.method ]); } } diff --git a/src/server/util/RouterHandler.ts b/src/server/util/RouterHandler.ts index b48393c58..aefc8e697 100644 --- a/src/server/util/RouterHandler.ts +++ b/src/server/util/RouterHandler.ts @@ -47,7 +47,7 @@ export class RouterHandler extends HttpHandler { throw new BadRequestHttpError('Cannot handle request without a method'); } if (!this.allMethods && !this.allowedMethods.includes(request.method)) { - throw new MethodNotAllowedHttpError(`${request.method} is not allowed.`); + throw new MethodNotAllowedHttpError([ request.method ], `${request.method} is not allowed.`); } const pathName = await getRelativeUrl(this.baseUrl, request, this.targetExtractor); if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) { diff --git a/src/storage/DataAccessorBasedStore.ts b/src/storage/DataAccessorBasedStore.ts index 451fde55f..ca3f5511c 100644 --- a/src/storage/DataAccessorBasedStore.ts +++ b/src/storage/DataAccessorBasedStore.ts @@ -155,7 +155,7 @@ export class DataAccessorBasedStore implements ResourceStore { // that are not supported by the target resource." // https://solid.github.io/specification/protocol#reading-writing-resources if (!isContainerPath(parentMetadata.identifier.value)) { - throw new MethodNotAllowedHttpError('The given path is not a container.'); + throw new MethodNotAllowedHttpError([ 'POST' ], 'The given path is not a container.'); } this.validateConditions(conditions, parentMetadata); @@ -240,14 +240,15 @@ export class DataAccessorBasedStore implements ResourceStore { // 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.'); + throw new MethodNotAllowedHttpError([ 'DELETE' ], 'Cannot delete a root storage container.'); } if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) && this.auxiliaryStrategy.isRequiredInRoot(identifier)) { const subjectIdentifier = this.auxiliaryStrategy.getSubjectIdentifier(identifier); const parentMetadata = await this.accessor.getMetadata(subjectIdentifier); if (this.isRootStorage(parentMetadata)) { - throw new MethodNotAllowedHttpError(`Cannot delete ${identifier.path} from a root storage container.`); + throw new MethodNotAllowedHttpError([ 'DELETE' ], + `Cannot delete ${identifier.path} from a root storage container.`); } } diff --git a/src/util/Vocabularies.ts b/src/util/Vocabularies.ts index e46a35cfc..c7c6d1f43 100644 --- a/src/util/Vocabularies.ts +++ b/src/util/Vocabularies.ts @@ -141,6 +141,7 @@ export const SOLID = createUriAndTermNamespace('http://www.w3.org/ns/solid/terms ); export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:', + 'disallowedMethod', 'errorResponse', 'stack', ); diff --git a/src/util/errors/MethodNotAllowedHttpError.ts b/src/util/errors/MethodNotAllowedHttpError.ts index 5df8cb692..48e668f53 100644 --- a/src/util/errors/MethodNotAllowedHttpError.ts +++ b/src/util/errors/MethodNotAllowedHttpError.ts @@ -1,14 +1,32 @@ +import { DataFactory } from 'n3'; +import type { Quad, Quad_Subject } from 'rdf-js'; +import { toNamedTerm, toObjectTerm } from '../TermUtil'; +import { SOLID_ERROR } from '../Vocabularies'; import type { HttpErrorOptions } from './HttpError'; -import { HttpError } from './HttpError'; +import { generateHttpErrorClass } from './HttpError'; +import quad = DataFactory.quad; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const BaseHttpError = generateHttpErrorClass(405, 'MethodNotAllowedHttpError'); + /** * An error thrown when data was found for the requested identifier, but is not supported by the target resource. + * Can keep track of the methods that are not allowed. */ -export class MethodNotAllowedHttpError extends HttpError { - public constructor(message?: string, options?: HttpErrorOptions) { - super(405, 'MethodNotAllowedHttpError', message, options); +export class MethodNotAllowedHttpError extends BaseHttpError { + public readonly methods: Readonly; + + public constructor(methods: string[] = [], message?: string, options?: HttpErrorOptions) { + super(message ?? `${methods} are not allowed.`, options); + this.methods = methods; } - public static isInstance(error: any): error is MethodNotAllowedHttpError { - return HttpError.isInstance(error) && error.statusCode === 405; + public generateMetadata(subject: Quad_Subject | string): Quad[] { + const term = toNamedTerm(subject); + const quads = super.generateMetadata(term); + for (const method of this.methods) { + quads.push(quad(term, SOLID_ERROR.terms.disallowedMethod, toObjectTerm(method, true))); + } + return quads; } } diff --git a/test/unit/util/errors/HttpError.test.ts b/test/unit/util/errors/HttpError.test.ts index 8161f9457..46d9a8773 100644 --- a/test/unit/util/errors/HttpError.test.ts +++ b/test/unit/util/errors/HttpError.test.ts @@ -23,7 +23,6 @@ describe('HttpError', (): void => { [ 'UnauthorizedHttpError', 401, UnauthorizedHttpError ], [ 'ForbiddenHttpError', 403, ForbiddenHttpError ], [ 'NotFoundHttpError', 404, NotFoundHttpError ], - [ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ], [ 'ConflictHttpError', 409, ConflictHttpError ], [ 'PreconditionFailedHttpError', 412, PreconditionFailedHttpError ], [ 'PayloadHttpError', 413, PayloadHttpError ], @@ -84,4 +83,32 @@ describe('HttpError', (): void => { ]); }); }); + + // Separate test due to different constructor + describe('MethodNotAllowedHttpError', (): void => { + const options = { + cause: new Error('cause'), + errorCode: 'E1234', + details: { some: 'detail' }, + }; + const instance = new MethodNotAllowedHttpError([ 'GET' ], 'my message', options); + + it('is valid.', async(): Promise => { + expect(new MethodNotAllowedHttpError().methods).toHaveLength(0); + expect(MethodNotAllowedHttpError.isInstance(instance)).toBe(true); + expect(MethodNotAllowedHttpError.uri).toEqualRdfTerm(generateHttpErrorUri(405)); + expect(instance.name).toBe('MethodNotAllowedHttpError'); + expect(instance.statusCode).toBe(405); + expect(instance.message).toBe('my message'); + expect(instance.cause).toBe(options.cause); + expect(instance.errorCode).toBe(options.errorCode); + expect(new MethodNotAllowedHttpError([ 'GET' ]).errorCode).toBe(`H${405}`); + + const subject = namedNode('subject'); + expect(instance.generateMetadata(subject)).toBeRdfIsomorphic([ + quad(subject, SOLID_ERROR.terms.errorResponse, MethodNotAllowedHttpError.uri), + quad(subject, SOLID_ERROR.terms.disallowedMethod, literal('GET')), + ]); + }); + }); });