Skip to content
Merged
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
2 changes: 1 addition & 1 deletion plugins/rw-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"dependencies": {
"@backstage/catalog-model": "^1.7.6",
"@backstage/errors": "^1.2.7",
"@rwdocs/core": "^0.1.18",
"@rwdocs/core": "^0.1.19",
"express": "^4.21.0",
"express-promise-router": "^4.1.0"
},
Expand Down
33 changes: 32 additions & 1 deletion plugins/rw-backend/src/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ function makeApp(hub: Hub) {
const app = express().use(router);
app.use(
(err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
const statusByName: Record<string, number> = { InputError: 400, NotFoundError: 404 };
const statusByName: Record<string, number> = {
InputError: 400,
NotFoundError: 404,
ServiceUnavailableError: 503,
};
const status = statusByName[err.name] ?? 500;
res.status(status).json({ error: { name: err.name, message: err.message } });
},
Expand Down Expand Up @@ -142,6 +146,33 @@ describe("createRouter", () => {
});
});

describe("storage errors", () => {
it("returns 503 when getNavigation throws storage error", async () => {
mockSite.getNavigation.mockImplementation(() => {
throw new Error("S3: storage unavailable");
});
const res = await request(app).get(`${prefix}/navigation`);
expect(res.status).toBe(503);
expect(res.body.error.name).toBe("ServiceUnavailableError");
});

it("returns 503 when renderPage throws storage error", async () => {
mockSite.renderPage.mockRejectedValue(new Error("Storage error: S3: storage unavailable"));
const res = await request(app).get(`${prefix}/pages/guide`);
expect(res.status).toBe(503);
expect(res.body.error.name).toBe("ServiceUnavailableError");
});

it("returns 503 when getNavigation throws on scope resolution", async () => {
mockSite.getNavigation.mockImplementation(() => {
throw new Error("S3: storage unavailable");
});
const res = await request(app).get(`${prefix}/pages/?sectionRef=domain:default/billing`);
expect(res.status).toBe(503);
expect(res.body.error.name).toBe("ServiceUnavailableError");
});
});

