mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Add metadata to errors
This commit is contained in:
parent
a333412e19
commit
f373dff1d7
@ -1,6 +1,7 @@
|
|||||||
import type { TLSSocket } from 'tls';
|
import type { TLSSocket } from 'tls';
|
||||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||||
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../util/errors/BadRequestHttpError';
|
||||||
|
import { errorTermsToMetadata } from '../../../util/errors/HttpErrorUtil';
|
||||||
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../../util/errors/InternalServerError';
|
||||||
import { parseForwarded } from '../../../util/HeaderUtil';
|
import { parseForwarded } from '../../../util/HeaderUtil';
|
||||||
import type { IdentifierStrategy } from '../../../util/identifiers/IdentifierStrategy';
|
import type { IdentifierStrategy } from '../../../util/identifiers/IdentifierStrategy';
|
||||||
@ -73,7 +74,7 @@ export class OriginalUrlExtractor extends TargetExtractor {
|
|||||||
// Check if the configured IdentifierStrategy supports the identifier
|
// Check if the configured IdentifierStrategy supports the identifier
|
||||||
if (!this.identifierStrategy.supportsIdentifier(identifier)) {
|
if (!this.identifierStrategy.supportsIdentifier(identifier)) {
|
||||||
throw new InternalServerError(`The identifier ${identifier.path} is outside the configured identifier space.`,
|
throw new InternalServerError(`The identifier ${identifier.path} is outside the configured identifier space.`,
|
||||||
{ errorCode: 'E0001', details: { path: identifier.path }});
|
{ errorCode: 'E0001', metadata: errorTermsToMetadata({ path: identifier.path }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
return identifier;
|
return identifier;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||||
|
import { parseContentType } from '../../../util/HeaderUtil';
|
||||||
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
import type { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
||||||
import { MetadataParser } from './MetadataParser';
|
import { MetadataParser } from './MetadataParser';
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export class ContentTypeParser extends MetadataParser {
|
|||||||
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
|
public async handle(input: { request: HttpRequest; metadata: RepresentationMetadata }): Promise<void> {
|
||||||
const contentType = input.request.headers['content-type'];
|
const contentType = input.request.headers['content-type'];
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
input.metadata.contentType = contentType;
|
input.metadata.contentTypeObject = parseContentType(contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||||
import type { AcceptHeader } from '../../../util/HeaderUtil';
|
import type { AcceptHeader } from '../../../util/Header';
|
||||||
import {
|
import {
|
||||||
parseAccept,
|
parseAccept,
|
||||||
parseAcceptCharset,
|
parseAcceptCharset,
|
||||||
|
@ -3,13 +3,9 @@ import type {
|
|||||||
RepresentationConverterArgs,
|
RepresentationConverterArgs,
|
||||||
} from '../../../storage/conversion/RepresentationConverter';
|
} from '../../../storage/conversion/RepresentationConverter';
|
||||||
import { INTERNAL_ERROR } from '../../../util/ContentTypes';
|
import { INTERNAL_ERROR } from '../../../util/ContentTypes';
|
||||||
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
|
|
||||||
import { toLiteral } from '../../../util/TermUtil';
|
|
||||||
import { HTTP, XSD } from '../../../util/Vocabularies';
|
|
||||||
import type { PreferenceParser } from '../../input/preferences/PreferenceParser';
|
import type { PreferenceParser } from '../../input/preferences/PreferenceParser';
|
||||||
import { BasicRepresentation } from '../../representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../representation/Representation';
|
import type { Representation } from '../../representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
|
||||||
import type { ResponseDescription } from '../response/ResponseDescription';
|
import type { ResponseDescription } from '../response/ResponseDescription';
|
||||||
import type { ErrorHandlerArgs } from './ErrorHandler';
|
import type { ErrorHandlerArgs } from './ErrorHandler';
|
||||||
import { ErrorHandler } from './ErrorHandler';
|
import { ErrorHandler } from './ErrorHandler';
|
||||||
@ -64,11 +60,13 @@ export class ConvertingErrorHandler extends ErrorHandler {
|
|||||||
* Prepares the arguments used by all functions.
|
* Prepares the arguments used by all functions.
|
||||||
*/
|
*/
|
||||||
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
|
private async extractErrorDetails({ error, request }: ErrorHandlerArgs): Promise<PreparedArguments> {
|
||||||
const statusCode = getStatusCode(error);
|
if (!this.showStackTrace) {
|
||||||
const representation = this.toRepresentation(error, statusCode);
|
delete error.stack;
|
||||||
|
}
|
||||||
|
const representation = new BasicRepresentation([ error ], error.metadata, INTERNAL_ERROR, false);
|
||||||
const identifier = { path: representation.metadata.identifier.value };
|
const identifier = { path: representation.metadata.identifier.value };
|
||||||
const preferences = await this.preferenceParser.handle({ request });
|
const preferences = await this.preferenceParser.handle({ request });
|
||||||
return { statusCode, conversionArgs: { identifier, representation, preferences }};
|
return { statusCode: error.statusCode, conversionArgs: { identifier, representation, preferences }};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -81,20 +79,4 @@ export class ConvertingErrorHandler extends ErrorHandler {
|
|||||||
data: converted.data,
|
data: converted.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Representation based on the given error.
|
|
||||||
* Content type will be internal/error.
|
|
||||||
* The status code is used for metadata.
|
|
||||||
*/
|
|
||||||
private toRepresentation(error: Error, statusCode: number): Representation {
|
|
||||||
const metadata = new RepresentationMetadata(INTERNAL_ERROR);
|
|
||||||
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
|
|
||||||
|
|
||||||
if (!this.showStackTrace) {
|
|
||||||
delete error.stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BasicRepresentation([ error ], metadata, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import type { HttpRequest } from '../../../server/HttpRequest';
|
import type { HttpRequest } from '../../../server/HttpRequest';
|
||||||
|
import type { HttpError } from '../../../util/errors/HttpError';
|
||||||
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
import { AsyncHandler } from '../../../util/handlers/AsyncHandler';
|
||||||
import type { ResponseDescription } from '../response/ResponseDescription';
|
import type { ResponseDescription } from '../response/ResponseDescription';
|
||||||
|
|
||||||
export interface ErrorHandlerArgs {
|
export interface ErrorHandlerArgs {
|
||||||
error: Error;
|
error: HttpError;
|
||||||
request: HttpRequest;
|
request: HttpRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { getLoggerFor } from '../../../logging/LogUtil';
|
import { getLoggerFor } from '../../../logging/LogUtil';
|
||||||
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
|
import { createErrorMessage } from '../../../util/errors/ErrorUtil';
|
||||||
import { getStatusCode } from '../../../util/errors/HttpErrorUtil';
|
|
||||||
import { guardedStreamFrom } from '../../../util/StreamUtil';
|
import { guardedStreamFrom } from '../../../util/StreamUtil';
|
||||||
import { toLiteral } from '../../../util/TermUtil';
|
|
||||||
import { HTTP, XSD } from '../../../util/Vocabularies';
|
|
||||||
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
|
||||||
import type { ResponseDescription } from '../response/ResponseDescription';
|
import type { ResponseDescription } from '../response/ResponseDescription';
|
||||||
import type { ErrorHandlerArgs } from './ErrorHandler';
|
import type { ErrorHandlerArgs } from './ErrorHandler';
|
||||||
import { ErrorHandler } from './ErrorHandler';
|
import { ErrorHandler } from './ErrorHandler';
|
||||||
@ -32,17 +28,15 @@ export class SafeErrorHandler extends ErrorHandler {
|
|||||||
this.logger.debug(`Recovering from error handler failure: ${createErrorMessage(error)}`);
|
this.logger.debug(`Recovering from error handler failure: ${createErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
const { error } = input;
|
const { error } = input;
|
||||||
const statusCode = getStatusCode(error);
|
error.metadata.contentType = 'text/plain';
|
||||||
const metadata = new RepresentationMetadata('text/plain');
|
|
||||||
metadata.add(HTTP.terms.statusCodeNumber, toLiteral(statusCode, XSD.terms.integer));
|
|
||||||
|
|
||||||
const text = typeof error.stack === 'string' && this.showStackTrace ?
|
const text = typeof error.stack === 'string' && this.showStackTrace ?
|
||||||
`${error.stack}\n` :
|
`${error.stack}\n` :
|
||||||
`${error.name}: ${error.message}\n`;
|
`${error.name}: ${error.message}\n`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode,
|
statusCode: error.statusCode,
|
||||||
metadata,
|
metadata: error.metadata,
|
||||||
data: guardedStreamFrom(text),
|
data: guardedStreamFrom(text),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { RedirectHttpError } from '../../../util/errors/RedirectHttpError';
|
import type { RedirectHttpError } from '../../../util/errors/RedirectHttpError';
|
||||||
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
import { SOLID_HTTP } from '../../../util/Vocabularies';
|
||||||
import { RepresentationMetadata } from '../../representation/RepresentationMetadata';
|
|
||||||
import { ResponseDescription } from './ResponseDescription';
|
import { ResponseDescription } from './ResponseDescription';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,7 +8,7 @@ import { ResponseDescription } from './ResponseDescription';
|
|||||||
*/
|
*/
|
||||||
export class RedirectResponseDescription extends ResponseDescription {
|
export class RedirectResponseDescription extends ResponseDescription {
|
||||||
public constructor(error: RedirectHttpError) {
|
public constructor(error: RedirectHttpError) {
|
||||||
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(error.location) });
|
error.metadata.set(SOLID_HTTP.terms.location, DataFactory.namedNode(error.location));
|
||||||
super(error.statusCode, metadata);
|
super(error.statusCode, error.metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { DataFactory, Store } from 'n3';
|
import { DataFactory, Store } from 'n3';
|
||||||
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
import type { BlankNode, DefaultGraph, Literal, NamedNode, Quad, Term } from 'rdf-js';
|
||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
import { ContentType, SIMPLE_MEDIA_RANGE } from '../../util/Header';
|
||||||
import { ContentType, parseContentType } from '../../util/HeaderUtil';
|
|
||||||
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
|
import { toNamedTerm, toObjectTerm, isTerm, toLiteral } from '../../util/TermUtil';
|
||||||
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
|
import { CONTENT_TYPE_TERM, CONTENT_LENGTH_TERM, XSD, SOLID_META, RDFS } from '../../util/Vocabularies';
|
||||||
import type { ResourceIdentifier } from './ResourceIdentifier';
|
import type { ResourceIdentifier } from './ResourceIdentifier';
|
||||||
@ -67,18 +66,18 @@ export class RepresentationMetadata {
|
|||||||
* @param identifier - Identifier of the resource relevant to this metadata.
|
* @param identifier - Identifier of the resource relevant to this metadata.
|
||||||
* @param contentType - Override for the content type of the representation.
|
* @param contentType - Override for the content type of the representation.
|
||||||
*/
|
*/
|
||||||
public constructor(identifier?: MetadataIdentifier, contentType?: string);
|
public constructor(identifier?: MetadataIdentifier, contentType?: string | ContentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param metadata - Starts as a copy of the input metadata.
|
* @param metadata - Starts as a copy of the input metadata.
|
||||||
* @param contentType - Override for the content type of the representation.
|
* @param contentType - Override for the content type of the representation.
|
||||||
*/
|
*/
|
||||||
public constructor(metadata?: RepresentationMetadata, contentType?: string);
|
public constructor(metadata?: RepresentationMetadata, contentType?: string | ContentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param contentType - The content type of the representation.
|
* @param contentType - The content type of the representation.
|
||||||
*/
|
*/
|
||||||
public constructor(contentType?: string);
|
public constructor(contentType?: string | ContentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param metadata - Metadata values (defaulting to content type if a string)
|
* @param metadata - Metadata values (defaulting to content type if a string)
|
||||||
@ -86,8 +85,8 @@ export class RepresentationMetadata {
|
|||||||
public constructor(metadata?: RepresentationMetadata | MetadataRecord | string);
|
public constructor(metadata?: RepresentationMetadata | MetadataRecord | string);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
input?: MetadataIdentifier | RepresentationMetadata | MetadataRecord | string,
|
input?: MetadataIdentifier | RepresentationMetadata | MetadataRecord | ContentType | string,
|
||||||
overrides?: MetadataRecord | string,
|
overrides?: MetadataRecord | string | ContentType,
|
||||||
) {
|
) {
|
||||||
this.store = new Store();
|
this.store = new Store();
|
||||||
if (isResourceIdentifier(input)) {
|
if (isResourceIdentifier(input)) {
|
||||||
@ -105,6 +104,8 @@ export class RepresentationMetadata {
|
|||||||
if (overrides) {
|
if (overrides) {
|
||||||
if (typeof overrides === 'string') {
|
if (typeof overrides === 'string') {
|
||||||
this.contentType = overrides;
|
this.contentType = overrides;
|
||||||
|
} else if (overrides instanceof ContentType) {
|
||||||
|
this.contentTypeObject = overrides;
|
||||||
} else {
|
} else {
|
||||||
this.setOverrides(overrides);
|
this.setOverrides(overrides);
|
||||||
}
|
}
|
||||||
@ -313,7 +314,8 @@ export class RepresentationMetadata {
|
|||||||
}
|
}
|
||||||
if (terms.length > 1) {
|
if (terms.length > 1) {
|
||||||
this.logger.error(`Multiple results for ${predicate.value}`);
|
this.logger.error(`Multiple results for ${predicate.value}`);
|
||||||
throw new InternalServerError(
|
// We can not use an `InternalServerError` here as otherwise errors and metadata files would depend on each other
|
||||||
|
throw new Error(
|
||||||
`Multiple results for ${predicate.value}`,
|
`Multiple results for ${predicate.value}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -344,7 +346,16 @@ export class RepresentationMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
input = parseContentType(input);
|
// Simple check to estimate if this is a simple content type.
|
||||||
|
// If not, mention that the `contentTypeObject` should be used instead.
|
||||||
|
// Not calling `parseContentType` here as that would cause a dependency loop with `HttpError`.
|
||||||
|
if (!SIMPLE_MEDIA_RANGE.test(input)) {
|
||||||
|
// Not using an HttpError as HttpError depends on metadata
|
||||||
|
throw new Error(
|
||||||
|
'Only simple content types can be set by string. Use the `contentTypeObject` function for complexer types.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
input = new ContentType(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [ key, value ] of Object.entries(input.parameters)) {
|
for (const [ key, value ] of Object.entries(input.parameters)) {
|
||||||
|
@ -20,6 +20,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
|||||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
import type { HttpError } from '../../util/errors/HttpError';
|
import type { HttpError } from '../../util/errors/HttpError';
|
||||||
|
import { errorTermsToMetadata } from '../../util/errors/HttpErrorUtil';
|
||||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||||
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
||||||
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
||||||
@ -398,10 +399,10 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
const unknownClientError = new BadRequestHttpError(
|
const unknownClientError = new BadRequestHttpError(
|
||||||
'Unknown client, you might need to clear the local storage on the client.', {
|
'Unknown client, you might need to clear the local storage on the client.', {
|
||||||
errorCode: 'E0003',
|
errorCode: 'E0003',
|
||||||
details: {
|
metadata: errorTermsToMetadata({
|
||||||
client_id: ctx.request.query.client_id,
|
client_id: ctx.request.query.client_id as string,
|
||||||
redirect_uri: ctx.request.query.redirect_uri,
|
redirect_uri: ctx.request.query.redirect_uri as string,
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
unknownClientError.stack = oidcError.stack;
|
unknownClientError.stack = oidcError.stack;
|
||||||
|
@ -4,6 +4,7 @@ import { v4 } from 'uuid';
|
|||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
import type { ExpiringStorage } from '../../storage/keyvalue/ExpiringStorage';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
|
import { errorTermsToMetadata } from '../../util/errors/HttpErrorUtil';
|
||||||
import { fetchDataset } from '../../util/FetchUtil';
|
import { fetchDataset } from '../../util/FetchUtil';
|
||||||
import { SOLID } from '../../util/Vocabularies';
|
import { SOLID } from '../../util/Vocabularies';
|
||||||
import { OwnershipValidator } from './OwnershipValidator';
|
import { OwnershipValidator } from './OwnershipValidator';
|
||||||
@ -89,6 +90,6 @@ export class TokenOwnershipValidator extends OwnershipValidator {
|
|||||||
'You can remove this triple again after validation.',
|
'You can remove this triple again after validation.',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
const details = { quad: `<${webId}> <${SOLID.oidcIssuerRegistrationToken}> "${token}".` };
|
const details = { quad: `<${webId}> <${SOLID.oidcIssuerRegistrationToken}> "${token}".` };
|
||||||
throw new BadRequestHttpError(errorMessage, { details });
|
throw new BadRequestHttpError(errorMessage, { metadata: errorTermsToMetadata(details) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,9 @@ import type { ErrorHandler } from '../http/output/error/ErrorHandler';
|
|||||||
import type { ResponseDescription } from '../http/output/response/ResponseDescription';
|
import type { ResponseDescription } from '../http/output/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../http/output/ResponseWriter';
|
import type { ResponseWriter } from '../http/output/ResponseWriter';
|
||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import { assertError } from '../util/errors/ErrorUtil';
|
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||||
import { HttpError } from '../util/errors/HttpError';
|
import { HttpError } from '../util/errors/HttpError';
|
||||||
|
import { InternalServerError } from '../util/errors/InternalServerError';
|
||||||
import type { HttpHandlerInput } from './HttpHandler';
|
import type { HttpHandlerInput } from './HttpHandler';
|
||||||
import { HttpHandler } from './HttpHandler';
|
import { HttpHandler } from './HttpHandler';
|
||||||
import type { HttpRequest } from './HttpRequest';
|
import type { HttpRequest } from './HttpRequest';
|
||||||
@ -81,12 +82,11 @@ export class ParsingHttpHandler extends HttpHandler {
|
|||||||
* Handles the error output correctly based on the preferences.
|
* Handles the error output correctly based on the preferences.
|
||||||
*/
|
*/
|
||||||
protected async handleError(error: unknown, request: HttpRequest): Promise<ResponseDescription> {
|
protected async handleError(error: unknown, request: HttpRequest): Promise<ResponseDescription> {
|
||||||
assertError(error);
|
if (!HttpError.isInstance(error)) {
|
||||||
const result = await this.errorHandler.handleSafe({ error, request });
|
error = new InternalServerError(`Received unexpected non-HttpError: ${createErrorMessage(error)}`,
|
||||||
if (HttpError.isInstance(error) && result.metadata) {
|
{ cause: error });
|
||||||
const quads = error.generateMetadata(result.metadata.identifier);
|
|
||||||
result.metadata.addQuads(quads);
|
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
|
return this.errorHandler.handleSafe({ error: error as HttpError, request });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||||
import { HttpError } from '../../util/errors/HttpError';
|
import type { HttpError } from '../../util/errors/HttpError';
|
||||||
|
import { extractErrorTerms } from '../../util/errors/HttpErrorUtil';
|
||||||
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
||||||
import { getSingleItem } from '../../util/StreamUtil';
|
import { getSingleItem } from '../../util/StreamUtil';
|
||||||
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
||||||
@ -16,11 +17,14 @@ export class ErrorToJsonConverter extends BaseTypedRepresentationConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
const error = await getSingleItem(representation.data) as Error;
|
const error = await getSingleItem(representation.data) as HttpError;
|
||||||
|
|
||||||
const result: Record<string, any> = {
|
const result: Record<string, any> = {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
errorCode: error.errorCode,
|
||||||
|
details: extractErrorTerms(error.metadata),
|
||||||
};
|
};
|
||||||
|
|
||||||
// OAuth errors responses require additional fields
|
// OAuth errors responses require additional fields
|
||||||
@ -28,22 +32,6 @@ export class ErrorToJsonConverter extends BaseTypedRepresentationConverter {
|
|||||||
Object.assign(result, error.mandatoryFields);
|
Object.assign(result, error.mandatoryFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HttpError.isInstance(error)) {
|
|
||||||
result.statusCode = error.statusCode;
|
|
||||||
result.errorCode = error.errorCode;
|
|
||||||
if (error.details) {
|
|
||||||
try {
|
|
||||||
// The details might not be serializable
|
|
||||||
JSON.stringify(error.details);
|
|
||||||
result.details = error.details;
|
|
||||||
} catch {
|
|
||||||
// Do not store the details
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.statusCode = 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
result.stack = error.stack;
|
result.stack = error.stack;
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,9 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
|
|||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
|
import { INTERNAL_ERROR, INTERNAL_QUADS } from '../../util/ContentTypes';
|
||||||
|
import type { HttpError } from '../../util/errors/HttpError';
|
||||||
import { getSingleItem } from '../../util/StreamUtil';
|
import { getSingleItem } from '../../util/StreamUtil';
|
||||||
import { DC, SOLID_ERROR } from '../../util/Vocabularies';
|
import { DC, SOLID_ERROR, SOLID_ERROR_TERM } from '../../util/Vocabularies';
|
||||||
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ export class ErrorToQuadConverter extends BaseTypedRepresentationConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
|
public async handle({ identifier, representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
const error = await getSingleItem(representation.data) as Error;
|
const error = await getSingleItem(representation.data) as HttpError;
|
||||||
|
|
||||||
// A metadata object makes it easier to add triples due to the utility functions
|
// A metadata object makes it easier to add triples due to the utility functions
|
||||||
const data = new RepresentationMetadata(identifier);
|
const data = new RepresentationMetadata(identifier);
|
||||||
@ -25,6 +26,9 @@ export class ErrorToQuadConverter extends BaseTypedRepresentationConverter {
|
|||||||
if (error.stack) {
|
if (error.stack) {
|
||||||
data.add(SOLID_ERROR.terms.stack, error.stack);
|
data.add(SOLID_ERROR.terms.stack, error.stack);
|
||||||
}
|
}
|
||||||
|
// Add all the error terms from the metadata
|
||||||
|
data.addQuads(representation.metadata.quads()
|
||||||
|
.filter((quad): boolean => quad.predicate.value.startsWith(SOLID_ERROR_TERM.namespace)));
|
||||||
|
|
||||||
// Update the content-type to quads
|
// Update the content-type to quads
|
||||||
return new BasicRepresentation(data.quads(), representation.metadata, INTERNAL_QUADS, false);
|
return new BasicRepresentation(data.quads(), representation.metadata, INTERNAL_QUADS, false);
|
||||||
|
@ -2,7 +2,8 @@ import assert from 'assert';
|
|||||||
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../http/representation/BasicRepresentation';
|
||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import { INTERNAL_ERROR } from '../../util/ContentTypes';
|
import { INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||||
import { HttpError } from '../../util/errors/HttpError';
|
import type { HttpError } from '../../util/errors/HttpError';
|
||||||
|
import { extractErrorTerms } from '../../util/errors/HttpErrorUtil';
|
||||||
import { resolveModulePath } from '../../util/PathUtil';
|
import { resolveModulePath } from '../../util/PathUtil';
|
||||||
import { getSingleItem } from '../../util/StreamUtil';
|
import { getSingleItem } from '../../util/StreamUtil';
|
||||||
import { isValidFileName } from '../../util/StringUtil';
|
import { isValidFileName } from '../../util/StringUtil';
|
||||||
@ -57,20 +58,21 @@ export class ErrorToTemplateConverter extends BaseTypedRepresentationConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||||
const error = await getSingleItem(representation.data) as Error;
|
const error = await getSingleItem(representation.data) as HttpError;
|
||||||
|
|
||||||
// Render the error description using an error-specific template
|
// Render the error description using an error-specific template
|
||||||
let description: string | undefined;
|
let description: string | undefined;
|
||||||
if (HttpError.isInstance(error)) {
|
|
||||||
try {
|
try {
|
||||||
const templateFile = `${error.errorCode}${this.extension}`;
|
const templateFile = `${error.errorCode}${this.extension}`;
|
||||||
assert(isValidFileName(templateFile), 'Invalid error template name');
|
assert(isValidFileName(templateFile), 'Invalid error template name');
|
||||||
description = await this.templateEngine.handleSafe({ contents: error.details ?? {},
|
// Filter out the error terms to pass to the template
|
||||||
template: { templateFile, templatePath: this.codeTemplatesPath }});
|
description = await this.templateEngine.handleSafe({
|
||||||
|
contents: extractErrorTerms(error.metadata),
|
||||||
|
template: { templateFile, templatePath: this.codeTemplatesPath },
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// In case no template is found, or rendering errors, we still want to convert
|
// In case no template is found, or rendering errors, we still want to convert
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Render the main template, embedding the rendered error description
|
// Render the main template, embedding the rendered error description
|
||||||
const { name, message, stack } = error;
|
const { name, message, stack } = error;
|
||||||
|
124
src/util/Header.ts
Normal file
124
src/util/Header.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// The interfaces here are split off from HttpErrorUtil.ts to prevent a dependency loop in RepresentationMetadata
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General interface for all Accept* headers.
|
||||||
|
*/
|
||||||
|
export interface AcceptHeader {
|
||||||
|
/** Requested range. Can be a specific value or `*`, matching all. */
|
||||||
|
range: string;
|
||||||
|
/** Weight of the preference [0, 1]. */
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Accept header.
|
||||||
|
* Range is type/subtype. Both can be `*`.
|
||||||
|
*/
|
||||||
|
export interface Accept extends AcceptHeader {
|
||||||
|
parameters: {
|
||||||
|
/** Media type parameters. These are the parameters that came before the q value. */
|
||||||
|
mediaType: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Extension parameters. These are the parameters that came after the q value.
|
||||||
|
* Value will be an empty string if there was none.
|
||||||
|
*/
|
||||||
|
extension: Record<string, string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Accept-Charset header.
|
||||||
|
*/
|
||||||
|
export interface AcceptCharset extends AcceptHeader { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Accept-Encoding header.
|
||||||
|
*/
|
||||||
|
export interface AcceptEncoding extends AcceptHeader { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Accept-Language header.
|
||||||
|
*/
|
||||||
|
export interface AcceptLanguage extends AcceptHeader { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Accept-Datetime header.
|
||||||
|
*/
|
||||||
|
export interface AcceptDatetime extends AcceptHeader { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contents of an HTTP Content-Type Header.
|
||||||
|
* Optional parameters Record is included.
|
||||||
|
*/
|
||||||
|
export class ContentType {
|
||||||
|
public constructor(public value: string, public parameters: Record<string, string> = {}) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize this ContentType object to a ContentType header appropriate value string.
|
||||||
|
* @returns The value string, including parameters, if present.
|
||||||
|
*/
|
||||||
|
public toHeaderValueString(): string {
|
||||||
|
return Object.entries(this.parameters)
|
||||||
|
.sort((entry1, entry2): number => entry1[0].localeCompare(entry2[0]))
|
||||||
|
.reduce((acc, entry): string => `${acc}; ${entry[0]}=${entry[1]}`, this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkEntryParameters extends Record<string, string> {
|
||||||
|
/** Required rel properties of Link entry */
|
||||||
|
rel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkEntry {
|
||||||
|
target: string;
|
||||||
|
parameters: LinkEntryParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// BNF based on https://tools.ietf.org/html/rfc7231
|
||||||
|
//
|
||||||
|
// media-type = type "/" subtype *( OWS ";" OWS parameter )
|
||||||
|
//
|
||||||
|
// media-range = ( "*/*"
|
||||||
|
// / ( type "/" "*" )
|
||||||
|
// / ( type "/" subtype )
|
||||||
|
// ) *( OWS ";" OWS parameter ) ; media type parameters
|
||||||
|
// accept-params = weight *( accept-ext )
|
||||||
|
// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] ; extension parameters
|
||||||
|
//
|
||||||
|
// weight = OWS ";" OWS "q=" qvalue
|
||||||
|
// qvalue = ( "0" [ "." 0*3DIGIT ] )
|
||||||
|
// / ( "1" [ "." 0*3("0") ] )
|
||||||
|
//
|
||||||
|
// type = token
|
||||||
|
// subtype = token
|
||||||
|
// parameter = token "=" ( token / quoted-string )
|
||||||
|
//
|
||||||
|
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
||||||
|
// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
||||||
|
// obs-text = %x80-FF
|
||||||
|
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
||||||
|
//
|
||||||
|
// charset = token
|
||||||
|
//
|
||||||
|
// codings = content-coding / "identity" / "*"
|
||||||
|
// content-coding = token
|
||||||
|
//
|
||||||
|
// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
|
||||||
|
// alphanum = ALPHA / DIGIT
|
||||||
|
//
|
||||||
|
// Delimiters are chosen from the set of US-ASCII visual characters
|
||||||
|
// not allowed in a token (DQUOTE and "(),/:;<=>?@[\]{}").
|
||||||
|
// token = 1*tchar
|
||||||
|
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
||||||
|
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
||||||
|
// / DIGIT / ALPHA
|
||||||
|
// ; any VCHAR, except delimiters
|
||||||
|
//
|
||||||
|
|
||||||
|
// REUSED REGEXES
|
||||||
|
export const TCHAR = /[a-zA-Z0-9!#$%&'*+-.^_`|~]/u;
|
||||||
|
export const TOKEN = new RegExp(`^${TCHAR.source}+$`, 'u');
|
||||||
|
export const SIMPLE_MEDIA_RANGE = new RegExp(`^${TCHAR.source}+/${TCHAR.source}+$`, 'u');
|
||||||
|
export const QUOTED_STRING =
|
||||||
|
/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u;
|
||||||
|
export const QVALUE = /^(?:(0(?:\.\d{0,3})?)|(1(?:\.0{0,3})?))$/u;
|
@ -3,10 +3,18 @@ import escapeStringRegexp from 'escape-string-regexp';
|
|||||||
import { getLoggerFor } from '../logging/LogUtil';
|
import { getLoggerFor } from '../logging/LogUtil';
|
||||||
import type { HttpResponse } from '../server/HttpResponse';
|
import type { HttpResponse } from '../server/HttpResponse';
|
||||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||||
|
import type {
|
||||||
|
Accept,
|
||||||
|
AcceptCharset,
|
||||||
|
AcceptDatetime,
|
||||||
|
AcceptEncoding,
|
||||||
|
AcceptHeader,
|
||||||
|
AcceptLanguage,
|
||||||
|
LinkEntry,
|
||||||
|
} from './Header';
|
||||||
|
import { ContentType, SIMPLE_MEDIA_RANGE, QUOTED_STRING, QVALUE, TOKEN } from './Header';
|
||||||
|
|
||||||
const logger = getLoggerFor('HeaderUtil');
|
const logger = getLoggerFor('HeaderUtil');
|
||||||
// Map used as a simple cache in the helper function matchesAuthorizationScheme.
|
|
||||||
const authSchemeRegexCache: Map<string, RegExp> = new Map();
|
|
||||||
|
|
||||||
// BNF based on https://tools.ietf.org/html/rfc7231
|
// BNF based on https://tools.ietf.org/html/rfc7231
|
||||||
//
|
//
|
||||||
@ -16,124 +24,6 @@ const authSchemeRegexCache: Map<string, RegExp> = new Map();
|
|||||||
// Accept-Language = 1#( language-range [ weight ] )
|
// Accept-Language = 1#( language-range [ weight ] )
|
||||||
//
|
//
|
||||||
// Content-Type = media-type
|
// Content-Type = media-type
|
||||||
// media-type = type "/" subtype *( OWS ";" OWS parameter )
|
|
||||||
//
|
|
||||||
// media-range = ( "*/*"
|
|
||||||
// / ( type "/" "*" )
|
|
||||||
// / ( type "/" subtype )
|
|
||||||
// ) *( OWS ";" OWS parameter ) ; media type parameters
|
|
||||||
// accept-params = weight *( accept-ext )
|
|
||||||
// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] ; extension parameters
|
|
||||||
//
|
|
||||||
// weight = OWS ";" OWS "q=" qvalue
|
|
||||||
// qvalue = ( "0" [ "." 0*3DIGIT ] )
|
|
||||||
// / ( "1" [ "." 0*3("0") ] )
|
|
||||||
//
|
|
||||||
// type = token
|
|
||||||
// subtype = token
|
|
||||||
// parameter = token "=" ( token / quoted-string )
|
|
||||||
//
|
|
||||||
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
|
||||||
// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
|
||||||
// obs-text = %x80-FF
|
|
||||||
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
|
||||||
//
|
|
||||||
// charset = token
|
|
||||||
//
|
|
||||||
// codings = content-coding / "identity" / "*"
|
|
||||||
// content-coding = token
|
|
||||||
//
|
|
||||||
// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
|
|
||||||
// alphanum = ALPHA / DIGIT
|
|
||||||
//
|
|
||||||
// Delimiters are chosen from the set of US-ASCII visual characters
|
|
||||||
// not allowed in a token (DQUOTE and "(),/:;<=>?@[\]{}").
|
|
||||||
// token = 1*tchar
|
|
||||||
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
||||||
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
|
||||||
// / DIGIT / ALPHA
|
|
||||||
// ; any VCHAR, except delimiters
|
|
||||||
//
|
|
||||||
|
|
||||||
// INTERFACES
|
|
||||||
/**
|
|
||||||
* General interface for all Accept* headers.
|
|
||||||
*/
|
|
||||||
export interface AcceptHeader {
|
|
||||||
/** Requested range. Can be a specific value or `*`, matching all. */
|
|
||||||
range: string;
|
|
||||||
/** Weight of the preference [0, 1]. */
|
|
||||||
weight: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of an HTTP Accept header.
|
|
||||||
* Range is type/subtype. Both can be `*`.
|
|
||||||
*/
|
|
||||||
export interface Accept extends AcceptHeader {
|
|
||||||
parameters: {
|
|
||||||
/** Media type parameters. These are the parameters that came before the q value. */
|
|
||||||
mediaType: Record<string, string>;
|
|
||||||
/**
|
|
||||||
* Extension parameters. These are the parameters that came after the q value.
|
|
||||||
* Value will be an empty string if there was none.
|
|
||||||
*/
|
|
||||||
extension: Record<string, string>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of an HTTP Accept-Charset header.
|
|
||||||
*/
|
|
||||||
export interface AcceptCharset extends AcceptHeader { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of an HTTP Accept-Encoding header.
|
|
||||||
*/
|
|
||||||
export interface AcceptEncoding extends AcceptHeader { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of an HTTP Accept-Language header.
|
|
||||||
*/
|
|
||||||
export interface AcceptLanguage extends AcceptHeader { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of an HTTP Accept-Datetime header.
|
|
||||||
*/
|
|
||||||
export interface AcceptDatetime extends AcceptHeader { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contents of a HTTP Content-Type Header.
|
|
||||||
* Optional parameters Record is included.
|
|
||||||
*/
|
|
||||||
export class ContentType {
|
|
||||||
public constructor(public value: string, public parameters: Record<string, string> = {}) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serialize this ContentType object to a ContentType header appropriate value string.
|
|
||||||
* @returns The value string, including parameters, if present.
|
|
||||||
*/
|
|
||||||
public toHeaderValueString(): string {
|
|
||||||
return Object.entries(this.parameters)
|
|
||||||
.sort((entry1, entry2): number => entry1[0].localeCompare(entry2[0]))
|
|
||||||
.reduce((acc, entry): string => `${acc}; ${entry[0]}=${entry[1]}`, this.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LinkEntryParameters extends Record<string, string> {
|
|
||||||
/** Required rel properties of Link entry */
|
|
||||||
rel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LinkEntry {
|
|
||||||
target: string;
|
|
||||||
parameters: LinkEntryParameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// REUSED REGEXES
|
|
||||||
const tchar = /[a-zA-Z0-9!#$%&'*+-.^_`|~]/u;
|
|
||||||
const token = new RegExp(`^${tchar.source}+$`, 'u');
|
|
||||||
const mediaRange = new RegExp(`${tchar.source}+/${tchar.source}+`, 'u');
|
|
||||||
|
|
||||||
// HELPER FUNCTIONS
|
// HELPER FUNCTIONS
|
||||||
/**
|
/**
|
||||||
@ -150,7 +40,7 @@ export function transformQuotedStrings(input: string): { result: string; replace
|
|||||||
const replacements: Record<string, string> = {};
|
const replacements: Record<string, string> = {};
|
||||||
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
||||||
// Not all characters allowed in quoted strings, see BNF above
|
// Not all characters allowed in quoted strings, see BNF above
|
||||||
if (!/^"(?:[\t !\u0023-\u005B\u005D-\u007E\u0080-\u00FF]|(?:\\[\t\u0020-\u007E\u0080-\u00FF]))*"$/u.test(match)) {
|
if (!QUOTED_STRING.test(match)) {
|
||||||
logger.warn(`Invalid quoted string in header: ${match}`);
|
logger.warn(`Invalid quoted string in header: ${match}`);
|
||||||
throw new BadRequestHttpError(`Invalid quoted string in header: ${match}`);
|
throw new BadRequestHttpError(`Invalid quoted string in header: ${match}`);
|
||||||
}
|
}
|
||||||
@ -175,17 +65,6 @@ export function splitAndClean(input: string): string[] {
|
|||||||
.filter((part): boolean => part.length > 0);
|
.filter((part): boolean => part.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the input string matches the qvalue regex.
|
|
||||||
*
|
|
||||||
* @param qvalue - Input qvalue string (so "q=....").
|
|
||||||
*
|
|
||||||
* @returns true if q value is valid, false otherwise.
|
|
||||||
*/
|
|
||||||
function isValidQValue(qvalue: string): boolean {
|
|
||||||
return /^(?:(?:0(?:\.\d{0,3})?)|(?:1(?:\.0{0,3})?))$/u.test(qvalue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a qvalue to a number.
|
* Converts a qvalue to a number.
|
||||||
* Returns 1 if the value is not a valid number or 1 if it is more than 1.
|
* Returns 1 if the value is not a valid number or 1 if it is more than 1.
|
||||||
@ -237,7 +116,7 @@ export function parseParameters(parameters: string[], replacements: Record<strin
|
|||||||
// Test replaced string for easier check
|
// Test replaced string for easier check
|
||||||
// parameter = token "=" ( token / quoted-string )
|
// parameter = token "=" ( token / quoted-string )
|
||||||
// second part is optional for certain parameters
|
// second part is optional for certain parameters
|
||||||
if (!(token.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || token.test(rawValue)))) {
|
if (!(TOKEN.test(name) && (!rawValue || /^"\d+"$/u.test(rawValue) || TOKEN.test(rawValue)))) {
|
||||||
handleInvalidValue(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
|
handleInvalidValue(`Invalid parameter value: ${name}=${replacements[rawValue] || rawValue} ` +
|
||||||
`does not match (token ( "=" ( token / quoted-string ))?). `, strict);
|
`does not match (token ( "=" ( token / quoted-string ))?). `, strict);
|
||||||
return acc;
|
return acc;
|
||||||
@ -271,7 +150,7 @@ function parseAcceptPart(part: string, replacements: Record<string, string>, str
|
|||||||
const [ range, ...parameters ] = part.split(';').map((param): string => param.trim());
|
const [ range, ...parameters ] = part.split(';').map((param): string => param.trim());
|
||||||
|
|
||||||
// No reason to test differently for * since we don't check if the type exists
|
// No reason to test differently for * since we don't check if the type exists
|
||||||
if (!mediaRange.test(range)) {
|
if (!SIMPLE_MEDIA_RANGE.test(range)) {
|
||||||
handleInvalidValue(
|
handleInvalidValue(
|
||||||
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, strict,
|
`Invalid Accept range: ${range} does not match ( "*/*" / ( token "/" "*" ) / ( token "/" token ) )`, strict,
|
||||||
);
|
);
|
||||||
@ -287,7 +166,7 @@ function parseAcceptPart(part: string, replacements: Record<string, string>, str
|
|||||||
if (name === 'q') {
|
if (name === 'q') {
|
||||||
// Extension parameters appear after the q value
|
// Extension parameters appear after the q value
|
||||||
map = extensionParams;
|
map = extensionParams;
|
||||||
if (!isValidQValue(value)) {
|
if (!QVALUE.test(value)) {
|
||||||
handleInvalidValue(`Invalid q value for range ${range}: ${value
|
handleInvalidValue(`Invalid q value for range ${range}: ${value
|
||||||
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
||||||
}
|
}
|
||||||
@ -332,7 +211,7 @@ function parseNoParameters(input: string, strict = false): AcceptHeader[] {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const val = qvalue.slice(2);
|
const val = qvalue.slice(2);
|
||||||
if (!isValidQValue(val)) {
|
if (!QVALUE.test(val)) {
|
||||||
handleInvalidValue(`Invalid q value for range ${range}: ${val
|
handleInvalidValue(`Invalid q value for range ${range}: ${val
|
||||||
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
||||||
}
|
}
|
||||||
@ -382,7 +261,7 @@ export function parseAccept(input: string, strict = false): Accept[] {
|
|||||||
export function parseAcceptCharset(input: string, strict = false): AcceptCharset[] {
|
export function parseAcceptCharset(input: string, strict = false): AcceptCharset[] {
|
||||||
const results = parseNoParameters(input);
|
const results = parseNoParameters(input);
|
||||||
return results.filter((result): boolean => {
|
return results.filter((result): boolean => {
|
||||||
if (!token.test(result.range)) {
|
if (!TOKEN.test(result.range)) {
|
||||||
handleInvalidValue(
|
handleInvalidValue(
|
||||||
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`, strict,
|
`Invalid Accept-Charset range: ${result.range} does not match (content-coding / "identity" / "*")`, strict,
|
||||||
);
|
);
|
||||||
@ -404,7 +283,7 @@ export function parseAcceptCharset(input: string, strict = false): AcceptCharset
|
|||||||
export function parseAcceptEncoding(input: string, strict = false): AcceptEncoding[] {
|
export function parseAcceptEncoding(input: string, strict = false): AcceptEncoding[] {
|
||||||
const results = parseNoParameters(input);
|
const results = parseNoParameters(input);
|
||||||
return results.filter((result): boolean => {
|
return results.filter((result): boolean => {
|
||||||
if (!token.test(result.range)) {
|
if (!TOKEN.test(result.range)) {
|
||||||
handleInvalidValue(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`, strict);
|
handleInvalidValue(`Invalid Accept-Encoding range: ${result.range} does not match (charset / "*")`, strict);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -495,7 +374,7 @@ export function parseContentType(input: string): ContentType {
|
|||||||
// Quoted strings could prevent split from having correct results
|
// Quoted strings could prevent split from having correct results
|
||||||
const { result, replacements } = transformQuotedStrings(input);
|
const { result, replacements } = transformQuotedStrings(input);
|
||||||
const [ value, ...params ] = result.split(';').map((str): string => str.trim());
|
const [ value, ...params ] = result.split(';').map((str): string => str.trim());
|
||||||
if (!mediaRange.test(value)) {
|
if (!SIMPLE_MEDIA_RANGE.test(value)) {
|
||||||
logger.warn(`Invalid content-type: ${value}`);
|
logger.warn(`Invalid content-type: ${value}`);
|
||||||
throw new BadRequestHttpError(`Invalid content-type: ${value} does not match ( token "/" token )`);
|
throw new BadRequestHttpError(`Invalid content-type: ${value} does not match ( token "/" token )`);
|
||||||
}
|
}
|
||||||
@ -595,6 +474,8 @@ export function parseLinkHeader(link: string | string[] = []): LinkEntry[] {
|
|||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map used as a simple cache in the helper function matchesAuthorizationScheme.
|
||||||
|
const authSchemeRegexCache: Map<string, RegExp> = new Map();
|
||||||
/**
|
/**
|
||||||
* Checks if the value of an HTTP Authorization header matches a specific scheme (e.g. Basic, Bearer, etc).
|
* Checks if the value of an HTTP Authorization header matches a specific scheme (e.g. Basic, Bearer, etc).
|
||||||
*
|
*
|
||||||
|
@ -5,6 +5,7 @@ import type { TargetExtractor } from '../http/input/identifier/TargetExtractor';
|
|||||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||||
import type { HttpRequest } from '../server/HttpRequest';
|
import type { HttpRequest } from '../server/HttpRequest';
|
||||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||||
|
import { errorTermsToMetadata } from './errors/HttpErrorUtil';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes a potential Windows path into a POSIX path.
|
* Changes a potential Windows path into a POSIX path.
|
||||||
@ -235,7 +236,7 @@ Promise<string> {
|
|||||||
const target = await targetExtractor.handleSafe({ request });
|
const target = await targetExtractor.handleSafe({ request });
|
||||||
if (!target.path.startsWith(baseUrl)) {
|
if (!target.path.startsWith(baseUrl)) {
|
||||||
throw new BadRequestHttpError(`The identifier ${target.path} is outside the configured identifier space.`,
|
throw new BadRequestHttpError(`The identifier ${target.path} is outside the configured identifier space.`,
|
||||||
{ errorCode: 'E0001', details: { path: target.path }});
|
{ errorCode: 'E0001', metadata: errorTermsToMetadata({ path: target.path }) });
|
||||||
}
|
}
|
||||||
return target.path.slice(baseUrl.length - 1);
|
return target.path.slice(baseUrl.length - 1);
|
||||||
}
|
}
|
||||||
|
@ -253,10 +253,17 @@ export const SOLID_AS = createVocabulary('urn:npm:solid:community-server:activit
|
|||||||
|
|
||||||
export const SOLID_ERROR = createVocabulary('urn:npm:solid:community-server:error:',
|
export const SOLID_ERROR = createVocabulary('urn:npm:solid:community-server:error:',
|
||||||
'disallowedMethod',
|
'disallowedMethod',
|
||||||
|
'errorCode',
|
||||||
'errorResponse',
|
'errorResponse',
|
||||||
'stack',
|
'stack',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Used to pass parameters to error templates
|
||||||
|
export const SOLID_ERROR_TERM = createVocabulary('urn:npm:solid:community-server:error-term:',
|
||||||
|
// Identifier of the resource responsible for the error
|
||||||
|
'path',
|
||||||
|
);
|
||||||
|
|
||||||
export const SOLID_HTTP = createVocabulary('urn:npm:solid:community-server:http:',
|
export const SOLID_HTTP = createVocabulary('urn:npm:solid:community-server:http:',
|
||||||
'location',
|
'location',
|
||||||
'slug',
|
'slug',
|
||||||
|
@ -12,15 +12,8 @@ export function isError(error: any): error is Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the input is a native error.
|
* Creates a string representing the error message of this object.
|
||||||
* If not the input will be re-thrown.
|
|
||||||
*/
|
*/
|
||||||
export function assertError(error: unknown): asserts error is Error {
|
|
||||||
if (!isError(error)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createErrorMessage(error: unknown): string {
|
export function createErrorMessage(error: unknown): string {
|
||||||
return isError(error) ? error.message : `Unknown error: ${error}`;
|
return isError(error) ? error.message : `Unknown error: ${error}`;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { DataFactory } from 'n3';
|
import type { NamedNode } from 'rdf-js';
|
||||||
import type { NamedNode, Quad, Quad_Subject } from 'rdf-js';
|
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
import { toNamedTerm } from '../TermUtil';
|
import { toLiteral, toNamedTerm } from '../TermUtil';
|
||||||
import { SOLID_ERROR } from '../Vocabularies';
|
import { HTTP, SOLID_ERROR, XSD } from '../Vocabularies';
|
||||||
import { isError } from './ErrorUtil';
|
import { isError } from './ErrorUtil';
|
||||||
import quad = DataFactory.quad;
|
|
||||||
|
|
||||||
export interface HttpErrorOptions {
|
export interface HttpErrorOptions {
|
||||||
cause?: unknown;
|
cause?: unknown;
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
details?: NodeJS.Dict<unknown>;
|
metadata?: RepresentationMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +25,7 @@ export class HttpError<T extends number = number> extends Error implements HttpE
|
|||||||
public readonly statusCode: T;
|
public readonly statusCode: T;
|
||||||
public readonly cause?: unknown;
|
public readonly cause?: unknown;
|
||||||
public readonly errorCode: string;
|
public readonly errorCode: string;
|
||||||
public readonly details?: NodeJS.Dict<unknown>;
|
public readonly metadata: RepresentationMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new HTTP error. Subclasses should call this with their fixed status code.
|
* Creates a new HTTP error. Subclasses should call this with their fixed status code.
|
||||||
@ -41,23 +40,20 @@ export class HttpError<T extends number = number> extends Error implements HttpE
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
this.cause = options.cause;
|
this.cause = options.cause;
|
||||||
this.errorCode = options.errorCode ?? `H${statusCode}`;
|
this.errorCode = options.errorCode ?? `H${statusCode}`;
|
||||||
this.details = options.details;
|
this.metadata = options.metadata ?? new RepresentationMetadata();
|
||||||
|
this.generateMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isInstance(error: any): error is HttpError {
|
public static isInstance(error: any): error is HttpError {
|
||||||
return isError(error) && typeof (error as any).statusCode === 'number';
|
return isError(error) && typeof (error as any).statusCode === 'number' && (error as any).metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns quads representing metadata relevant to this error.
|
* Initializes the error metadata.
|
||||||
*/
|
*/
|
||||||
public generateMetadata(subject: Quad_Subject | string): Quad[] {
|
protected generateMetadata(): void {
|
||||||
// The reason we have this here instead of the generate function below
|
this.metadata.add(SOLID_ERROR.terms.errorResponse, generateHttpErrorUri(this.statusCode));
|
||||||
// is because we still want errors created with `new HttpError` to be treated identical
|
this.metadata.add(HTTP.terms.statusCodeNumber, toLiteral(this.statusCode, XSD.terms.integer));
|
||||||
// as errors created with the constructor of the error class corresponding to that specific status code.
|
|
||||||
return [
|
|
||||||
quad(toNamedTerm(subject), SOLID_ERROR.terms.errorResponse, generateHttpErrorUri(this.statusCode)),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,24 +1,55 @@
|
|||||||
|
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||||
|
import { toPredicateTerm } from '../TermUtil';
|
||||||
|
import { SOLID_ERROR_TERM } from '../Vocabularies';
|
||||||
import { BadRequestHttpError } from './BadRequestHttpError';
|
import { BadRequestHttpError } from './BadRequestHttpError';
|
||||||
import { createErrorMessage } from './ErrorUtil';
|
import { createErrorMessage } from './ErrorUtil';
|
||||||
import { HttpError } from './HttpError';
|
import { HttpError } from './HttpError';
|
||||||
import { InternalServerError } from './InternalServerError';
|
import { InternalServerError } from './InternalServerError';
|
||||||
|
import Dict = NodeJS.Dict;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the HTTP status code corresponding to the error.
|
* Adds the given terms to error metadata.
|
||||||
|
* The keys will be converted to predicates by prepending them with the `SOLID_ERROR_TERM` namespace.
|
||||||
|
* The values will become literals.
|
||||||
|
*
|
||||||
|
* @param terms - Terms to add to the metadata.
|
||||||
|
* @param metadata - Metadata to add the terms to. A new metadata object will be created if this is undefined.
|
||||||
*/
|
*/
|
||||||
export function getStatusCode(error: Error): number {
|
export function errorTermsToMetadata(terms: Dict<string>, metadata?: RepresentationMetadata): RepresentationMetadata {
|
||||||
return HttpError.isInstance(error) ? error.statusCode : 500;
|
metadata = metadata ?? new RepresentationMetadata();
|
||||||
|
for (const [ key, value ] of Object.entries(terms)) {
|
||||||
|
if (value) {
|
||||||
|
metadata.add(toPredicateTerm(`${SOLID_ERROR_TERM.namespace}${key}`), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combines a list of errors into a single HttpErrors.
|
* Extracts all the error metadata terms and converts them to a simple object.
|
||||||
|
* All predicates in the `SOLID_ERROR_TERM` namespace will be found.
|
||||||
|
* The namespace will be removed from the predicate and the remainder will be used as a key.
|
||||||
|
* The object literal values will be used as values in the resulting object.
|
||||||
|
*
|
||||||
|
* @param metadata - Metadata to extract the terms from.
|
||||||
|
*/
|
||||||
|
export function extractErrorTerms(metadata: RepresentationMetadata): Dict<string> {
|
||||||
|
return metadata.quads()
|
||||||
|
.filter((quad): boolean => quad.predicate.value.startsWith(SOLID_ERROR_TERM.namespace))
|
||||||
|
.reduce<NodeJS.Dict<string>>((acc, quad): Dict<string> => {
|
||||||
|
acc[quad.predicate.value.slice(SOLID_ERROR_TERM.namespace.length)] = quad.object.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a list of errors into a single HttpError.
|
||||||
* Status code depends on the input errors. If they all share the same status code that code will be re-used.
|
* Status code depends on the input errors. If they all share the same status code that code will be re-used.
|
||||||
* If they are all within the 4xx range, 400 will be used, otherwise 500.
|
* If they are all within the 4xx range, 400 will be used, otherwise 500.
|
||||||
*
|
*
|
||||||
* @param errors - Errors to combine.
|
* @param errors - Errors to combine.
|
||||||
*/
|
*/
|
||||||
export function createAggregateError(errors: Error[]):
|
export function createAggregateError(errors: Error[]): HttpError {
|
||||||
HttpError {
|
|
||||||
const httpErrors = errors.map((error): HttpError =>
|
const httpErrors = errors.map((error): HttpError =>
|
||||||
HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
|
HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
|
||||||
const messages = httpErrors.map((error: Error): string => error.message).filter((msg): boolean => msg.length > 0);
|
const messages = httpErrors.map((error: Error): string => error.message).filter((msg): boolean => msg.length > 0);
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
import { DataFactory } from 'n3';
|
|
||||||
import type { Quad, Quad_Subject } from 'rdf-js';
|
|
||||||
import { toNamedTerm, toObjectTerm } from '../TermUtil';
|
|
||||||
import { SOLID_ERROR } from '../Vocabularies';
|
import { SOLID_ERROR } from '../Vocabularies';
|
||||||
import type { HttpErrorOptions } from './HttpError';
|
import type { HttpErrorOptions } from './HttpError';
|
||||||
import { generateHttpErrorClass } from './HttpError';
|
import { generateHttpErrorClass } from './HttpError';
|
||||||
import quad = DataFactory.quad;
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
const BaseHttpError = generateHttpErrorClass(405, 'MethodNotAllowedHttpError');
|
const BaseHttpError = generateHttpErrorClass(405, 'MethodNotAllowedHttpError');
|
||||||
@ -18,15 +14,10 @@ export class MethodNotAllowedHttpError extends BaseHttpError {
|
|||||||
|
|
||||||
public constructor(methods: string[] = [], message?: string, options?: HttpErrorOptions) {
|
public constructor(methods: string[] = [], message?: string, options?: HttpErrorOptions) {
|
||||||
super(message ?? `${methods} are not allowed.`, options);
|
super(message ?? `${methods} are not allowed.`, options);
|
||||||
|
// Can not override `generateMetadata` as `this.methods` is not defined yet
|
||||||
|
for (const method of methods) {
|
||||||
|
this.metadata.add(SOLID_ERROR.terms.disallowedMethod, method);
|
||||||
|
}
|
||||||
this.methods = methods;
|
this.methods = methods;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { DataFactory } from 'n3';
|
||||||
|
import { SOLID_HTTP } from '../Vocabularies';
|
||||||
import type { HttpErrorClass, HttpErrorOptions } from './HttpError';
|
import type { HttpErrorClass, HttpErrorOptions } from './HttpError';
|
||||||
import { generateHttpErrorClass, HttpError } from './HttpError';
|
import { generateHttpErrorUri, HttpError } from './HttpError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An error corresponding to a 3xx status code.
|
* An error corresponding to a 3xx status code.
|
||||||
@ -11,6 +13,7 @@ export class RedirectHttpError<TCode extends number = number> extends HttpError<
|
|||||||
public constructor(statusCode: TCode, name: string, location: string, message?: string, options?: HttpErrorOptions) {
|
public constructor(statusCode: TCode, name: string, location: string, message?: string, options?: HttpErrorOptions) {
|
||||||
super(statusCode, name, message, options);
|
super(statusCode, name, message, options);
|
||||||
this.location = location;
|
this.location = location;
|
||||||
|
this.metadata.add(SOLID_HTTP.terms.location, DataFactory.namedNode(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isInstance(error: any): error is RedirectHttpError {
|
public static isInstance(error: any): error is RedirectHttpError {
|
||||||
@ -35,16 +38,12 @@ export function generateRedirectHttpErrorClass<TCode extends number>(
|
|||||||
code: TCode,
|
code: TCode,
|
||||||
name: string,
|
name: string,
|
||||||
): RedirectHttpErrorClass<TCode> {
|
): RedirectHttpErrorClass<TCode> {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
return class SpecificRedirectHttpError extends RedirectHttpError<TCode> {
|
||||||
const BaseClass = generateHttpErrorClass(code, name);
|
public static readonly statusCode = code;
|
||||||
|
public static readonly uri = generateHttpErrorUri(code);
|
||||||
// Need to extend `BaseClass` instead of `RedirectHttpError` to have the required static methods
|
|
||||||
return class SpecificRedirectHttpError extends BaseClass implements RedirectHttpError {
|
|
||||||
public readonly location: string;
|
|
||||||
|
|
||||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||||
super(message, options);
|
super(code, name, location, message, options);
|
||||||
this.location = location;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isInstance(error: any): error is SpecificRedirectHttpError {
|
public static isInstance(error: any): error is SpecificRedirectHttpError {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
|
import { errorTermsToMetadata } from '../errors/HttpErrorUtil';
|
||||||
import { InternalServerError } from '../errors/InternalServerError';
|
import { InternalServerError } from '../errors/InternalServerError';
|
||||||
import { ensureTrailingSlash, isContainerIdentifier } from '../PathUtil';
|
import { ensureTrailingSlash, isContainerIdentifier } from '../PathUtil';
|
||||||
import type { IdentifierStrategy } from './IdentifierStrategy';
|
import type { IdentifierStrategy } from './IdentifierStrategy';
|
||||||
@ -18,7 +19,7 @@ export abstract class BaseIdentifierStrategy implements IdentifierStrategy {
|
|||||||
public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier {
|
public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||||
if (!this.supportsIdentifier(identifier)) {
|
if (!this.supportsIdentifier(identifier)) {
|
||||||
throw new InternalServerError(`The identifier ${identifier.path} is outside the configured identifier space.`,
|
throw new InternalServerError(`The identifier ${identifier.path} is outside the configured identifier space.`,
|
||||||
{ errorCode: 'E0001', details: { path: identifier.path }});
|
{ errorCode: 'E0001', metadata: errorTermsToMetadata({ path: identifier.path }) });
|
||||||
}
|
}
|
||||||
if (this.isRootContainer(identifier)) {
|
if (this.isRootContainer(identifier)) {
|
||||||
throw new InternalServerError(`Cannot obtain the parent of ${identifier.path} because it is a root container.`);
|
throw new InternalServerError(`Cannot obtain the parent of ${identifier.path} because it is a root container.`);
|
||||||
|
@ -9,6 +9,7 @@ import { RepresentationMetadata } from '../../../../../src/http/representation/R
|
|||||||
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../../../src/server/HttpRequest';
|
||||||
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
||||||
|
import { ContentType } from '../../../../../src/util/Header';
|
||||||
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
|
import { guardedStreamFrom } from '../../../../../src/util/StreamUtil';
|
||||||
const { namedNode, quad } = DataFactory;
|
const { namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
@ -24,11 +25,10 @@ describe('A SparqlUpdateBodyParser', (): void => {
|
|||||||
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
input.metadata.contentType = 'text/plain';
|
input.metadata.contentType = 'text/plain';
|
||||||
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
await expect(bodyParser.canHandle(input)).rejects.toThrow(UnsupportedMediaTypeHttpError);
|
||||||
input.metadata.contentType = 'application/sparql-update;charset=utf-8';
|
const contentType = new ContentType('application/sparql-update');
|
||||||
|
input.metadata.contentTypeObject = contentType;
|
||||||
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
||||||
input.metadata.contentType = 'application/sparql-update ; foo=bar';
|
contentType.parameters = { charset: 'utf-8' };
|
||||||
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
|
||||||
input.metadata.contentType = 'application/sparql-update';
|
|
||||||
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
await expect(bodyParser.canHandle(input)).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import type {
|
|||||||
RepresentationConverter,
|
RepresentationConverter,
|
||||||
RepresentationConverterArgs,
|
RepresentationConverterArgs,
|
||||||
} from '../../../../../src/storage/conversion/RepresentationConverter';
|
} from '../../../../../src/storage/conversion/RepresentationConverter';
|
||||||
|
import type { HttpError } from '../../../../../src/util/errors/HttpError';
|
||||||
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../../../../src/util/errors/NotFoundHttpError';
|
||||||
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
|
import { HTTP, XSD } from '../../../../../src/util/Vocabularies';
|
||||||
import literal = DataFactory.literal;
|
import literal = DataFactory.literal;
|
||||||
@ -33,7 +34,7 @@ async function expectValidArgs(args: RepresentationConverterArgs, stack?: string
|
|||||||
|
|
||||||
describe('A ConvertingErrorHandler', (): void => {
|
describe('A ConvertingErrorHandler', (): void => {
|
||||||
// The error object can get modified by the handler
|
// The error object can get modified by the handler
|
||||||
let error: Error;
|
let error: HttpError;
|
||||||
let stack: string | undefined;
|
let stack: string | undefined;
|
||||||
const request = {} as HttpRequest;
|
const request = {} as HttpRequest;
|
||||||
let converter: jest.Mocked<RepresentationConverter>;
|
let converter: jest.Mocked<RepresentationConverter>;
|
||||||
|
@ -2,6 +2,7 @@ import { createResponse } from 'node-mocks-http';
|
|||||||
import { ContentTypeMetadataWriter } from '../../../../../src/http/output/metadata/ContentTypeMetadataWriter';
|
import { ContentTypeMetadataWriter } from '../../../../../src/http/output/metadata/ContentTypeMetadataWriter';
|
||||||
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../../src/http/representation/RepresentationMetadata';
|
||||||
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../../../src/server/HttpResponse';
|
||||||
|
import { ContentType } from '../../../../../src/util/Header';
|
||||||
|
|
||||||
describe('A ContentTypeMetadataWriter', (): void => {
|
describe('A ContentTypeMetadataWriter', (): void => {
|
||||||
const writer = new ContentTypeMetadataWriter();
|
const writer = new ContentTypeMetadataWriter();
|
||||||
@ -18,18 +19,12 @@ describe('A ContentTypeMetadataWriter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds a Content-Type header with parameters if present.', async(): Promise<void> => {
|
it('adds a Content-Type header with parameters if present.', async(): Promise<void> => {
|
||||||
const metadata = new RepresentationMetadata('text/plain; charset=utf-8');
|
const metadata = new RepresentationMetadata(new ContentType('text/plain', { charset: 'utf-8' }));
|
||||||
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
await expect(writer.handle({ response, metadata })).resolves.toBeUndefined();
|
||||||
|
|
||||||
expect(response.getHeaders()).toEqual({
|
expect(response.getHeaders()).toEqual({
|
||||||
'content-type': 'text/plain; charset=utf-8',
|
'content-type': 'text/plain; charset=utf-8',
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadata2 = new RepresentationMetadata('text/plain; charset="utf-8"');
|
|
||||||
await expect(writer.handle({ response, metadata: metadata2 })).resolves.toBeUndefined();
|
|
||||||
expect(response.getHeaders()).toEqual({
|
|
||||||
'content-type': 'text/plain; charset=utf-8',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds a Content-Type header without parameters.', async(): Promise<void> => {
|
it('adds a Content-Type header without parameters.', async(): Promise<void> => {
|
||||||
|
@ -8,6 +8,7 @@ describe('A RedirectResponseDescription', (): void => {
|
|||||||
it('has status the code and location of the error.', async(): Promise<void> => {
|
it('has status the code and location of the error.', async(): Promise<void> => {
|
||||||
const description = new RedirectResponseDescription(error);
|
const description = new RedirectResponseDescription(error);
|
||||||
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(error.location);
|
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(error.location);
|
||||||
|
expect(description.metadata).toBe(error.metadata);
|
||||||
expect(description.statusCode).toBe(error.statusCode);
|
expect(description.statusCode).toBe(error.statusCode);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,8 +2,8 @@ import 'jest-rdf';
|
|||||||
import type { BlankNode } from 'n3';
|
import type { BlankNode } from 'n3';
|
||||||
import { DataFactory } from 'n3';
|
import { DataFactory } from 'n3';
|
||||||
import type { NamedNode, Quad } from 'rdf-js';
|
import type { NamedNode, Quad } from 'rdf-js';
|
||||||
import { ContentType } from '../../../../src';
|
|
||||||
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
|
import { ContentType } from '../../../../src/util/Header';
|
||||||
import { CONTENT_TYPE_TERM, SOLID_META, RDFS } from '../../../../src/util/Vocabularies';
|
import { CONTENT_TYPE_TERM, SOLID_META, RDFS } from '../../../../src/util/Vocabularies';
|
||||||
const { defaultGraph, literal, namedNode, quad } = DataFactory;
|
const { defaultGraph, literal, namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
@ -308,14 +308,14 @@ describe('A RepresentationMetadata', (): void => {
|
|||||||
it('has a shorthand for Content-Type as string.', async(): Promise<void> => {
|
it('has a shorthand for Content-Type as string.', async(): Promise<void> => {
|
||||||
expect(metadata.contentType).toBeUndefined();
|
expect(metadata.contentType).toBeUndefined();
|
||||||
expect(metadata.contentTypeObject).toBeUndefined();
|
expect(metadata.contentTypeObject).toBeUndefined();
|
||||||
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
|
metadata.contentType = 'text/plain';
|
||||||
expect(metadata.contentTypeObject).toEqual({
|
expect(metadata.contentTypeObject).toEqual({ value: 'text/plain', parameters: {}});
|
||||||
value: 'text/plain',
|
|
||||||
parameters: {
|
|
||||||
charset: 'utf-8',
|
|
||||||
test: 'value1',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors trying to set a Content-Type with parameters using a string.', async(): Promise<void> => {
|
||||||
|
expect((): void => {
|
||||||
|
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
|
||||||
|
}).toThrow(Error);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a shorthand for Content-Type as object.', async(): Promise<void> => {
|
it('has a shorthand for Content-Type as object.', async(): Promise<void> => {
|
||||||
@ -341,7 +341,10 @@ describe('A RepresentationMetadata', (): void => {
|
|||||||
it('can properly clear the Content-Type parameters explicitly.', async(): Promise<void> => {
|
it('can properly clear the Content-Type parameters explicitly.', async(): Promise<void> => {
|
||||||
expect(metadata.contentType).toBeUndefined();
|
expect(metadata.contentType).toBeUndefined();
|
||||||
expect(metadata.contentTypeObject).toBeUndefined();
|
expect(metadata.contentTypeObject).toBeUndefined();
|
||||||
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
|
metadata.contentTypeObject = new ContentType('text/plain', {
|
||||||
|
charset: 'utf-8',
|
||||||
|
test: 'value1',
|
||||||
|
});
|
||||||
metadata.contentType = undefined;
|
metadata.contentType = undefined;
|
||||||
expect(metadata.contentType).toBeUndefined();
|
expect(metadata.contentType).toBeUndefined();
|
||||||
expect(metadata.contentTypeObject).toBeUndefined();
|
expect(metadata.contentTypeObject).toBeUndefined();
|
||||||
@ -353,7 +356,10 @@ describe('A RepresentationMetadata', (): void => {
|
|||||||
it('can properly clear the Content-Type parameters implicitly.', async(): Promise<void> => {
|
it('can properly clear the Content-Type parameters implicitly.', async(): Promise<void> => {
|
||||||
expect(metadata.contentType).toBeUndefined();
|
expect(metadata.contentType).toBeUndefined();
|
||||||
expect(metadata.contentTypeObject).toBeUndefined();
|
expect(metadata.contentTypeObject).toBeUndefined();
|
||||||
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
|
metadata.contentTypeObject = new ContentType('text/plain', {
|
||||||
|
charset: 'utf-8',
|
||||||
|
test: 'value1',
|
||||||
|
});
|
||||||
metadata.contentType = 'text/turtle';
|
metadata.contentType = 'text/turtle';
|
||||||
expect(metadata.contentType).toBe('text/turtle');
|
expect(metadata.contentType).toBe('text/turtle');
|
||||||
expect(metadata.contentTypeObject).toEqual({
|
expect(metadata.contentTypeObject).toEqual({
|
||||||
@ -368,7 +374,10 @@ describe('A RepresentationMetadata', (): void => {
|
|||||||
it('can return invalid parameters when too many quads are present.', async(): Promise<void> => {
|
it('can return invalid parameters when too many quads are present.', async(): Promise<void> => {
|
||||||
expect(metadata.contentType).toBeUndefined();
|
expect(metadata.contentType).toBeUndefined();
|
||||||
expect(metadata.contentTypeObject).toBeUndefined();
|
expect(metadata.contentTypeObject).toBeUndefined();
|
||||||
metadata.contentType = 'text/plain; charset=utf-8; test=value1';
|
metadata.contentTypeObject = new ContentType('text/plain', {
|
||||||
|
charset: 'utf-8',
|
||||||
|
test: 'value1',
|
||||||
|
});
|
||||||
const param = metadata.quads(null, SOLID_META.terms.value)[0].subject;
|
const param = metadata.quads(null, SOLID_META.terms.value)[0].subject;
|
||||||
metadata.addQuad(param as BlankNode, SOLID_META.terms.value, 'anomaly');
|
metadata.addQuad(param as BlankNode, SOLID_META.terms.value, 'anomaly');
|
||||||
expect(metadata.contentTypeObject?.parameters).toMatchObject({ invalid: '' });
|
expect(metadata.contentTypeObject?.parameters).toMatchObject({ invalid: '' });
|
||||||
|
@ -14,6 +14,7 @@ import type { Interaction, InteractionHandler } from '../../../../src/identity/i
|
|||||||
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
|
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
|
||||||
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
|
||||||
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
||||||
|
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
@ -269,12 +270,12 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
name: 'BadRequestHttpError',
|
name: 'BadRequestHttpError',
|
||||||
message: 'Unknown client, you might need to clear the local storage on the client.',
|
message: 'Unknown client, you might need to clear the local storage on the client.',
|
||||||
errorCode: 'E0003',
|
errorCode: 'E0003',
|
||||||
details: {
|
|
||||||
client_id: 'CLIENT_ID',
|
|
||||||
redirect_uri: 'REDIRECT_URI',
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
request: ctx.req });
|
request: ctx.req });
|
||||||
|
expect(extractErrorTerms(errorHandler.handleSafe.mock.calls[0][0].error.metadata)).toEqual({
|
||||||
|
client_id: 'CLIENT_ID',
|
||||||
|
redirect_uri: 'REDIRECT_URI',
|
||||||
|
});
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
||||||
});
|
});
|
||||||
|
@ -5,6 +5,8 @@ import rdfDereferencer from 'rdf-dereference';
|
|||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
|
import { TokenOwnershipValidator } from '../../../../src/identity/ownership/TokenOwnershipValidator';
|
||||||
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
|
import type { ExpiringStorage } from '../../../../src/storage/keyvalue/ExpiringStorage';
|
||||||
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
import { SOLID } from '../../../../src/util/Vocabularies';
|
import { SOLID } from '../../../../src/util/Vocabularies';
|
||||||
const { literal, namedNode, quad } = DataFactory;
|
const { literal, namedNode, quad } = DataFactory;
|
||||||
|
|
||||||
@ -57,10 +59,16 @@ describe('A TokenOwnershipValidator', (): void => {
|
|||||||
it('errors if no token is stored in the storage.', async(): Promise<void> => {
|
it('errors if no token is stored in the storage.', async(): Promise<void> => {
|
||||||
// Even if the token is in the WebId, it will error since it's not in the storage
|
// Even if the token is in the WebId, it will error since it's not in the storage
|
||||||
mockDereference(tokenTriple);
|
mockDereference(tokenTriple);
|
||||||
await expect(validator.handle({ webId })).rejects.toThrow(expect.objectContaining({
|
let error: unknown;
|
||||||
message: expect.stringContaining(tokenString),
|
try {
|
||||||
details: { quad: tokenString },
|
await validator.handle({ webId });
|
||||||
}));
|
} catch (err: unknown) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
expect(error).toEqual(expect.objectContaining({ message: expect.stringContaining(tokenString) }));
|
||||||
|
expect(BadRequestHttpError.isInstance(error)).toBe(true);
|
||||||
|
expect(extractErrorTerms((error as BadRequestHttpError).metadata))
|
||||||
|
.toEqual({ quad: tokenString });
|
||||||
expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(0);
|
expect(rdfDereferenceMock.dereference).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4,12 +4,11 @@ import type { ErrorHandler } from '../../../src/http/output/error/ErrorHandler';
|
|||||||
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
|
import { ResponseDescription } from '../../../src/http/output/response/ResponseDescription';
|
||||||
import type { ResponseWriter } from '../../../src/http/output/ResponseWriter';
|
import type { ResponseWriter } from '../../../src/http/output/ResponseWriter';
|
||||||
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../src/http/representation/BasicRepresentation';
|
||||||
import { RepresentationMetadata } from '../../../src/http/representation/RepresentationMetadata';
|
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
import type { OperationHttpHandler } from '../../../src/server/OperationHttpHandler';
|
||||||
import { ParsingHttpHandler } from '../../../src/server/ParsingHttpHandler';
|
import { ParsingHttpHandler } from '../../../src/server/ParsingHttpHandler';
|
||||||
import { HttpError } from '../../../src/util/errors/HttpError';
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
|
|
||||||
describe('A ParsingHttpHandler', (): void => {
|
describe('A ParsingHttpHandler', (): void => {
|
||||||
const request: HttpRequest = {} as any;
|
const request: HttpRequest = {} as any;
|
||||||
@ -57,7 +56,7 @@ describe('A ParsingHttpHandler', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
|
it('calls the error handler if something goes wrong.', async(): Promise<void> => {
|
||||||
const error = new Error('bad data');
|
const error = new BadRequestHttpError('bad data');
|
||||||
source.handleSafe.mockRejectedValueOnce(error);
|
source.handleSafe.mockRejectedValueOnce(error);
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
@ -66,16 +65,14 @@ describe('A ParsingHttpHandler', (): void => {
|
|||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds error metadata if able.', async(): Promise<void> => {
|
it('creates an InternalServerError if th error was not an HttpError.', async(): Promise<void> => {
|
||||||
const error = new HttpError(0, 'error');
|
const error = new Error('bad data');
|
||||||
source.handleSafe.mockRejectedValueOnce(error);
|
source.handleSafe.mockRejectedValueOnce(error);
|
||||||
const metaResponse = new ResponseDescription(0, new RepresentationMetadata());
|
|
||||||
errorHandler.handleSafe.mockResolvedValueOnce(metaResponse);
|
|
||||||
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
await expect(handler.handle({ request, response })).resolves.toBeUndefined();
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith({ error, request });
|
expect(errorHandler.handleSafe).toHaveBeenLastCalledWith(expect.objectContaining({ request }));
|
||||||
|
expect(errorHandler.handleSafe.mock.calls[0][0].error.cause).toBe(error);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: metaResponse });
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response, result: errorResponse });
|
||||||
expect(metaResponse.metadata?.quads()).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,6 +20,7 @@ import { NotFoundHttpError } from '../../../src/util/errors/NotFoundHttpError';
|
|||||||
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../../src/util/errors/NotImplementedHttpError';
|
||||||
import { PreconditionFailedHttpError } from '../../../src/util/errors/PreconditionFailedHttpError';
|
import { PreconditionFailedHttpError } from '../../../src/util/errors/PreconditionFailedHttpError';
|
||||||
import type { Guarded } from '../../../src/util/GuardedStream';
|
import type { Guarded } from '../../../src/util/GuardedStream';
|
||||||
|
import { ContentType } from '../../../src/util/Header';
|
||||||
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
|
import { SingleRootIdentifierStrategy } from '../../../src/util/identifiers/SingleRootIdentifierStrategy';
|
||||||
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
import { trimTrailingSlashes } from '../../../src/util/PathUtil';
|
||||||
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
import { guardedStreamFrom } from '../../../src/util/StreamUtil';
|
||||||
@ -673,7 +674,7 @@ describe('A DataAccessorBasedStore', (): void => {
|
|||||||
representation.metadata.add(
|
representation.metadata.add(
|
||||||
SOLID_META.terms.preserve, namedNode(metaResourceID.path), SOLID_META.terms.ResponseMetadata,
|
SOLID_META.terms.preserve, namedNode(metaResourceID.path), SOLID_META.terms.ResponseMetadata,
|
||||||
);
|
);
|
||||||
representation.metadata.contentType = 'text/plain; charset=UTF-8';
|
representation.metadata.contentTypeObject = new ContentType('text/plain', { charset: 'UTF-8' });
|
||||||
await store.setRepresentation(resourceID, representation);
|
await store.setRepresentation(resourceID, representation);
|
||||||
const { metadata } = accessor.data[resourceID.path];
|
const { metadata } = accessor.data[resourceID.path];
|
||||||
expect(metadata.quads(null, RDF.terms.type)).toHaveLength(2);
|
expect(metadata.quads(null, RDF.terms.type)).toHaveLength(2);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import { ErrorToJsonConverter } from '../../../../src/storage/conversion/ErrorToJsonConverter';
|
import { ErrorToJsonConverter } from '../../../../src/storage/conversion/ErrorToJsonConverter';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import { errorTermsToMetadata } from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
import type { OAuthErrorFields } from '../../../../src/util/errors/OAuthHttpError';
|
import type { OAuthErrorFields } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
import { readJsonStream } from '../../../../src/util/StreamUtil';
|
import { readJsonStream } from '../../../../src/util/StreamUtil';
|
||||||
@ -28,11 +29,13 @@ describe('An ErrorToJsonConverter', (): void => {
|
|||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'H400',
|
errorCode: 'H400',
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
|
details: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('copies the HttpError details.', async(): Promise<void> => {
|
it('copies the HttpError details.', async(): Promise<void> => {
|
||||||
const error = new BadRequestHttpError('error text', { details: { important: 'detail' }});
|
const metadata = errorTermsToMetadata({ important: 'detail' });
|
||||||
|
const error = new BadRequestHttpError('error text', { metadata });
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
await expect(prom).resolves.toBeDefined();
|
await expect(prom).resolves.toBeDefined();
|
||||||
@ -75,11 +78,13 @@ describe('An ErrorToJsonConverter', (): void => {
|
|||||||
error_description: 'error_description',
|
error_description: 'error_description',
|
||||||
scope: 'scope',
|
scope: 'scope',
|
||||||
state: 'state',
|
state: 'state',
|
||||||
|
details: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not copy the details if they are not serializable.', async(): Promise<void> => {
|
it('only adds stack if it is defined.', async(): Promise<void> => {
|
||||||
const error = new BadRequestHttpError('error text', { details: { object: BigInt(1) }});
|
const error = new BadRequestHttpError('error text');
|
||||||
|
delete error.stack;
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
await expect(prom).resolves.toBeDefined();
|
await expect(prom).resolves.toBeDefined();
|
||||||
@ -91,39 +96,7 @@ describe('An ErrorToJsonConverter', (): void => {
|
|||||||
message: 'error text',
|
message: 'error text',
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'H400',
|
errorCode: 'H400',
|
||||||
stack: error.stack,
|
details: {},
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defaults to status code 500 for non-HTTP errors.', async(): Promise<void> => {
|
|
||||||
const error = new Error('error text');
|
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
|
||||||
await expect(prom).resolves.toBeDefined();
|
|
||||||
const result = await prom;
|
|
||||||
expect(result.binary).toBe(true);
|
|
||||||
expect(result.metadata.contentType).toBe('application/json');
|
|
||||||
await expect(readJsonStream(result.data)).resolves.toEqual({
|
|
||||||
name: 'Error',
|
|
||||||
message: 'error text',
|
|
||||||
statusCode: 500,
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('only adds stack if it is defined.', async(): Promise<void> => {
|
|
||||||
const error = new Error('error text');
|
|
||||||
delete error.stack;
|
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
|
||||||
await expect(prom).resolves.toBeDefined();
|
|
||||||
const result = await prom;
|
|
||||||
expect(result.binary).toBe(true);
|
|
||||||
expect(result.metadata.contentType).toBe('application/json');
|
|
||||||
await expect(readJsonStream(result.data)).resolves.toEqual({
|
|
||||||
name: 'Error',
|
|
||||||
message: 'error text',
|
|
||||||
statusCode: 500,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import { ErrorToTemplateConverter } from '../../../../src/storage/conversion/ErrorToTemplateConverter';
|
import { ErrorToTemplateConverter } from '../../../../src/storage/conversion/ErrorToTemplateConverter';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import { errorTermsToMetadata } from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
import { resolveModulePath } from '../../../../src/util/PathUtil';
|
import { resolveModulePath } from '../../../../src/util/PathUtil';
|
||||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||||
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
|
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
|
||||||
@ -91,7 +92,8 @@ describe('An ErrorToTemplateConverter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds additional information if an error code description is found.', async(): Promise<void> => {
|
it('adds additional information if an error code description is found.', async(): Promise<void> => {
|
||||||
const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }});
|
const metadata = errorTermsToMetadata({ key: 'val' });
|
||||||
|
const error = new BadRequestHttpError('error text', { errorCode, metadata });
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
await expect(prom).resolves.toBeDefined();
|
await expect(prom).resolves.toBeDefined();
|
||||||
@ -154,8 +156,9 @@ describe('An ErrorToTemplateConverter', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has default template options.', async(): Promise<void> => {
|
it('has default template options.', async(): Promise<void> => {
|
||||||
|
const metadata = errorTermsToMetadata({ key: 'val' });
|
||||||
converter = new ErrorToTemplateConverter(templateEngine);
|
converter = new ErrorToTemplateConverter(templateEngine);
|
||||||
const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }});
|
const error = new BadRequestHttpError('error text', { errorCode, metadata });
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
const prom = converter.handle({ identifier, representation, preferences });
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
await expect(prom).resolves.toBeDefined();
|
await expect(prom).resolves.toBeDefined();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
import { ContentType,
|
import { ContentType } from '../../../src/util/Header';
|
||||||
addHeader,
|
import { addHeader,
|
||||||
hasScheme,
|
hasScheme,
|
||||||
matchesAuthorizationScheme,
|
matchesAuthorizationScheme,
|
||||||
parseAccept,
|
parseAccept,
|
||||||
|
@ -2,6 +2,8 @@ import { promises as fsPromises } from 'fs';
|
|||||||
import type { TargetExtractor } from '../../../src/http/input/identifier/TargetExtractor';
|
import type { TargetExtractor } from '../../../src/http/input/identifier/TargetExtractor';
|
||||||
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../src/http/representation/ResourceIdentifier';
|
||||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||||
|
import { BadRequestHttpError } from '../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import { extractErrorTerms } from '../../../src/util/errors/HttpErrorUtil';
|
||||||
import {
|
import {
|
||||||
absoluteFilePath,
|
absoluteFilePath,
|
||||||
createSubdomainRegexp,
|
createSubdomainRegexp,
|
||||||
@ -218,8 +220,15 @@ describe('PathUtil', (): void => {
|
|||||||
|
|
||||||
it('errors if the target is outside of the server scope.', async(): Promise<void> => {
|
it('errors if the target is outside of the server scope.', async(): Promise<void> => {
|
||||||
targetExtractor.handleSafe.mockResolvedValueOnce({ path: 'http://somewhere.else/resource' });
|
targetExtractor.handleSafe.mockResolvedValueOnce({ path: 'http://somewhere.else/resource' });
|
||||||
await expect(getRelativeUrl(baseUrl, request, targetExtractor)).rejects
|
let error: unknown;
|
||||||
.toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: 'http://somewhere.else/resource' }}));
|
try {
|
||||||
|
await getRelativeUrl(baseUrl, request, targetExtractor);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
expect(error).toEqual(expect.objectContaining({ errorCode: 'E0001' }));
|
||||||
|
expect(BadRequestHttpError.isInstance(error)).toBe(true);
|
||||||
|
expect(extractErrorTerms((error as BadRequestHttpError).metadata)).toEqual({ path: 'http://somewhere.else/resource' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { assertError, createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil';
|
import { createErrorMessage, isError } from '../../../../src/util/errors/ErrorUtil';
|
||||||
|
|
||||||
describe('ErrorUtil', (): void => {
|
describe('ErrorUtil', (): void => {
|
||||||
describe('#isError', (): void => {
|
describe('#isError', (): void => {
|
||||||
@ -19,16 +19,6 @@ describe('ErrorUtil', (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#assertError', (): void => {
|
|
||||||
it('returns undefined on native errors.', async(): Promise<void> => {
|
|
||||||
expect(assertError(new Error('error'))).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws on other values.', async(): Promise<void> => {
|
|
||||||
expect((): void => assertError('apple')).toThrow('apple');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#createErrorMessage', (): void => {
|
describe('#createErrorMessage', (): void => {
|
||||||
it('returns the given message for normal Errors.', async(): Promise<void> => {
|
it('returns the given message for normal Errors.', async(): Promise<void> => {
|
||||||
expect(createErrorMessage(new Error('error msg'))).toBe('error msg');
|
expect(createErrorMessage(new Error('error msg'))).toBe('error msg');
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'jest-rdf';
|
import 'jest-rdf';
|
||||||
import { DataFactory } from 'n3';
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError';
|
import { ConflictHttpError } from '../../../../src/util/errors/ConflictHttpError';
|
||||||
import { ForbiddenHttpError } from '../../../../src/util/errors/ForbiddenHttpError';
|
import { ForbiddenHttpError } from '../../../../src/util/errors/ForbiddenHttpError';
|
||||||
@ -15,9 +15,7 @@ import { PreconditionFailedHttpError } from '../../../../src/util/errors/Precond
|
|||||||
import { UnauthorizedHttpError } from '../../../../src/util/errors/UnauthorizedHttpError';
|
import { UnauthorizedHttpError } from '../../../../src/util/errors/UnauthorizedHttpError';
|
||||||
import { UnprocessableEntityHttpError } from '../../../../src/util/errors/UnprocessableEntityHttpError';
|
import { UnprocessableEntityHttpError } from '../../../../src/util/errors/UnprocessableEntityHttpError';
|
||||||
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
import { UnsupportedMediaTypeHttpError } from '../../../../src/util/errors/UnsupportedMediaTypeHttpError';
|
||||||
import { SOLID_ERROR } from '../../../../src/util/Vocabularies';
|
import { HTTP, SOLID_ERROR } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
const { literal, namedNode, quad } = DataFactory;
|
|
||||||
|
|
||||||
describe('HttpError', (): void => {
|
describe('HttpError', (): void => {
|
||||||
const errors: [string, number, HttpErrorClass][] = [
|
const errors: [string, number, HttpErrorClass][] = [
|
||||||
@ -39,7 +37,7 @@ describe('HttpError', (): void => {
|
|||||||
const options = {
|
const options = {
|
||||||
cause: new Error('cause'),
|
cause: new Error('cause'),
|
||||||
errorCode: 'E1234',
|
errorCode: 'E1234',
|
||||||
details: {},
|
metadata: new RepresentationMetadata(),
|
||||||
};
|
};
|
||||||
const instance = new constructor('my message', options);
|
const instance = new constructor('my message', options);
|
||||||
|
|
||||||
@ -75,15 +73,11 @@ describe('HttpError', (): void => {
|
|||||||
expect(new constructor().errorCode).toBe(`H${statusCode}`);
|
expect(new constructor().errorCode).toBe(`H${statusCode}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the details.', (): void => {
|
it('sets the metadata.', (): void => {
|
||||||
expect(instance.details).toBe(options.details);
|
expect(instance.metadata).toBe(options.metadata);
|
||||||
});
|
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
|
||||||
|
.toBe(`${SOLID_ERROR.namespace}H${statusCode}`);
|
||||||
it('generates metadata.', (): void => {
|
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe(`${statusCode}`);
|
||||||
const subject = namedNode('subject');
|
|
||||||
expect(instance.generateMetadata(subject)).toBeRdfIsomorphic([
|
|
||||||
quad(subject, SOLID_ERROR.terms.errorResponse, constructor.uri),
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -92,7 +86,6 @@ describe('HttpError', (): void => {
|
|||||||
const options = {
|
const options = {
|
||||||
cause: new Error('cause'),
|
cause: new Error('cause'),
|
||||||
errorCode: 'E1234',
|
errorCode: 'E1234',
|
||||||
details: { some: 'detail' },
|
|
||||||
};
|
};
|
||||||
const instance = new MethodNotAllowedHttpError([ 'GET' ], 'my message', options);
|
const instance = new MethodNotAllowedHttpError([ 'GET' ], 'my message', options);
|
||||||
|
|
||||||
@ -107,11 +100,10 @@ describe('HttpError', (): void => {
|
|||||||
expect(instance.errorCode).toBe(options.errorCode);
|
expect(instance.errorCode).toBe(options.errorCode);
|
||||||
expect(new MethodNotAllowedHttpError([ 'GET' ]).errorCode).toBe(`H${405}`);
|
expect(new MethodNotAllowedHttpError([ 'GET' ]).errorCode).toBe(`H${405}`);
|
||||||
|
|
||||||
const subject = namedNode('subject');
|
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
|
||||||
expect(instance.generateMetadata(subject)).toBeRdfIsomorphic([
|
.toBe(`${SOLID_ERROR.namespace}H405`);
|
||||||
quad(subject, SOLID_ERROR.terms.errorResponse, MethodNotAllowedHttpError.uri),
|
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe('405');
|
||||||
quad(subject, SOLID_ERROR.terms.disallowedMethod, literal('GET')),
|
expect(instance.metadata.get(SOLID_ERROR.terms.disallowedMethod)?.value).toBe('GET');
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,53 @@
|
|||||||
|
import { RepresentationMetadata } from '../../../../src/http/representation/RepresentationMetadata';
|
||||||
import { HttpError } from '../../../../src/util/errors/HttpError';
|
import { HttpError } from '../../../../src/util/errors/HttpError';
|
||||||
import { createAggregateError, getStatusCode } from '../../../../src/util/errors/HttpErrorUtil';
|
import {
|
||||||
import { NotFoundHttpError } from '../../../../src/util/errors/NotFoundHttpError';
|
createAggregateError,
|
||||||
|
errorTermsToMetadata,
|
||||||
|
extractErrorTerms,
|
||||||
|
} from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
|
import { toPredicateTerm } from '../../../../src/util/TermUtil';
|
||||||
|
|
||||||
|
describe('HttpErrorUtil', (): void => {
|
||||||
|
describe('#errorTermsToMetadata', (): void => {
|
||||||
|
it('creates a metadata object with the necessary triples.', async(): Promise<void> => {
|
||||||
|
const metadata = errorTermsToMetadata({
|
||||||
|
test: 'apple',
|
||||||
|
test2: 'pear',
|
||||||
|
not: undefined,
|
||||||
|
});
|
||||||
|
expect(metadata.quads()).toHaveLength(2);
|
||||||
|
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test'))?.value).toBe('apple');
|
||||||
|
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test2'))?.value).toBe('pear');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add the necessary triples to existing metadata.', async(): Promise<void> => {
|
||||||
|
const metadata = new RepresentationMetadata();
|
||||||
|
const response = errorTermsToMetadata({
|
||||||
|
test: 'apple',
|
||||||
|
test2: 'pear',
|
||||||
|
not: undefined,
|
||||||
|
}, metadata);
|
||||||
|
expect(response).toBe(metadata);
|
||||||
|
expect(metadata.quads()).toHaveLength(2);
|
||||||
|
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test'))?.value).toBe('apple');
|
||||||
|
expect(metadata.get(toPredicateTerm('urn:npm:solid:community-server:error-term:test2'))?.value).toBe('pear');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#extractErrorTerms', (): void => {
|
||||||
|
it('returns an object describing the terms.', async(): Promise<void> => {
|
||||||
|
const metadata = new RepresentationMetadata({
|
||||||
|
'urn:npm:solid:community-server:error-term:test': 'apple',
|
||||||
|
'urn:npm:solid:community-server:error-term:test2': 'pear',
|
||||||
|
'urn:npm:solid:community-server:other:test3': 'mango',
|
||||||
|
});
|
||||||
|
expect(extractErrorTerms(metadata)).toEqual({
|
||||||
|
test: 'apple',
|
||||||
|
test2: 'pear',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('ErrorUtil', (): void => {
|
|
||||||
describe('#createAggregateError', (): void => {
|
describe('#createAggregateError', (): void => {
|
||||||
const error401 = new HttpError(401, 'UnauthorizedHttpError');
|
const error401 = new HttpError(401, 'UnauthorizedHttpError');
|
||||||
const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');
|
const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');
|
||||||
@ -50,14 +95,4 @@ describe('ErrorUtil', (): void => {
|
|||||||
.toBe('Multiple handler errors: noStatusCode, noStatusCode');
|
.toBe('Multiple handler errors: noStatusCode, noStatusCode');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#getStatusCode', (): void => {
|
|
||||||
it('returns the corresponding status code for HttpErrors.', async(): Promise<void> => {
|
|
||||||
expect(getStatusCode(new NotFoundHttpError())).toBe(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 500 for other errors.', async(): Promise<void> => {
|
|
||||||
expect(getStatusCode(new Error('404'))).toBe(500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,7 @@ import { RedirectHttpError } from '../../../../src/util/errors/RedirectHttpError
|
|||||||
import type { RedirectHttpErrorClass } from '../../../../src/util/errors/RedirectHttpError';
|
import type { RedirectHttpErrorClass } from '../../../../src/util/errors/RedirectHttpError';
|
||||||
import { SeeOtherHttpError } from '../../../../src/util/errors/SeeOtherHttpError';
|
import { SeeOtherHttpError } from '../../../../src/util/errors/SeeOtherHttpError';
|
||||||
import { TemporaryRedirectHttpError } from '../../../../src/util/errors/TemporaryRedirectHttpError';
|
import { TemporaryRedirectHttpError } from '../../../../src/util/errors/TemporaryRedirectHttpError';
|
||||||
|
import { HTTP, SOLID_ERROR, SOLID_HTTP } from '../../../../src/util/Vocabularies';
|
||||||
|
|
||||||
// Used to make sure the RedirectHttpError constructor also gets called in a test.
|
// Used to make sure the RedirectHttpError constructor also gets called in a test.
|
||||||
class FixedRedirectHttpError extends RedirectHttpError {
|
class FixedRedirectHttpError extends RedirectHttpError {
|
||||||
@ -70,7 +71,10 @@ describe('RedirectHttpError', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets the details.', (): void => {
|
it('sets the details.', (): void => {
|
||||||
expect(instance.details).toBe(options.details);
|
expect(instance.metadata.get(SOLID_ERROR.terms.errorResponse)?.value)
|
||||||
|
.toBe(`${SOLID_ERROR.namespace}H${statusCode}`);
|
||||||
|
expect(instance.metadata.get(HTTP.terms.statusCodeNumber)?.value).toBe(`${statusCode}`);
|
||||||
|
expect(instance.metadata.get(SOLID_HTTP.terms.location)?.value).toBe(location);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
|
||||||
|
import { extractErrorTerms } from '../../../../src/util/errors/HttpErrorUtil';
|
||||||
|
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||||
import { BaseIdentifierStrategy } from '../../../../src/util/identifiers/BaseIdentifierStrategy';
|
import { BaseIdentifierStrategy } from '../../../../src/util/identifiers/BaseIdentifierStrategy';
|
||||||
|
|
||||||
class DummyStrategy extends BaseIdentifierStrategy {
|
class DummyStrategy extends BaseIdentifierStrategy {
|
||||||
@ -21,10 +23,16 @@ describe('A BaseIdentifierStrategy', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise<void> => {
|
it('errors when attempting to get the parent of an unsupported identifier.', async(): Promise<void> => {
|
||||||
expect((): any => strategy.getParentContainer({ path: '/unsupported' }))
|
let error: unknown;
|
||||||
.toThrow('The identifier /unsupported is outside the configured identifier space.');
|
try {
|
||||||
expect((): any => strategy.getParentContainer({ path: '/unsupported' }))
|
strategy.getParentContainer({ path: '/unsupported' });
|
||||||
.toThrow(expect.objectContaining({ errorCode: 'E0001', details: { path: '/unsupported' }}));
|
} catch (err: unknown) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
expect(error).toEqual(expect.objectContaining({ errorCode: 'E0001',
|
||||||
|
message: 'The identifier /unsupported is outside the configured identifier space.' }));
|
||||||
|
expect(InternalServerError.isInstance(error)).toBe(true);
|
||||||
|
expect(extractErrorTerms((error as InternalServerError).metadata)).toEqual({ path: '/unsupported' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors when attempting to get the parent of a root container.', async(): Promise<void> => {
|
it('errors when attempting to get the parent of a root container.', async(): Promise<void> => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user