Skip to content

Commit

Permalink
feat: Use problem json (RFC 7807) for error reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
theorm committed Jul 12, 2024
1 parent fb86b73 commit 2970db3
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 25 deletions.
6 changes: 6 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Configuration {
rateLimiter?: RateLimiterConfiguration & { enabled?: boolean }
publicApiPrefix?: string
useDbUserInRequestContext?: boolean
problemUriBase?: string

// TODO: move to services:
sequelizeClient?: Sequelize
Expand Down Expand Up @@ -73,6 +74,11 @@ const configurationSchema: JSONSchemaDefinition = {
'If `true`, the user object is loaded from the db on every request. ' +
'If `false` (default), the user object is created from the JWT token',
},
problemUriBase: {
type: 'string',
description:
'Base URI for problem URIs. Falls back to the default URI (https://impresso-project.ch/probs) if not set',
},
},
} as const

Expand Down
92 changes: 75 additions & 17 deletions src/middleware/errorHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,83 @@ import { errorHandler } from '@feathersjs/express'
import type { ImpressoApplication } from '../types'
import { logger } from '../logger'

const DefaultProblemUriBase = 'https://impresso-project.ch/probs'

// https://datatracker.ietf.org/doc/html/rfc7807#section-3.1
interface Problem {
/**
* 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
}

export default (app: ImpressoApplication & ExpressApplication) => {
const isProduction = process.env.NODE_ENV === 'production'
const problemUriBase = app.get('problemUriBase') ?? DefaultProblemUriBase

app.use(
errorHandler({
json: {
404: (err: GeneralError, req: Request, res: Response) => {
delete err.stack
res.json({ message: 'Not found' })
res.json({
type: `${problemUriBase}/not-found`,
title: 'Item not found',
status: 404,
} satisfies Problem)
},
500: (err: GeneralError, req: Request, res: Response) => {
if (isProduction) {
delete err.stack
} else {
logger.error('Error [500]', err)
}
res.json({ message: 'service unavailable' })
res.json({
type: `${problemUriBase}/internal-error`,
title: 'Internal server error',
status: 500,
} satisfies Problem)
},
// unauthentified
401: (err: GeneralError, req: Request, res: Response) => {
res.json({
message: err.message,
name: err.name,
code: err.code,
})
type: `${problemUriBase}/unauthorized`,
title: 'Access to this resource requires authentication',
status: 401,
detail: `${err.name}: ${err.message}`,
} satisfies Problem)
},
403: (err: GeneralError, req: Request, res: Response) => {
res.json({
type: `${problemUriBase}/unauthorized`,
title: 'Access to this resource is forbidden',
status: 403,
detail: `${err.name}: ${err.message}`,
} satisfies Problem)
},
// bad request
400: (err: GeneralError, req: Request, res: Response) => {
Expand All @@ -40,11 +92,11 @@ export default (app: ImpressoApplication & ExpressApplication) => {
logger.error('Error [400]', err.data || err)
}
res.json({
message: err.message || 'Please check request params',
name: err.name,
code: err.code,
errors: err.data,
})
type: `${problemUriBase}/bad-request`,
title: 'Incorrect request parameters',
status: 400,
detail: `${err.name}: ${err.message}`,
} satisfies Problem)
},
default: (err: GeneralError, req: Request, res: Response) => {
// handle all other errors
Expand All @@ -53,13 +105,19 @@ export default (app: ImpressoApplication & ExpressApplication) => {

if (err instanceof FeathersError) {
return res.json({
code: err.code,
message: err.message,
data: err.data,
})
type: `${problemUriBase}/unclassified-error`,
title: 'An unclassified error occurred',
status: err.code,
detail: err.message,
} satisfies Problem)
}

res.json({ message: (err as GeneralError).message })
res.json({
type: `${problemUriBase}/unclassified-error`,
title: 'An unclassified error occurred',
status: (err as GeneralError).code ?? 500,
detail: (err as GeneralError).message,
} satisfies Problem)
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion src/models/articles.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ class Article extends BaseArticle {
this.labels = ['article']

if (mentions.length) {
this.mentions = mentions
this.mentions = mentions.filter(mention => mention != null)
}

if (topics.length) {
Expand Down
17 changes: 13 additions & 4 deletions src/schema/schemas/Error.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Default error response. TODO: replace with https://datatracker.ietf.org/doc/html/rfc9457",
"description": "Error response that follows https://datatracker.ietf.org/doc/html/rfc7807#section-3.1",
"title": "Error",
"type": "object",
"required": ["message"],
"required": ["type", "title", "status", "detail"],
"properties": {
"message": { "type": "string" },
"data": { "type": "object" }
"type": {
"type": "string",
"format": "uri",
"description": "A URI reference [RFC3986] that identifies the problem type."
},
"title": { "type": "string", "description": "A short, human-readable summary of the problem type." },
"status": { "type": "integer", "description": "The HTTP status code ([RFC7231], Section 6)" },
"detail": {
"type": "string",
"description": "A human-readable explanation specific to this occurrence of the problem."
}
}
}
4 changes: 1 addition & 3 deletions src/util/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const asApplicationJson = (schema: JSONSchema) => ({
})

const asApplicationProblemJson = (schema: JSONSchema) => ({
'application/json': {
'application/problem+json': {
schema,
},
})
Expand All @@ -47,8 +47,6 @@ export const getParameterRef = (schemaName: string) => ({
$ref: `#/components/parameters/${schemaName}`,
})

// TODO: convert schema to
// https://datatracker.ietf.org/doc/html/rfc7807
const defaultErrorSchema = getSchemaRef('Error')

interface GetStandardResponsesParams {
Expand Down

0 comments on commit 2970db3

Please sign in to comment.