-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support redirection through errors
- Loading branch information
Showing
12 changed files
with
192 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { NotImplementedHttpError } from '../../../util/errors/NotImplementedHttpError'; | ||
import { RedirectHttpError } from '../../../util/errors/RedirectHttpError'; | ||
import { RedirectResponseDescription } from '../response/RedirectResponseDescription'; | ||
import type { ResponseDescription } from '../response/ResponseDescription'; | ||
import type { ErrorHandlerArgs } from './ErrorHandler'; | ||
import { ErrorHandler } from './ErrorHandler'; | ||
|
||
/** | ||
* Internally we create redirects by throwing specific {@link RedirectHttpError}s. | ||
* This Error handler converts those to {@link RedirectResponseDescription}s that are used for output. | ||
*/ | ||
export class RedirectingErrorHandler extends ErrorHandler { | ||
public async canHandle({ error }: ErrorHandlerArgs): Promise<void> { | ||
if (!RedirectHttpError.isInstance(error)) { | ||
throw new NotImplementedHttpError('Only redirect errors are supported.'); | ||
} | ||
} | ||
|
||
public async handle({ error }: ErrorHandlerArgs): Promise<ResponseDescription> { | ||
// Cast verified by canHandle | ||
return new RedirectResponseDescription(error as RedirectHttpError); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
import { DataFactory } from 'n3'; | ||
import type { RedirectHttpError } from '../../../util/errors/RedirectHttpError'; | ||
import { SOLID_HTTP } from '../../../util/Vocabularies'; | ||
import { RepresentationMetadata } from '../../representation/RepresentationMetadata'; | ||
import { ResponseDescription } from './ResponseDescription'; | ||
|
||
/** | ||
* Corresponds to a 301/302 response, containing the relevant location metadata. | ||
* Corresponds to a redirect response, containing the relevant location metadata. | ||
*/ | ||
export class RedirectResponseDescription extends ResponseDescription { | ||
public constructor(location: string, permanently = false) { | ||
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(location) }); | ||
super(permanently ? 301 : 302, metadata); | ||
public constructor(error: RedirectHttpError) { | ||
const metadata = new RepresentationMetadata({ [SOLID_HTTP.location]: DataFactory.namedNode(error.location) }); | ||
super(error.statusCode, metadata); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { HttpErrorOptions } from './HttpError'; | ||
import { RedirectHttpError } from './RedirectHttpError'; | ||
|
||
/** | ||
* Error used for resources that have been moved temporarily. | ||
*/ | ||
export class FoundHttpError extends RedirectHttpError { | ||
public constructor(location: string, message?: string, options?: HttpErrorOptions) { | ||
super(302, location, 'FoundHttpError', message, options); | ||
} | ||
|
||
public static isInstance(error: any): error is FoundHttpError { | ||
return RedirectHttpError.isInstance(error) && error.statusCode === 302; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { HttpErrorOptions } from './HttpError'; | ||
import { RedirectHttpError } from './RedirectHttpError'; | ||
|
||
/** | ||
* Error used for resources that have been moved permanently. | ||
*/ | ||
export class MovedPermanentlyHttpError extends RedirectHttpError { | ||
public constructor(location: string, message?: string, options?: HttpErrorOptions) { | ||
super(301, location, 'MovedPermanentlyHttpError', message, options); | ||
} | ||
|
||
public static isInstance(error: any): error is MovedPermanentlyHttpError { | ||
return RedirectHttpError.isInstance(error) && error.statusCode === 301; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { HttpErrorOptions } from './HttpError'; | ||
import { HttpError } from './HttpError'; | ||
|
||
/** | ||
* Abstract class representing a 3xx redirect. | ||
*/ | ||
export abstract class RedirectHttpError extends HttpError { | ||
public readonly location: string; | ||
|
||
protected constructor(statusCode: number, location: string, name: string, message?: string, | ||
options?: HttpErrorOptions) { | ||
super(statusCode, name, message, options); | ||
this.location = location; | ||
} | ||
|
||
public static isInstance(error: any): error is RedirectHttpError { | ||
return HttpError.isInstance(error) && typeof (error as any).location === 'string'; | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
test/unit/http/output/error/RedirectingErrorHandler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { RedirectingErrorHandler } from '../../../../../src/http/output/error/RedirectingErrorHandler'; | ||
import { BadRequestHttpError } from '../../../../../src/util/errors/BadRequestHttpError'; | ||
import { FoundHttpError } from '../../../../../src/util/errors/FoundHttpError'; | ||
import { NotImplementedHttpError } from '../../../../../src/util/errors/NotImplementedHttpError'; | ||
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies'; | ||
|
||
describe('A RedirectingErrorHandler', (): void => { | ||
const preferences = {}; | ||
const handler = new RedirectingErrorHandler(); | ||
|
||
it('only accepts redirect errors.', async(): Promise<void> => { | ||
const unsupportedError = new BadRequestHttpError(); | ||
await expect(handler.canHandle({ error: unsupportedError, preferences })).rejects.toThrow(NotImplementedHttpError); | ||
|
||
const supportedError = new FoundHttpError('http://test.com/foo/bar'); | ||
await expect(handler.canHandle({ error: supportedError, preferences })).resolves.toBeUndefined(); | ||
}); | ||
|
||
it('creates redirect responses.', async(): Promise<void> => { | ||
const error = new FoundHttpError('http://test.com/foo/bar'); | ||
const result = await handler.handle({ error, preferences }); | ||
expect(result.statusCode).toBe(error.statusCode); | ||
expect(result.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(error.location); | ||
}); | ||
}); |
17 changes: 6 additions & 11 deletions
17
test/unit/http/output/response/RedirectResponseDescription.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,13 @@ | ||
import { RedirectResponseDescription } from '../../../../../src/http/output/response/RedirectResponseDescription'; | ||
import { FoundHttpError } from '../../../../../src/util/errors/FoundHttpError'; | ||
import { SOLID_HTTP } from '../../../../../src/util/Vocabularies'; | ||
|
||
describe('A RedirectResponseDescription', (): void => { | ||
const location = 'http://test.com/foo'; | ||
const error = new FoundHttpError('http://test.com/foo'); | ||
|
||
it('has status code 302 and a location.', async(): Promise<void> => { | ||
const description = new RedirectResponseDescription(location); | ||
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location); | ||
expect(description.statusCode).toBe(302); | ||
}); | ||
|
||
it('has status code 301 if the change is permanent.', async(): Promise<void> => { | ||
const description = new RedirectResponseDescription(location, true); | ||
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(location); | ||
expect(description.statusCode).toBe(301); | ||
it('has status the code and location of the error.', async(): Promise<void> => { | ||
const description = new RedirectResponseDescription(error); | ||
expect(description.metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(error.location); | ||
expect(description.statusCode).toBe(error.statusCode); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError'; | ||
import type { HttpErrorOptions } from '../../../../src/util/errors/HttpError'; | ||
import { MovedPermanentlyHttpError } from '../../../../src/util/errors/MovedPermanentlyHttpError'; | ||
import { RedirectHttpError } from '../../../../src/util/errors/RedirectHttpError'; | ||
|
||
class FixedRedirectHttpError extends RedirectHttpError { | ||
public constructor(location: string, message?: string, options?: HttpErrorOptions) { | ||
super(0, location, '', message, options); | ||
} | ||
} | ||
|
||
describe('RedirectHttpError', (): void => { | ||
const errors: [string, number, typeof FixedRedirectHttpError][] = [ | ||
[ 'MovedPermanentlyHttpError', 301, MovedPermanentlyHttpError ], | ||
[ 'FoundHttpError', 302, FoundHttpError ], | ||
]; | ||
|
||
describe.each(errors)('%s', (name, statusCode, constructor): void => { | ||
const location = 'http://test.com/foo/bar'; | ||
const options = { | ||
cause: new Error('cause'), | ||
errorCode: 'E1234', | ||
details: {}, | ||
}; | ||
const instance = new constructor(location, 'my message', options); | ||
|
||
it(`is an instance of ${name}.`, (): void => { | ||
expect(constructor.isInstance(instance)).toBeTruthy(); | ||
}); | ||
|
||
it(`has name ${name}.`, (): void => { | ||
expect(instance.name).toBe(name); | ||
}); | ||
|
||
it(`has status code ${statusCode}.`, (): void => { | ||
expect(instance.statusCode).toBe(statusCode); | ||
}); | ||
|
||
it('sets the location.', (): void => { | ||
expect(instance.location).toBe(location); | ||
}); | ||
|
||
it('sets the message.', (): void => { | ||
expect(instance.message).toBe('my message'); | ||
}); | ||
|
||
it('sets the cause.', (): void => { | ||
expect(instance.cause).toBe(options.cause); | ||
}); | ||
|
||
it('sets the error code.', (): void => { | ||
expect(instance.errorCode).toBe(options.errorCode); | ||
}); | ||
|
||
it('defaults to an HTTP-specific error code.', (): void => { | ||
expect(new constructor(location).errorCode).toBe(`H${statusCode}`); | ||
}); | ||
|
||
it('sets the details.', (): void => { | ||
expect(instance.details).toBe(options.details); | ||
}); | ||
}); | ||
}); |