feat: Support conditions for GET/HEAD requests

* fix: updated WrappedExpiringStorage tests and timer.unref calls

* fix: removed finalizable configs and inheritors that only used timer

* fix: updated test function to test setSafeInterval and timer.unref

* fix: added NotModifiedHttpError class

* fix: added 304 error test to HttpError test file

* fix: 304 errors when making read request with matching ETag

* Update src/util/errors/NotModifiedHttpError.ts

Co-authored-by: Ted Thibodeau Jr <tthibodeau@openlinksw.com>

* fix: updated tests

* fix: try notMatchesEtag in test

* fix: DataAccessorBasedStore test passes

* fix: removed conditions check and added extra test

---------

Co-authored-by: Ted Thibodeau Jr <tthibodeau@openlinksw.com>
This commit is contained in:
zg009
2023-03-28 02:24:15 -05:00
committed by GitHub
parent 2780e88acf
commit f0596c2eb8
7 changed files with 74 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ import { BasicRepresentation } from '../http/representation/BasicRepresentation'
import type { Patch } from '../http/representation/Patch';
import type { Representation } from '../http/representation/Representation';
import { RepresentationMetadata } from '../http/representation/RepresentationMetadata';
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
import { getLoggerFor } from '../logging/LogUtil';
import { INTERNAL_QUADS } from '../util/ContentTypes';
@@ -17,6 +18,7 @@ import { ForbiddenHttpError } from '../util/errors/ForbiddenHttpError';
import { MethodNotAllowedHttpError } from '../util/errors/MethodNotAllowedHttpError';
import { NotFoundHttpError } from '../util/errors/NotFoundHttpError';
import { NotImplementedHttpError } from '../util/errors/NotImplementedHttpError';
import { NotModifiedHttpError } from '../util/errors/NotModifiedHttpError';
import { PreconditionFailedHttpError } from '../util/errors/PreconditionFailedHttpError';
import type { IdentifierStrategy } from '../util/identifiers/IdentifierStrategy';
import { concat } from '../util/IterableUtil';
@@ -103,7 +105,8 @@ export class DataAccessorBasedStore implements ResourceStore {
}
}
public async getRepresentation(identifier: ResourceIdentifier): Promise<Representation> {
public async getRepresentation(identifier: ResourceIdentifier,
preferences?: RepresentationPreferences, conditions?: Conditions): Promise<Representation> {
this.validateIdentifier(identifier);
let isMetadata = false;
@@ -116,6 +119,8 @@ export class DataAccessorBasedStore implements ResourceStore {
let metadata = await this.accessor.getMetadata(identifier);
let representation: Representation;
this.validateConditions(true, conditions, metadata);
// Potentially add auxiliary related metadata
// Solid, §4.3: "Clients can discover auxiliary resources associated with a subject resource by making an HTTP HEAD
// or GET request on the target URL, and checking the HTTP Link header with the rel parameter"
@@ -181,7 +186,7 @@ export class DataAccessorBasedStore implements ResourceStore {
throw new MethodNotAllowedHttpError([ 'POST' ], 'The given path is not a container.');
}
this.validateConditions(conditions, parentMetadata);
this.validateConditions(false, conditions, parentMetadata);
// Solid, §5.1: "Servers MAY allow clients to suggest the URI of a resource created through POST,
// using the HTTP Slug header as defined in [RFC5023].
@@ -246,7 +251,7 @@ export class DataAccessorBasedStore implements ResourceStore {
await this.accessor.canHandle(representation);
}
this.validateConditions(conditions, oldMetadata);
this.validateConditions(false, conditions, oldMetadata);
if (this.metadataStrategy.isAuxiliaryIdentifier(identifier)) {
return await this.writeMetadata(identifier, representation);
@@ -268,7 +273,7 @@ export class DataAccessorBasedStore implements ResourceStore {
}
}
this.validateConditions(conditions, metadata);
this.validateConditions(false, conditions, metadata);
}
throw new NotImplementedHttpError('Patches are not supported by the default store.');
@@ -309,7 +314,7 @@ export class DataAccessorBasedStore implements ResourceStore {
throw new ConflictHttpError('Can only delete empty containers.');
}
this.validateConditions(conditions, metadata);
this.validateConditions(false, conditions, metadata);
// Solid, §5.4: "When a contained resource is deleted,
// the server MUST also delete the associated auxiliary resources"
@@ -347,10 +352,13 @@ export class DataAccessorBasedStore implements ResourceStore {
/**
* Verify if the given metadata matches the conditions.
*/
protected validateConditions(conditions?: Conditions, metadata?: RepresentationMetadata): void {
protected validateConditions(read: boolean, conditions?: Conditions, metadata?: RepresentationMetadata): void {
// The 412 (Precondition Failed) status code indicates
// that one or more conditions given in the request header fields evaluated to false when tested on the server.
if (conditions && !conditions.matchesMetadata(metadata)) {
if (read) {
throw new NotModifiedHttpError();
}
throw new PreconditionFailedHttpError();
}
}

View File

@@ -0,0 +1,14 @@
import type { HttpErrorOptions } from './HttpError';
import { generateHttpErrorClass } from './HttpError';
// eslint-disable-next-line @typescript-eslint/naming-convention
const BaseHttpError = generateHttpErrorClass(304, 'NotModifiedHttpError');
/**
* An error is thrown when a request conflicts with the current state of the server.
*/
export class NotModifiedHttpError extends BaseHttpError {
public constructor(message?: string, options?: HttpErrorOptions) {
super(message, options);
}
}