fix(storage): recognize errors across module packages
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 29s
CI / migration-smoke (pull_request) Successful in 1m44s
CI / package-release (pull_request) Successful in 35s
CI / image-build (pull_request) Successful in 53s
CI / deploy-smoke (same-origin) (pull_request) Successful in 44s
CI / deploy-smoke (split) (pull_request) Successful in 2m20s
CI / package-publication (pull_request) Successful in 51s
CI / verify (pull_request) Successful in 1m7s
CI / infrastructure-smoke (pull_request) Successful in 29s
CI / migration-smoke (pull_request) Successful in 1m44s
CI / package-release (pull_request) Successful in 35s
CI / image-build (pull_request) Successful in 53s
CI / deploy-smoke (same-origin) (pull_request) Successful in 44s
CI / deploy-smoke (split) (pull_request) Successful in 2m20s
CI / package-publication (pull_request) Successful in 51s
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
EMPTY_MODULE_STORAGE,
|
EMPTY_MODULE_STORAGE,
|
||||||
|
isModuleStorageError,
|
||||||
isValidModuleStorageKey,
|
isValidModuleStorageKey,
|
||||||
MODULE_STORAGE_KEY_MAX_BYTES,
|
MODULE_STORAGE_KEY_MAX_BYTES,
|
||||||
MODULE_STORAGE_READ_URL_DEFAULT_SECONDS,
|
MODULE_STORAGE_READ_URL_DEFAULT_SECONDS,
|
||||||
@@ -322,6 +323,17 @@ describe("module storage scoping", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("recognizes a storage error from an installed module's separate package copy", () => {
|
||||||
|
const external = Object.assign(new Error("The storage service could not be reached."), {
|
||||||
|
code: "STORAGE_PROVIDER_UNAVAILABLE",
|
||||||
|
name: "ModuleStorageError",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(external).not.toBeInstanceOf(ModuleStorageError);
|
||||||
|
expect(isModuleStorageError(external)).toBe(true);
|
||||||
|
expect(isModuleStorageError(Object.assign(new Error("no"), { code: "UNKNOWN" }))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("gives a module no way to reach another module's storage by knowing its name", async () => {
|
it("gives a module no way to reach another module's storage by knowing its name", async () => {
|
||||||
const shared = fakeProvider();
|
const shared = fakeProvider();
|
||||||
const announcements = createModuleStorage({
|
const announcements = createModuleStorage({
|
||||||
|
|||||||
@@ -87,6 +87,28 @@ export class ModuleStorageError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether `value` is a module-storage error, judged by its stable public shape rather than its class.
|
||||||
|
*
|
||||||
|
* An installed module bundles its own copy of the contracts package (ADR 0049), so an error created
|
||||||
|
* by the host's storage provider does not share class identity with the module's
|
||||||
|
* `ModuleStorageError`. `instanceof ModuleStorageError` is therefore not a valid check across the
|
||||||
|
* module boundary.
|
||||||
|
*/
|
||||||
|
export function isModuleStorageError(
|
||||||
|
value: unknown,
|
||||||
|
): value is Pick<ModuleStorageError, "code" | "key" | "message" | "name"> {
|
||||||
|
if (!(value instanceof Error) || value.name !== "ModuleStorageError") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const { code, key } = value as { code?: unknown; key?: unknown };
|
||||||
|
return (
|
||||||
|
typeof code === "string" &&
|
||||||
|
(MODULE_STORAGE_ERROR_CODES as readonly string[]).includes(code) &&
|
||||||
|
(key === undefined || typeof key === "string")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** The S3 object-key ceiling, which every S3-compatible provider shares. Measured in UTF-8 bytes,
|
/** The S3 object-key ceiling, which every S3-compatible provider shares. Measured in UTF-8 bytes,
|
||||||
* not characters, because that is what the wire format bounds. */
|
* not characters, because that is what the wire format bounds. */
|
||||||
export const MODULE_STORAGE_KEY_MAX_BYTES = 1024;
|
export const MODULE_STORAGE_KEY_MAX_BYTES = 1024;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export {
|
|||||||
isDevicePrincipal,
|
isDevicePrincipal,
|
||||||
isModuleHttpMethod,
|
isModuleHttpMethod,
|
||||||
isModuleRouteBodyMode,
|
isModuleRouteBodyMode,
|
||||||
|
isModuleStorageError,
|
||||||
isValidModuleStorageKey,
|
isValidModuleStorageKey,
|
||||||
isValidModuleStorageReadUrlFilename,
|
isValidModuleStorageReadUrlFilename,
|
||||||
MODULE_HTTP_BODY_MODES,
|
MODULE_HTTP_BODY_MODES,
|
||||||
|
|||||||
@@ -426,7 +426,9 @@ async function seedSignage(owner) {
|
|||||||
const base = "/api/v1/modules/signage";
|
const base = "/api/v1/modules/signage";
|
||||||
const images = [];
|
const images = [];
|
||||||
for (const [index, colour] of ["#b03030", "#3060b0", "#30a050"].entries()) {
|
for (const [index, colour] of ["#b03030", "#3060b0", "#30a050"].entries()) {
|
||||||
const uploaded = await owner.evaluate(
|
let uploaded;
|
||||||
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
||||||
|
uploaded = await owner.evaluate(
|
||||||
`(async () => {
|
`(async () => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = 640;
|
canvas.width = 640;
|
||||||
@@ -445,11 +447,26 @@ async function seedSignage(owner) {
|
|||||||
})()`,
|
})()`,
|
||||||
`uploading slide ${index}`,
|
`uploading slide ${index}`,
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
attempt === 1 &&
|
||||||
|
uploaded.status === 503 &&
|
||||||
|
uploaded.body?.code === "SIGNAGE_STORAGE_UNAVAILABLE"
|
||||||
|
) {
|
||||||
|
note(`slide ${index} met one transient storage outage; retrying once`);
|
||||||
|
await sleep(1_000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (uploaded.status === "timeout") {
|
if (uploaded.status === "timeout") {
|
||||||
throw new HangError(`HTTP POST ${base}/images from page "owner"`, requestTimeoutMs);
|
throw new HangError(`HTTP POST ${base}/images from page "owner"`, requestTimeoutMs);
|
||||||
}
|
}
|
||||||
if (uploaded.status !== 200 && uploaded.status !== 201) {
|
if (uploaded.status !== 200 && uploaded.status !== 201) {
|
||||||
throw new Error(`Uploading slide ${index} returned HTTP ${uploaded.status}.`);
|
const code =
|
||||||
|
typeof uploaded.body?.code === "string" && /^SIGNAGE_[A-Z_]+$/u.test(uploaded.body.code)
|
||||||
|
? ` (${uploaded.body.code})`
|
||||||
|
: "";
|
||||||
|
throw new Error(`Uploading slide ${index} returned HTTP ${uploaded.status}${code}.`);
|
||||||
}
|
}
|
||||||
images.push(uploaded.body?.image?.id);
|
images.push(uploaded.body?.image?.id);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user