Skip to content
Draft
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
10 changes: 5 additions & 5 deletions app/modules/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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
})));
});

/**
Expand Down
13 changes: 13 additions & 0 deletions app/modules/api/routeErrorHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
94 changes: 94 additions & 0 deletions test/storageRouteErrors.test.ts
Original file line number Diff line number Diff line change
@@ -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));
}