describe("unknown entity ref", () => {
it("returns 404 for non-existent entity", async () => {
const res = await request(app).get("/site/default/component/unknown/config");
Expand Down
24 changes: 21 additions & 3 deletions plugins/rw-backend/src/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Router from "express-promise-router";
import type { HttpAuthService, LoggerService } from "@backstage/backend-plugin-api";
import { InputError, NotFoundError } from "@backstage/errors";
import { InputError, NotFoundError, ServiceUnavailableError } from "@backstage/errors";
import type { RwSite } from "@rwdocs/core";
import type { Hub } from "./hub";

Expand Down Expand Up @@ -39,7 +39,7 @@ export async function createRouter(options: RouterOptions) {
const site: RwSite = res.locals.rwSite;
const sectionRefParam = req.query.sectionRef;
const sectionRef = typeof sectionRefParam === "string" ? sectionRefParam : null;
const nav = site.getNavigation(sectionRef);
const nav = getNavigationOrThrow(site, sectionRef);
res.json(nav);
});

Expand All @@ -50,7 +50,7 @@ export async function createRouter(options: RouterOptions) {

let pagePath = "";
if (sectionRef) {
const nav = site.getNavigation(sectionRef);
const nav = getNavigationOrThrow(site, sectionRef);
if (nav.scope?.path) {
pagePath = nav.scope.path.replace(/^\//, "");
}
Expand All @@ -73,6 +73,14 @@ export async function createRouter(options: RouterOptions) {
return router;
}

function getNavigationOrThrow(site: RwSite, sectionRef: string | null) {
try {
return site.getNavigation(sectionRef);
} catch (err) {
throw toStorageError(err);
}
}

async function renderPageOrThrow(site: RwSite, pagePath: string) {
try {
return await site.renderPage(pagePath);
Expand All @@ -81,6 +89,16 @@ async function renderPageOrThrow(site: RwSite, pagePath: string) {
if (message.includes("Content not found")) {
throw new NotFoundError(`Page not found: /${pagePath}`);
}
// Message prefix comes from @rwdocs/core native addon (RenderError::Storage).
// Must be updated if the upstream error format changes.
if (message.includes("Storage error")) {
throw toStorageError(err);
}
throw err;
}
}

function toStorageError(err: unknown): ServiceUnavailableError {
const message = err instanceof Error ? err.message : String(err);
return new ServiceUnavailableError(`Storage unavailable: ${message}`);
}
2 changes: 1 addition & 1 deletion plugins/rw/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"dependencies": {
"@backstage/catalog-model": "^1.7.6",
"@material-ui/icons": "^4.11.3",
"@rwdocs/viewer": "^0.1.18"
"@rwdocs/viewer": "^0.1.19"
},
"peerDependencies": {
"@backstage/core-components": "^0.18.0",
Expand Down
44 changes: 22 additions & 22 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8626,7 +8626,7 @@ __metadata:
"@backstage/cli": "npm:^0.36.0"
"@backstage/errors": "npm:^1.2.7"
"@jest/environment-jsdom-abstract": "npm:^30.2.0"
"@rwdocs/core": "npm:^0.1.18"
"@rwdocs/core": "npm:^0.1.19"
"@types/express": "npm:^4.17.0"
"@types/jest": "npm:^30.0.0"
"@types/jsdom": "npm:^28"
Expand Down Expand Up @@ -8656,7 +8656,7 @@ __metadata:
"@backstage/plugin-catalog-react": "npm:^2.0.0"
"@backstage/test-utils": "npm:^1.7.0"
"@material-ui/icons": "npm:^4.11.3"
"@rwdocs/viewer": "npm:^0.1.18"
"@rwdocs/viewer": "npm:^0.1.19"
"@testing-library/dom": "npm:^10.0.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"
Expand All @@ -8680,52 +8680,52 @@ __metadata:
languageName: unknown
linkType: soft

"@rwdocs/core-darwin-arm64@npm:0.1.18":
version: 0.1.18
resolution: "@rwdocs/core-darwin-arm64@npm:0.1.18"
"@rwdocs/core-darwin-arm64@npm:0.1.19":
version: 0.1.19
resolution: "@rwdocs/core-darwin-arm64@npm:0.1.19"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard

"@rwdocs/core-linux-x64-gnu@npm:0.1.18":
version: 0.1.18
resolution: "@rwdocs/core-linux-x64-gnu@npm:0.1.18"
"@rwdocs/core-linux-x64-gnu@npm:0.1.19":
version: 0.1.19
resolution: "@rwdocs/core-linux-x64-gnu@npm:0.1.19"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard

"@rwdocs/core-linux-x64-musl@npm:0.1.18":
version: 0.1.18
resolution: "@rwdocs/core-linux-x64-musl@npm:0.1.18"
"@rwdocs/core-linux-x64-musl@npm:0.1.19":
version: 0.1.19
resolution: "@rwdocs/core-linux-x64-musl@npm:0.1.19"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard

"@rwdocs/core@npm:^0.1.18":
version: 0.1.18
resolution: "@rwdocs/core@npm:0.1.18"
"@rwdocs/core@npm:^0.1.19":
version: 0.1.19
resolution: "@rwdocs/core@npm:0.1.19"
dependencies:
"@rwdocs/core-darwin-arm64": "npm:0.1.18"
"@rwdocs/core-linux-x64-gnu": "npm:0.1.18"
"@rwdocs/core-linux-x64-musl": "npm:0.1.18"
"@rwdocs/core-darwin-arm64": "npm:0.1.19"
"@rwdocs/core-linux-x64-gnu": "npm:0.1.19"
"@rwdocs/core-linux-x64-musl": "npm:0.1.19"
dependenciesMeta:
"@rwdocs/core-darwin-arm64":
optional: true
"@rwdocs/core-linux-x64-gnu":
optional: true
"@rwdocs/core-linux-x64-musl":
optional: true
checksum: 10c0/cde530d6d0fa05e69f7e5ba3855c5e87519532ba128b8aff03889d186a239ab25af4bb7d2ceec9a6c8d021e780519b641cfb94b6f0068605a52c2adb148f0472
checksum: 10c0/063ea2008633c42861c4449c5391d14eb6d0bf8cf1595b3fac3b8fb75bdeec691532069cddc04a30fdd39d067fa19816afe15f6d98dbae632ad169ca374d88f7
languageName: node
linkType: hard

"@rwdocs/viewer@npm:^0.1.18":
version: 0.1.18
resolution: "@rwdocs/viewer@npm:0.1.18"
"@rwdocs/viewer@npm:^0.1.19":
version: 0.1.19
resolution: "@rwdocs/viewer@npm:0.1.19"
dependencies:
"@fontsource/jetbrains-mono": "npm:^5.2.8"
"@fontsource/roboto": "npm:^5.2.9"
checksum: 10c0/5ed199671bae1136f1ab8ce6c4b0ada8676512faf4db7b3e406934933724f9f7ea8fd4b1a366105ca3b9b4cb60f1bc3822786c0ff9cb5c8ef427722f8b5cc4f9
checksum: 10c0/23b96bdd0a16a24c404e30f73076f9b58a146a9b487aed7ab46b0dbf3f0906d4631e1e2069df018dec4888f1515b76768304ad32ff5bf3e3548c52a751c95663
languageName: node
linkType: hard

Expand Down