diff --git a/app.js b/app.js index 75e67b4..5063775 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,4 @@ #!/usr/bin/env node -import createError from "http-errors" import express from "express" import path from "path" import { fileURLToPath } from "url" @@ -12,12 +11,14 @@ import createRouter from "./routes/create.js" import updateRouter from "./routes/update.js" import deleteRouter from "./routes/delete.js" import overwriteRouter from "./routes/overwrite.js" +import { messenger } from './error-messenger.js' import cors from "cors" let app = express() app.use(logger('dev')) app.use(express.json()) +app.use(express.text()) if(process.env.OPEN_API_CORS !== "false") { // This enables CORS for all requests. We may want to update this in the future and only apply to some routes. app.use( @@ -68,20 +69,7 @@ app.use('/app/update', updateRouter) app.use('/app/delete', deleteRouter) app.use('/app/overwrite', overwriteRouter) -// catch 404 and forward to error handler -app.use(function(req, res, next) { - next(createError(404)) -}) - -// error handler -app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message - res.locals.error = req.app.get('env') === 'development' ? err : {} - - // render the error page - res.status(err.status || 500) - res.send(err.message) -}) +// RERUM error response handler, as well as unhandled generic app error handler +app.use(messenger) export default app diff --git a/error-messenger.js b/error-messenger.js new file mode 100644 index 0000000..acaa354 --- /dev/null +++ b/error-messenger.js @@ -0,0 +1,34 @@ +/** + * Errors from RERUM are a response code with a text body (except those handled specifically upstream) + * We want to send the same error code and message through. It is assumed to be RESTful and useful. + * This will also handle generic (500) app level errors, as well as app level 404 errors. + * + * @param rerum_error_res A Fetch API Response object from a fetch() to RERUM that encountered an error. Explanatory text is in .text(). In some cases it is a unhandled generic (500) app level Error. + * @param req The Express Request object from the request into TinyNode + * @param res The Express Response object to send out of TinyNode + * @param next The Express next() operator, unused here but required to support the middleware chain. + */ +export async function messenger(rerum_error_res, req, res, next) { + if (res.headersSent) { + return + } + let error = {} + let rerum_err_text + try { + // Unless already handled upstream the rerum_error_res is an error Response with details as a textual body. + rerum_err_text = await rerum_error_res.text() + } + catch (err) { + // It is some 500 + rerum_err_text = undefined + } + if (rerum_err_text) error.message = rerum_err_text + else { + // Perhaps this is a more generic 500 from the app, perhaps involving RERUM, and there is no good rerum_error_res + error.message = rerum_error_res.statusMessage ?? rerum_error_res.message ?? `A server error has occured` + } + error.status = rerum_error_res.statusCode ?? rerum_error_res.status ?? 500 + console.error(error) + res.set("Content-Type", "text/plain; charset=utf-8") + res.status(error.status).send(error.message) +} diff --git a/public/scripts/api.js b/public/scripts/api.js index b16b25b..ce165f0 100644 --- a/public/scripts/api.js +++ b/public/scripts/api.js @@ -126,7 +126,7 @@ function create(form) { JSON.parse(obj) } catch (error) { console.error("You did not provide valid JSON") - setMessage("You did not provide valid JSON") + _customEvent("rerum-error", "You did not provide valid JSON", {}, error) document.getElementById("obj-viewer").style.display = "none" return false } diff --git a/routes/__tests__/create.test.js b/routes/__tests__/create.test.js index 9b21d83..e928820 100644 --- a/routes/__tests__/create.test.js +++ b/routes/__tests__/create.test.js @@ -20,7 +20,9 @@ beforeEach(() => { */ global.fetch = jest.fn(() => Promise.resolve({ - json: () => Promise.resolve({ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }) + json: () => Promise.resolve({ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }), + ok: true, + text: () => Promise.resolve("Descriptive Error Here") }) ) }) diff --git a/routes/__tests__/delete.test.js b/routes/__tests__/delete.test.js index eeb6e11..f92d6b1 100644 --- a/routes/__tests__/delete.test.js +++ b/routes/__tests__/delete.test.js @@ -19,7 +19,8 @@ beforeEach(() => { */ global.fetch = jest.fn(() => Promise.resolve({ - text: () => Promise.resolve("") + text: () => Promise.resolve(""), + ok: true }) ) }) diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index 225c280..6ee5ae4 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -16,9 +16,9 @@ const rerum_tiny_test_obj_id = `${process.env.RERUM_ID_PATTERN}tiny_tester` beforeEach(() => { global.fetch = jest.fn(() => Promise.resolve({ - status: 200, + json: () => Promise.resolve({ "@id": rerum_tiny_test_obj_id, "testing": "item", "__rerum": { "stuff": "here" } }), ok: true, - json: () => Promise.resolve({ "@id": rerum_tiny_test_obj_id, "testing": "item", "__rerum": { "stuff": "here" } }) + text: () => Promise.resolve("Descriptive Error Here") }) ) }) diff --git a/routes/__tests__/query.test.js b/routes/__tests__/query.test.js index c7caed8..3b62cd4 100644 --- a/routes/__tests__/query.test.js +++ b/routes/__tests__/query.test.js @@ -19,7 +19,9 @@ beforeEach(() => { */ global.fetch = jest.fn(() => Promise.resolve({ - json: () => Promise.resolve([{ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }]) + json: () => Promise.resolve([{ "@id": rerum_uri, "test": "item", "__rerum": { "stuff": "here" } }]), + ok: true, + text: () => Promise.resolve("Descriptive Error Here") }) ) }) diff --git a/routes/__tests__/update.test.js b/routes/__tests__/update.test.js index 3385609..1ddc3fc 100644 --- a/routes/__tests__/update.test.js +++ b/routes/__tests__/update.test.js @@ -21,7 +21,9 @@ beforeEach(() => { */ global.fetch = jest.fn(() => Promise.resolve({ - json: () => Promise.resolve({ "@id": rerum_uri_updated, "testing": "item", "__rerum": { "stuff": "here" } }) + json: () => Promise.resolve({ "@id": rerum_uri_updated, "testing": "item", "__rerum": { "stuff": "here" } }), + ok: true, + text: () => Promise.resolve("Descriptive Error Here") }) ) }) diff --git a/routes/create.js b/routes/create.js index 6a2c7f2..23056f4 100644 --- a/routes/create.js +++ b/routes/create.js @@ -18,8 +18,17 @@ router.post('/', checkAccessToken, async (req, res, next) => { } } const createURL = `${process.env.RERUM_API_ADDR}create` - const result = await fetch(createURL, createOptions).then(res=>res.json()) - .catch(err=>next(err)) + let errored = false + const result = await fetch(createURL, createOptions).then(res=>{ + if (res.ok) return res.json() + errored = true + return res + }) + .catch(err => { + throw err + }) + // Send RERUM error responses to error-messenger.js + if (errored) return next(result) res.setHeader("Location", result["@id"] ?? result.id) res.status(201) res.json(result) diff --git a/routes/delete.js b/routes/delete.js index ea941ef..a3e80a1 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -23,7 +23,16 @@ router.delete('/', checkAccessToken, async (req, res, next) => { } } const deleteURL = `${process.env.RERUM_API_ADDR}delete` - const result = await fetch(deleteURL, deleteOptions).then(res => res.text()) + let errored = false + const result = await fetch(deleteURL, deleteOptions).then(res=>{ + if (!res.ok) errored = true + return res.text() + }) + .catch(err => { + throw err + }) + // Send RERUM error responses to error-messenger.js + if (errored) return next(results) res.status(204) res.send(result) } @@ -44,8 +53,16 @@ router.delete('/:id', async (req, res, next) => { 'Authorization': `Bearer ${process.env.ACCESS_TOKEN}`, } } - const result = await fetch(deleteURL, deleteOptions).then(res => res.text()) - .catch(err=>next(err)) + let errored = false + const result = await fetch(deleteURL, deleteOptions).then(res => { + if(!res.ok) errored = true + return res + }) + .catch(err => { + throw err + }) + // Send RERUM error responses to error-messenger.js + if (errored) return next(results) res.status(204) res.send(result) } diff --git a/routes/overwrite.js b/routes/overwrite.js index 8c66600..b2bb6dd 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -37,26 +37,31 @@ router.put('/', checkAccessToken, async (req, res, next) => { } const overwriteURL = `${process.env.RERUM_API_ADDR}overwrite` + let errored = false const response = await fetch(overwriteURL, overwriteOptions) - .then(resp=>{ - if (!resp.ok) throw resp - return resp - }) - .catch(async err => { - // Handle 409 conflict error for version mismatch - if (err.status === 409) { - const currentVersion = await err.json() - return res.status(409).json(currentVersion) + .then(async rerum_res=>{ + if (rerum_res.ok) return rerum_res.json() + errored = true + if (rerum_res.headers.get("Content-Type").includes("json")) { + // Special handling. This does not go through to error-messenger.js + if (rerum_res.status === 409) { + const currentVersion = await rerum_res.json() + return res.status(409).json(currentVersion) + } } - throw new Error(`Error in overwrite request: ${err.status} ${err.statusText}`) + return rerum_res + }) + .catch(err => { + throw err }) - if(res.headersSent) return - const result = await response.json() + // Send RERUM error responses to error-messenger.js + if (errored) return next(response) + const result = response const location = result?.["@id"] ?? result?.id if (location) { res.setHeader("Location", location) } - res.status(response.status ?? 200) + res.status(200) res.json(result) } catch (err) { diff --git a/routes/query.js b/routes/query.js index a147cc3..4b06a82 100644 --- a/routes/query.js +++ b/routes/query.js @@ -25,8 +25,17 @@ router.post('/', async (req, res, next) => { } } const queryURL = `${process.env.RERUM_API_ADDR}query?limit=${lim}&skip=${skip}` - const results = await fetch(queryURL, queryOptions).then(res=>res.json()) - .catch(err=>next(err)) + let errored = false + const results = await fetch(queryURL, queryOptions).then(res=>{ + if (res.ok) return res.json() + errored = true + return res + }) + .catch(err => { + throw err + }) + // Send RERUM error responses to error-messenger.js + if (errored) return next(results) res.status(200) res.send(results) } diff --git a/routes/update.js b/routes/update.js index 391b3ce..b5ca5d5 100644 --- a/routes/update.js +++ b/routes/update.js @@ -23,8 +23,17 @@ router.put('/', checkAccessToken, async (req, res, next) => { } } const updateURL = `${process.env.RERUM_API_ADDR}update` - const result = await fetch(updateURL, updateOptions).then(res=>res.json()) - .catch(err=>next(err)) + let errored = false + const result = await fetch(updateURL, updateOptions).then(res=>{ + if (res.ok) return res.json() + errored = true + return res + }) + .catch(err => { + throw err + }) + // Send RERUM error responses to error-messenger.js + if (errored) return next(result) res.setHeader("Location", result["@id"] ?? result.id) res.status(200) res.send(result)