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:
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 type { HttpResponse } from '../server/HttpResponse';
|
||||
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');
|
||||
// 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
|
||||
//
|
||||
@@ -16,124 +24,6 @@ const authSchemeRegexCache: Map<string, RegExp> = new Map();
|
||||
// Accept-Language = 1#( language-range [ weight ] )
|
||||
//
|
||||
// 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
|
||||
/**
|
||||
@@ -150,7 +40,7 @@ export function transformQuotedStrings(input: string): { result: string; replace
|
||||
const replacements: Record<string, string> = {};
|
||||
const result = input.replace(/"(?:[^"\\]|\\.)*"/gu, (match): string => {
|
||||
// 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}`);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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
|
||||
// parameter = token "=" ( token / quoted-string )
|
||||
// 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} ` +
|
||||
`does not match (token ( "=" ( token / quoted-string ))?). `, strict);
|
||||
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());
|
||||
|
||||
// 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(
|
||||
`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') {
|
||||
// Extension parameters appear after the q value
|
||||
map = extensionParams;
|
||||
if (!isValidQValue(value)) {
|
||||
if (!QVALUE.test(value)) {
|
||||
handleInvalidValue(`Invalid q value for range ${range}: ${value
|
||||
} does not match ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] ).`, strict);
|
||||
}
|
||||
@@ -332,7 +211,7 @@ function parseNoParameters(input: string, strict = false): AcceptHeader[] {
|
||||
return result;
|
||||
}
|
||||
const val = qvalue.slice(2);
|
||||
if (!isValidQValue(val)) {
|
||||
if (!QVALUE.test(val)) {
|
||||
handleInvalidValue(`Invalid q value for range ${range}: ${val
|
||||
} 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[] {
|
||||
const results = parseNoParameters(input);
|
||||
return results.filter((result): boolean => {
|
||||
if (!token.test(result.range)) {
|
||||
if (!TOKEN.test(result.range)) {
|
||||
handleInvalidValue(
|
||||
`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[] {
|
||||
const results = parseNoParameters(input);
|
||||
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);
|
||||
return false;
|
||||
}
|
||||
@@ -495,7 +374,7 @@ export function parseContentType(input: string): ContentType {
|
||||
// Quoted strings could prevent split from having correct results
|
||||
const { result, replacements } = transformQuotedStrings(input);
|
||||
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}`);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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).
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TargetExtractor } from '../http/input/identifier/TargetExtractor';
|
||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import { BadRequestHttpError } from './errors/BadRequestHttpError';
|
||||
import { errorTermsToMetadata } from './errors/HttpErrorUtil';
|
||||
|
||||
/**
|
||||
* Changes a potential Windows path into a POSIX path.
|
||||
@@ -235,7 +236,7 @@ Promise<string> {
|
||||
const target = await targetExtractor.handleSafe({ request });
|
||||
if (!target.path.startsWith(baseUrl)) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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:',
|
||||
'disallowedMethod',
|
||||
'errorCode',
|
||||
'errorResponse',
|
||||
'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:',
|
||||
'location',
|
||||
'slug',
|
||||
|
||||
@@ -12,15 +12,8 @@ export function isError(error: any): error is Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the input is a native error.
|
||||
* If not the input will be re-thrown.
|
||||
* Creates a string representing the error message of this object.
|
||||
*/
|
||||
export function assertError(error: unknown): asserts error is Error {
|
||||
if (!isError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function createErrorMessage(error: unknown): string {
|
||||
return isError(error) ? error.message : `Unknown error: ${error}`;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DataFactory } from 'n3';
|
||||
import type { NamedNode, Quad, Quad_Subject } from 'rdf-js';
|
||||
import { toNamedTerm } from '../TermUtil';
|
||||
import { SOLID_ERROR } from '../Vocabularies';
|
||||
import type { NamedNode } from 'rdf-js';
|
||||
import { RepresentationMetadata } from '../../http/representation/RepresentationMetadata';
|
||||
import { toLiteral, toNamedTerm } from '../TermUtil';
|
||||
import { HTTP, SOLID_ERROR, XSD } from '../Vocabularies';
|
||||
import { isError } from './ErrorUtil';
|
||||
import quad = DataFactory.quad;
|
||||
|
||||
export interface HttpErrorOptions {
|
||||
cause?: unknown;
|
||||
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 cause?: unknown;
|
||||
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.
|
||||
@@ -41,23 +40,20 @@ export class HttpError<T extends number = number> extends Error implements HttpE
|
||||
this.name = name;
|
||||
this.cause = options.cause;
|
||||
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 {
|
||||
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[] {
|
||||
// The reason we have this here instead of the generate function below
|
||||
// is because we still want errors created with `new HttpError` to be treated identical
|
||||
// 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)),
|
||||
];
|
||||
protected generateMetadata(): void {
|
||||
this.metadata.add(SOLID_ERROR.terms.errorResponse, generateHttpErrorUri(this.statusCode));
|
||||
this.metadata.add(HTTP.terms.statusCodeNumber, toLiteral(this.statusCode, XSD.terms.integer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { createErrorMessage } from './ErrorUtil';
|
||||
import { HttpError } from './HttpError';
|
||||
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 {
|
||||
return HttpError.isInstance(error) ? error.statusCode : 500;
|
||||
export function errorTermsToMetadata(terms: Dict<string>, metadata?: RepresentationMetadata): RepresentationMetadata {
|
||||
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.
|
||||
* If they are all within the 4xx range, 400 will be used, otherwise 500.
|
||||
*
|
||||
* @param errors - Errors to combine.
|
||||
*/
|
||||
export function createAggregateError(errors: Error[]):
|
||||
HttpError {
|
||||
export function createAggregateError(errors: Error[]): HttpError {
|
||||
const httpErrors = errors.map((error): HttpError =>
|
||||
HttpError.isInstance(error) ? error : new InternalServerError(createErrorMessage(error)));
|
||||
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 type { HttpErrorOptions } from './HttpError';
|
||||
import { generateHttpErrorClass } from './HttpError';
|
||||
import quad = DataFactory.quad;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const BaseHttpError = generateHttpErrorClass(405, 'MethodNotAllowedHttpError');
|
||||
@@ -18,15 +14,10 @@ export class MethodNotAllowedHttpError extends BaseHttpError {
|
||||
|
||||
public constructor(methods: string[] = [], message?: string, options?: HttpErrorOptions) {
|
||||
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;
|
||||
}
|
||||
|
||||
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 { generateHttpErrorClass, HttpError } from './HttpError';
|
||||
import { generateHttpErrorUri, HttpError } from './HttpError';
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(statusCode, name, message, options);
|
||||
this.location = location;
|
||||
this.metadata.add(SOLID_HTTP.terms.location, DataFactory.namedNode(location));
|
||||
}
|
||||
|
||||
public static isInstance(error: any): error is RedirectHttpError {
|
||||
@@ -35,16 +38,12 @@ export function generateRedirectHttpErrorClass<TCode extends number>(
|
||||
code: TCode,
|
||||
name: string,
|
||||
): RedirectHttpErrorClass<TCode> {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const BaseClass = generateHttpErrorClass(code, name);
|
||||
|
||||
// Need to extend `BaseClass` instead of `RedirectHttpError` to have the required static methods
|
||||
return class SpecificRedirectHttpError extends BaseClass implements RedirectHttpError {
|
||||
public readonly location: string;
|
||||
return class SpecificRedirectHttpError extends RedirectHttpError<TCode> {
|
||||
public static readonly statusCode = code;
|
||||
public static readonly uri = generateHttpErrorUri(code);
|
||||
|
||||
public constructor(location: string, message?: string, options?: HttpErrorOptions) {
|
||||
super(message, options);
|
||||
this.location = location;
|
||||
super(code, name, location, message, options);
|
||||
}
|
||||
|
||||
public static isInstance(error: any): error is SpecificRedirectHttpError {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { URL } from 'url';
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import { errorTermsToMetadata } from '../errors/HttpErrorUtil';
|
||||
import { InternalServerError } from '../errors/InternalServerError';
|
||||
import { ensureTrailingSlash, isContainerIdentifier } from '../PathUtil';
|
||||
import type { IdentifierStrategy } from './IdentifierStrategy';
|
||||
@@ -18,7 +19,7 @@ export abstract class BaseIdentifierStrategy implements IdentifierStrategy {
|
||||
public getParentContainer(identifier: ResourceIdentifier): ResourceIdentifier {
|
||||
if (!this.supportsIdentifier(identifier)) {
|
||||
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)) {
|
||||
throw new InternalServerError(`Cannot obtain the parent of ${identifier.path} because it is a root container.`);
|
||||
|
||||
Reference in New Issue
Block a user