diff --git a/app/modules/api/api.ts b/app/modules/api/api.ts index 02d3a799..32af634b 100644 --- a/app/modules/api/api.ts +++ b/app/modules/api/api.ts @@ -4,7 +4,7 @@ import http from 'node:http'; import IGeesomeApiModule from "./interface.js"; import {CorePermissionName} from "../database/interface.js"; import {IGeesomeApp} from "../../interface.js"; -import {sendForbiddenOnAuthRouteError} from "./routeErrorHelpers.js"; +import {sendBadGatewayOnStorageRouteError, sendForbiddenOnAuthRouteError} from "./routeErrorHelpers.js"; const {isNumber} = _; const log = debug('geesome:api:routes'); @@ -552,10 +552,10 @@ export default (app: IGeesomeApp, module: IGeesomeApiModule) => { res.writeHead(upstreamRes.statusCode || 500, upstreamRes.headers); upstreamRes.pipe(res.stream); }); - upstream.on('error', (error) => { - console.error(error); - res.send(null, 502); - }); + upstream.on('error', sendBadGatewayOnStorageRouteError(log, res, () => ({ + route: 'api/v0/refs', + proxyPath: req.route + }))); }); /** diff --git a/app/modules/api/routeErrorHelpers.ts b/app/modules/api/routeErrorHelpers.ts index 06c9851b..daa845a5 100644 --- a/app/modules/api/routeErrorHelpers.ts +++ b/app/modules/api/routeErrorHelpers.ts @@ -32,6 +32,19 @@ export function sendForbiddenOnAuthRouteError(log: DebugLog, res: IApiModuleComm }; } +export function sendBadGatewayOnStorageRouteError(log: DebugLog, res: IApiModuleCommonOutput, getContext: () => any = () => ({})) { + return (error) => { + helpers.logDebug(log, () => [ + 'storage route request failed', + { + ...getContext(), + error: getErrorMessage(error) + } + ]); + res.send(null, 502); + }; +} + function getErrorMessage(error) { if (error && error.message) { return error.message; diff --git a/test/storageRouteErrors.test.ts b/test/storageRouteErrors.test.ts new file mode 100644 index 00000000..1072f3c4 --- /dev/null +++ b/test/storageRouteErrors.test.ts @@ -0,0 +1,94 @@ +import assert from "assert"; +import http from "node:http"; +import {EventEmitter} from "node:events"; +import coreApi from "../app/modules/api/api.js"; +import {IGeesomeApp} from "../app/interface.js"; + +describe("storage route errors", function () { + it("keeps failed IPFS refs proxy requests out of stderr", async () => { + const routes: any = {}; + let storageHeadersSet = false; + const consoleErrors = await captureConsoleErrors(async () => { + await withHttpGetFailure(async () => { + coreApi({} as IGeesomeApp, { + ...getApiRouteStub(routes), + handleAuthResult: async () => null, + setStorageHeaders: () => { + storageHeadersSet = true; + } + } as any); + + const response = getResponseStub(); + routes["GET /api/v0/refs*"]({ + route: "/api/v0/refs?arg=bafk" + } as any, response); + await flushAsyncHandlers(); + + assert.equal(storageHeadersSet, true); + assert.deepEqual(response.sent, [[null, 502]]); + }); + }); + + assert.deepEqual(consoleErrors, []); + }); +}); + +function getApiRouteStub(routes) { + return { + onGet: (route, handler) => { + routes[`GET ${route}`] = handler; + }, + onPost: (route, handler) => { + routes[`POST ${route}`] = handler; + }, + onAuthorizedGet: (route, handler) => { + routes[`AUTHORIZED_GET ${route}`] = handler; + }, + onAuthorizedPost: (route, handler) => { + routes[`AUTHORIZED_POST ${route}`] = handler; + } + }; +} + +function getResponseStub() { + return { + sent: [] as any[], + send(...args) { + this.sent.push(args); + } + }; +} + +async function withHttpGetFailure(callback) { + const originalGet = http.get; + (http as any).get = () => { + const upstream = new EventEmitter(); + setImmediate(() => { + upstream.emit("error", new Error("proxy connection failed")); + }); + return upstream; + }; + try { + await callback(); + } finally { + (http as any).get = originalGet; + } +} + +async function captureConsoleErrors(callback) { + const originalConsoleError = console.error; + const consoleErrors: any[] = []; + console.error = ((...args) => { + consoleErrors.push(args); + }) as any; + try { + await callback(); + return consoleErrors; + } finally { + console.error = originalConsoleError; + } +} + +async function flushAsyncHandlers() { + await new Promise((resolve) => setImmediate(resolve)); +}