Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement RFC7807 #65

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ declare namespace createError {
code: string;
name: string;
statusCode?: number;

toRFC7807: (instance?: RFC7807Error['instance'], details?: RFC7807Error['details']) => RFC7807Error;
}

interface RFC7807Error {
/**
* A URI reference [RFC3986] that identifies the
* problem type. This specification encourages that, when
* dereferenced, it provide human-readable documentation for the
* problem type (e.g., using HTML [W3C.REC-html5-20141028]).
* When this member is not present, its value is assumed to be
* "about:blank".
*/
type: string;
/**
* A short, human-readable summary of the problem
* type. It SHOULD NOT change from occurrence to occurrence of the
* problem, except for purposes of localization (e.g., using
* proactive content negotiation; see [RFC7231], Section 3.4).
*/
title: string;
/**
* The HTTP status code ([RFC7231], Section 6)
* generated by the origin server for this occurrence of the problem.
*/
status: number;
/**
* A human-readable explanation specific to this
* occurrence of the problem.
*/
detail: string;
/**
* A URI reference that identifies the specific
* occurrence of the problem. It may or may not yield further
* information if dereferenced.
*/
instance: string;
code: string;
details: { [key: string]: any };
}

interface FastifyErrorConstructor {
Expand Down
15 changes: 14 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const { inherits, format } = require('util')

function createError (code, message, statusCode = 500, Base = Error) {
function createError (code, message, statusCode = 500, Base = Error, uriReference) {
if (!code) throw new Error('Fastify error code must not be empty')
if (!message) throw new Error('Fastify error message must not be empty')

Expand All @@ -28,13 +28,26 @@ function createError (code, message, statusCode = 500, Base = Error) {
}

this.statusCode = statusCode || undefined
this.uriReference = uriReference || undefined
}
FastifyError.prototype[Symbol.toStringTag] = 'Error'

FastifyError.prototype.toString = function () {
return `${this.name} [${this.code}]: ${this.message}`
}

FastifyError.prototype.toRFC7807 = function (instance, details) {
return {
type: this.uriReference || 'about:blank',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the URI reference should be taken from the request itself and defined at the top for all errors of the same kind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I called it uriReference because according to RFC7807 it is called type. But that would be imho confusing.

https://datatracker.ietf.org/doc/html/rfc7807#section-3.1

A URI reference [RFC3986] that identifies the
     * problem type.  This specification encourages that, when
     * dereferenced, it provide human-readable documentation for the
     * problem type (e.g., using HTML [W3C.REC-html5-20141028]).
     * When this member is not present, its value is assumed to be
     * "about:blank".

So e.g. we create a FastifyError with code 'FST_ERR_BAD_URL' then the uriReference would be https://www.fastify.io/docs/latest/Reference/Errors/#fst_err_bad_url

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats why this is an parameter of the createError function and not on the toRFC7807. On the toRFC7807 I use the instance parameter to provide uri from the request.

I wanted to ensure that we have no potential backwards breaking, but I could put instance and details as parameters of the actual Error. So toRFC7807 would just this.instance and this.details instead using them from toRFC7807.
Maybe that is better, as we would have those values at the time we throw the Error?

title: this.name,
status: this.statusCode,
detail: this.message,
instance: instance || '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the place where the error in the fastify route happens. Like you have an ForbiddenError in GET /users/1234 then the instance would be /users/1234

code: this.code,
details: details || {}
}
}

inherits(FastifyError, Base)

return FastifyError
Expand Down
8 changes: 6 additions & 2 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import createError, { FastifyError, FastifyErrorConstructor } from './'
import createError, { FastifyError, FastifyErrorConstructor, RFC7807Error } from './'
import { expectType } from 'tsd'

const CustomError = createError('ERROR_CODE', 'message')
Expand All @@ -7,4 +7,8 @@ const err = new CustomError()
expectType<FastifyError>(err)
expectType<string>(err.code)
expectType<string>(err.message)
expectType<number>(err.statusCode!)
expectType<number>(err.statusCode!)

expectType<RFC7807Error>(err.toRFC7807());
expectType<RFC7807Error>(err.toRFC7807('/dev/null'));
expectType<RFC7807Error>(err.toRFC7807(undefined, { description: 'missing value for key'}));
30 changes: 30 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,33 @@ test('Create the error without the new keyword', t => {
t.is(err.statusCode, 500)
t.truthy(err.stack)
})

test('FastifyError.toRFC7807 returns RFC7807 conform Object', t => {
const NewError = createError('CODE', 'foo')
const err = new NewError()
t.is(typeof err.toRFC7807(), 'object')
t.is(err.toRFC7807().code, 'CODE')
t.is(err.toRFC7807().detail, 'foo')
t.is(err.toRFC7807().title, 'FastifyError')
t.is(err.toRFC7807().type, 'about:blank')
t.deepEqual(err.toRFC7807().details, {})
t.is(err.toRFC7807().instance, '')
})

test('FastifyError.toRFC7807 accepts instance', t => {
const NewError = createError('CODE', 'foo')
const err = new NewError()
t.is(err.toRFC7807('/dev/null').instance, '/dev/null')
})

test('FastifyError.toRFC7807 accepts details', t => {
const NewError = createError('CODE', 'foo')
const err = new NewError()
t.deepEqual(err.toRFC7807(undefined, { max: 'not a valid maximum value for key' }).details, { max: 'not a valid maximum value for key' })
})

test('FastifyError accepts an uriReference which is later used for toRFC7807 type attribute', t => {
const NewError = createError('CODE', 'foo', undefined, undefined, 'https://www.fastify.io/docs/latest/Reference/Errors/#fst_err_bad_url')
const err = new NewError()
t.is(err.toRFC7807().type, 'https://www.fastify.io/docs/latest/Reference/Errors/#fst_err_bad_url')
})