feat: Store methods in MethodNotAllowedHttpError

This commit is contained in:
Joachim Van Herwegen 2022-03-25 11:34:19 +01:00
parent f3dedf4e27
commit effc20a270
8 changed files with 62 additions and 15 deletions

View File

@ -22,7 +22,7 @@ export abstract class BaseInteractionHandler extends InteractionHandler {
await super.canHandle(input); await super.canHandle(input);
const { method } = input.operation; const { method } = input.operation;
if (method !== 'GET' && method !== 'POST') { 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) { switch (input.operation.method) {
case 'GET': return this.handleGet(input); case 'GET': return this.handleGet(input);
case 'POST': return this.handlePost(input); case 'POST': return this.handlePost(input);
default: throw new MethodNotAllowedHttpError(); default: throw new MethodNotAllowedHttpError([ input.operation.method ]);
} }
} }

View File

@ -39,7 +39,7 @@ export class HtmlViewHandler extends InteractionHandler {
public async canHandle({ operation }: InteractionHandlerInput): Promise<void> { public async canHandle({ operation }: InteractionHandlerInput): Promise<void> {
if (operation.method !== 'GET') { if (operation.method !== 'GET') {
throw new MethodNotAllowedHttpError(); throw new MethodNotAllowedHttpError([ operation.method ]);
} }
if (!this.templates[operation.target.path]) { if (!this.templates[operation.target.path]) {
throw new NotFoundHttpError(); throw new NotFoundHttpError();

View File

@ -68,7 +68,7 @@ export class SetupHttpHandler extends OperationHttpHandler {
switch (operation.method) { switch (operation.method) {
case 'GET': return this.handleGet(operation); case 'GET': return this.handleGet(operation);
case 'POST': return this.handlePost(operation); case 'POST': return this.handlePost(operation);
default: throw new MethodNotAllowedHttpError(); default: throw new MethodNotAllowedHttpError([ operation.method ]);
} }
} }

View File

@ -47,7 +47,7 @@ export class RouterHandler extends HttpHandler {
throw new BadRequestHttpError('Cannot handle request without a method'); throw new BadRequestHttpError('Cannot handle request without a method');
} }
if (!this.allMethods && !this.allowedMethods.includes(request.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); const pathName = await getRelativeUrl(this.baseUrl, request, this.targetExtractor);
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) { if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {

View File

@ -155,7 +155,7 @@ export class DataAccessorBasedStore implements ResourceStore {
// that are not supported by the target resource." // that are not supported by the target resource."
// https://solid.github.io/specification/protocol#reading-writing-resources // https://solid.github.io/specification/protocol#reading-writing-resources
if (!isContainerPath(parentMetadata.identifier.value)) { 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); this.validateConditions(conditions, parentMetadata);
@ -240,14 +240,15 @@ export class DataAccessorBasedStore implements ResourceStore {
// the server MUST respond with the 405 status code." // the server MUST respond with the 405 status code."
// https://solid.github.io/specification/protocol#deleting-resources // https://solid.github.io/specification/protocol#deleting-resources
if (this.isRootStorage(metadata)) { 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) && if (this.auxiliaryStrategy.isAuxiliaryIdentifier(identifier) &&
this.auxiliaryStrategy.isRequiredInRoot(identifier)) { this.auxiliaryStrategy.isRequiredInRoot(identifier)) {
const subjectIdentifier = this.auxiliaryStrategy.getSubjectIdentifier(identifier); const subjectIdentifier = this.auxiliaryStrategy.getSubjectIdentifier(identifier);
const parentMetadata = await this.accessor.getMetadata(subjectIdentifier); const parentMetadata = await this.accessor.getMetadata(subjectIdentifier);
if (this.isRootStorage(parentMetadata)) { 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.`);
} }
} }

View File

@ -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:', export const SOLID_ERROR = createUriAndTermNamespace('urn:npm:solid:community-server:error:',
'disallowedMethod',
'errorResponse', 'errorResponse',
'stack', 'stack',
); );

View File

@ -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 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. * 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 { export class MethodNotAllowedHttpError extends BaseHttpError {
public constructor(message?: string, options?: HttpErrorOptions) { public readonly methods: Readonly<string[]>;
super(405, 'MethodNotAllowedHttpError', message, options);
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 { public generateMetadata(subject: Quad_Subject | string): Quad[] {
return HttpError.isInstance(error) && error.statusCode === 405; 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;
} }
} }

View File

@ -23,7 +23,6 @@ describe('HttpError', (): void => {
[ 'UnauthorizedHttpError', 401, UnauthorizedHttpError ], [ 'UnauthorizedHttpError', 401, UnauthorizedHttpError ],
[ 'ForbiddenHttpError', 403, ForbiddenHttpError ], [ 'ForbiddenHttpError', 403, ForbiddenHttpError ],
[ 'NotFoundHttpError', 404, NotFoundHttpError ], [ 'NotFoundHttpError', 404, NotFoundHttpError ],
[ 'MethodNotAllowedHttpError', 405, MethodNotAllowedHttpError ],
[ 'ConflictHttpError', 409, ConflictHttpError ], [ 'ConflictHttpError', 409, ConflictHttpError ],
[ 'PreconditionFailedHttpError', 412, PreconditionFailedHttpError ], [ 'PreconditionFailedHttpError', 412, PreconditionFailedHttpError ],
[ 'PayloadHttpError', 413, PayloadHttpError ], [ '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<void> => {
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')),
]);
});
});
}); });