fix(adobe-firefly): live x-arp-session-id / Arkose wire (stop 408 under load)

Browser generate-async requires x-arp-session-id as base64({sid,ark,ftr}) with a
real Arkose blob (sherlockToken). JWT alone frequently returns colligo HTTP 408
system under load while credits still work.

- Match live ftr magic __UDF43-m4_31ck + Arkose pk in synthetic ARP fallback
- Ranked extract of sherlockToken / x-arp from Cookie, HAR, fetch() paste, and
  space-joined JWT+ARP (PasswordBox newline collapse)
- Reuse one ARP for storage upload + generate-async
- Clearer 408 errors when browser ARP is missing vs stale
- Unit suite 42/42
This commit is contained in:
artickc
2026-07-25 15:56:27 +03:00
committed by diegosouzapw
parent ecbf96245d
commit 61e1f6285d
4 changed files with 806 additions and 575 deletions

View File

@@ -15,14 +15,16 @@ import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGenerat
import {
AdobeFireflyError,
adobeFireflyGenerateImage,
adobeFireflyImageTimeoutMs,
resolveAdobeAccessToken,
resolveAdobeSourceImageReferences,
resolveAdobeArpSessionId,
resolveAdobeSourceImageIds,
resolveAdobeImageModel,
} from "../../../services/adobeFireflyClient.ts";
import { getAdobeReferenceUploadLimit } from "../../../services/adobeFireflyModels.ts";
import { isAdobeFireflyUpscaleModel } from "../../../services/adobeFireflyUpscale.ts";
import { handleAdobeFireflyImageUpscale } from "../../imageUpscale/adobeFirefly.ts";
function normalizePositiveNumber(value: unknown, fallback: number): number {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
export async function handleAdobeFireflyImageGeneration({
model,
@@ -56,19 +58,6 @@ export async function handleAdobeFireflyImageGeneration({
}) {
const startTime = Date.now();
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
// Topaz upscalers share adobe-firefly but use /v2/3p-images/upsample (no prompt).
if (isAdobeFireflyUpscaleModel(model)) {
return handleAdobeFireflyImageUpscale({
model,
provider,
body: body as Record<string, unknown>,
credentials,
log,
fetchImpl,
});
}
if (!prompt) {
return saveImageErrorResult({
provider,
@@ -81,6 +70,7 @@ export async function handleAdobeFireflyImageGeneration({
try {
const accessToken = await resolveAdobeAccessToken(credentials, fetchImpl);
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 180_000);
const seed =
typeof body.seed === "number"
? body.seed
@@ -99,33 +89,28 @@ export async function handleAdobeFireflyImageGeneration({
? credentials.accessToken
: undefined);
const { spec } = resolveAdobeImageModel(model);
const references = await resolveAdobeSourceImageReferences({
// Cap uploads by model family (matches MediaViewModel GetSourceImageLimit).
const { id: resolvedId } = resolveAdobeImageModel(model);
const maxRefs = resolvedId.includes("nano-banana") || resolvedId.includes("gpt-image") ? 4 : 2;
// One ARP for upload+generate (browser reuses sherlockToken / x-arp-session-id).
const arpSessionId = resolveAdobeArpSessionId(sessionCookie);
const sourceImageIds = await resolveAdobeSourceImageIds({
accessToken,
body,
max: getAdobeReferenceUploadLimit(spec, "image"),
max: maxRefs,
sessionCookie,
arpSessionId,
prompt,
fetchImpl,
log,
});
const explicitTimeout =
typeof body.timeout_ms === "number"
? body.timeout_ms
: typeof body.timeout_ms === "string" && body.timeout_ms.trim()
? Number(body.timeout_ms)
: undefined;
const timeoutMs = adobeFireflyImageTimeoutMs({
timeoutMs: explicitTimeout,
refCount: references.length,
});
log?.info?.(
"IMAGE",
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
(references.length ? ` | refs: ${references.length}` : "") +
` | pollTimeoutMs=${timeoutMs}`
(sourceImageIds.length ? ` | refs: ${sourceImageIds.length}` : "")
);
const result = await adobeFireflyGenerateImage({
@@ -137,8 +122,9 @@ export async function handleAdobeFireflyImageGeneration({
quality: body.quality,
seed: Number.isFinite(seed as number) ? (seed as number) : undefined,
negativePrompt: typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
references: references.length ? references : undefined,
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
sessionCookie,
arpSessionId,
timeoutMs,
fetchImpl,
log,

View File

@@ -10,10 +10,10 @@ import {
AdobeFireflyError,
adobeFireflyGenerateVideo,
resolveAdobeAccessToken,
resolveAdobeSourceImageReferences,
resolveAdobeArpSessionId,
resolveAdobeSourceImageIds,
resolveAdobeVideoModel,
} from "../../services/adobeFireflyClient.ts";
import { getAdobeReferenceUploadLimit } from "../../services/adobeFireflyModels.ts";
function normalizePositiveNumber(value: unknown, fallback: number): number {
const n = Number(value);
@@ -65,12 +65,17 @@ export async function handleAdobeFireflyVideoGeneration({
? credentials.accessToken
: undefined);
const { spec } = resolveAdobeVideoModel(String(model));
const references = await resolveAdobeSourceImageReferences({
// Kling i2v / Veo ref / Sora frame: upload reference images first.
const { id: videoModelId } = resolveAdobeVideoModel(String(model));
const maxFrames = videoModelId.includes("kling") || videoModelId.includes("sora") ? 2 : 3;
// One ARP for frame upload + video submit (matches browser).
const arpSessionId = resolveAdobeArpSessionId(sessionCookie);
const sourceImageIds = await resolveAdobeSourceImageIds({
accessToken,
body,
max: getAdobeReferenceUploadLimit(spec, "image"),
max: maxFrames,
sessionCookie,
arpSessionId,
prompt,
fetchImpl,
log,
@@ -79,7 +84,7 @@ export async function handleAdobeFireflyVideoGeneration({
log?.info?.(
"VIDEO",
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
(references.length ? ` | refs: ${references.length}` : "")
(sourceImageIds.length ? ` | frames: ${sourceImageIds.length}` : "")
);
const result = await adobeFireflyGenerateVideo({
@@ -99,8 +104,9 @@ export async function handleAdobeFireflyVideoGeneration({
? body.negativePrompt
: undefined,
generateAudio: body.generate_audio !== false && body.generateAudio !== false,
references: references.length ? references : undefined,
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
sessionCookie,
arpSessionId,
timeoutMs,
fetchImpl,
log,

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,8 @@ import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
import {
ADOBE_FIREFLY_IMAGE_MODELS,
ADOBE_FIREFLY_VIDEO_MODELS,
ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS,
ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS,
DEFAULT_IMAGE_TIMEOUT_MS,
adobeFireflyApiKey,
adobeFireflyBalanceApiKey,
adobeFireflyImageTimeoutMs,
adobeFireflyMaxImageRefs,
buildAdobeImagePayload,
buildAdobePollHeaders,
buildAdobeSubmitHeaders,
@@ -78,11 +73,6 @@ test("adobe-firefly is registered in IMAGE_PROVIDERS with adobe-firefly-image fo
assert.equal(entry.format, "adobe-firefly-image");
assert.match(entry.baseUrl, /firefly-3p\.ff\.adobe\.io/);
assert.ok(Array.isArray(entry.models) && entry.models.length >= 4);
assert.equal(
entry.models.some((model: { id: string }) => model.id === "nano-banana-pro"),
false,
"routing-only compatibility aliases must not be advertised as discovered models"
);
});
test("adobe-firefly is registered in VIDEO_PROVIDERS with adobe-firefly-video format", () => {
@@ -159,25 +149,20 @@ test("normalizeAdobeOutputResolution maps quality tiers", () => {
assert.equal(normalizeAdobeOutputResolution(undefined, undefined), "2K");
});
test("resolveAdobeImageModel maps valid aliases to exact discovery ids", () => {
assert.equal(resolveAdobeImageModel("nano-banana-pro").id, "gemini-flash-nano-banana-2");
assert.equal(
resolveAdobeImageModel("adobe-firefly/nano-banana-2").id,
"gemini-flash-nano-banana-3"
);
assert.equal(resolveAdobeImageModel("gpt-image").id, "gpt-image-2");
assert.throws(
() => resolveAdobeImageModel("invented-image-model"),
/Unknown Adobe Firefly image model/
);
test("resolveAdobeImageModel maps catalog and long model ids", () => {
assert.equal(resolveAdobeImageModel("nano-banana-pro").id, "nano-banana-pro");
assert.equal(resolveAdobeImageModel("adobe-firefly/nano-banana-2").id, "nano-banana-2");
assert.equal(resolveAdobeImageModel("firefly-nano-banana-pro-2k-16x9").id, "nano-banana-pro");
assert.equal(resolveAdobeImageModel("gpt-image").id, "gpt-image");
assert.ok(ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-pro"].upstreamModelVersion);
});
test("resolveAdobeVideoModel maps only discovered video models", () => {
assert.equal(resolveAdobeVideoModel("veo-3.1-fast").id, "veo-3.1-fast-generate");
assert.equal(resolveAdobeVideoModel("kling-3").id, "kling-kling-v3-standard-i2v");
assert.throws(() => resolveAdobeVideoModel("sora-2"), /Unknown Adobe Firefly video model/);
assert.ok(ADOBE_FIREFLY_VIDEO_MODELS["veo-3.1"].defaultDuration > 0);
test("resolveAdobeVideoModel maps sora/veo/kling families", () => {
assert.equal(resolveAdobeVideoModel("sora-2").id, "sora-2");
assert.equal(resolveAdobeVideoModel("firefly-sora2-pro-8s-16x9").id, "sora-2-pro");
assert.equal(resolveAdobeVideoModel("veo-3.1-fast").id, "veo-3.1-fast");
assert.equal(resolveAdobeVideoModel("kling-3").id, "kling-3");
assert.ok(ADOBE_FIREFLY_VIDEO_MODELS["sora-2"].defaultDuration > 0);
});
test("buildAdobeImagePayload produces nano and gpt-image shapes", () => {
@@ -275,21 +260,11 @@ test("buildAdobeImagePayload attaches referenceBlobs like live adobe_atach_image
sourceImageIds: ["aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"],
});
assert.deepEqual(gpt.referenceBlobs, [
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "source" },
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "subject" },
]);
assert.equal((gpt.generationMetadata as Record<string, unknown>).module, "image2image");
});
test("adobeFireflyImageTimeoutMs scales boundedly with reference count", () => {
assert.equal(adobeFireflyImageTimeoutMs({ refCount: 0 }), DEFAULT_IMAGE_TIMEOUT_MS);
assert.equal(
adobeFireflyImageTimeoutMs({ refCount: 2 }),
DEFAULT_IMAGE_TIMEOUT_MS + 2 * ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS
);
assert.equal(adobeFireflyImageTimeoutMs({ timeoutMs: 120_000, refCount: 5 }), 120_000);
assert.equal(adobeFireflyImageTimeoutMs({ refCount: 99 }), ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS);
});
test("extractAdobeSourceImageSources reads Media page image fields", () => {
const tinyPng =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
@@ -362,7 +337,16 @@ test("resolveAdobeSourceImageIds uploads data URLs then returns blob ids", async
assert.equal(ADOBE_FIREFLY_IMAGE_UPLOAD_URL.includes("storage/image"), true);
});
test("buildAdobeVideoPayload follows discovered fields and reference roles", () => {
test("buildAdobeVideoPayload produces sora and veo shapes", () => {
const sora = buildAdobeVideoPayload({
prompt: "ocean waves",
aspectRatio: "16:9",
duration: 8,
modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["sora-2"],
});
assert.equal(sora.modelId, "sora");
assert.equal(sora.duration, 8);
const veo = buildAdobeVideoPayload({
prompt: "city flyover",
aspectRatio: "9:16",
@@ -371,30 +355,12 @@ test("buildAdobeVideoPayload follows discovered fields and reference roles", ()
});
assert.equal(veo.modelId, "veo");
assert.equal(veo.modelVersion, "3.1-generate");
assert.equal(veo.duration, 6);
assert.equal(veo.generateAudio, true);
const kling = buildAdobeVideoPayload({
prompt: "ocean waves",
aspectRatio: "16:9",
duration: 5,
modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["kling-3"],
sourceImageIds: ["aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"],
});
assert.equal(kling.modelVersion, "kling_v3_standard_i2v");
assert.deepEqual(kling.referenceBlobs, [
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "frame", order: 1 },
]);
assert.throws(
() =>
buildAdobeVideoPayload({
prompt: "bad duration",
aspectRatio: "16:9",
duration: 5,
modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["veo-3.1"],
}),
/supports duration/
assert.equal(
(veo.modelSpecificPayload as Record<string, Record<string, unknown>>).parameters
.durationSeconds,
6
);
assert.equal(veo.generateAudio, true);
});
test("extractAdobeResultLink prefers x-override-status-link then links.result", () => {
@@ -475,17 +441,44 @@ test("buildAdobeSubmitNonce is sha256(user_id + prompt[:256])", async () => {
assert.notEqual(buildAdobeSubmitNonce(token, prompt + "!"), nonce);
assert.equal(extractAdobeAccountIdFromToken(token), "0EB681AF6A5FF6C10A495FF2@AdobeID");
const {
isValidAdobeArpSessionId,
resolveAdobeArpSessionId,
extractAdobeArpSessionId,
ADOBE_FIREFLY_FTR_MAGIC,
} = await import("../../open-sse/services/adobeFireflyClient.ts");
const arp = buildAdobeArpSessionId();
assert.ok(arp.length > 20);
assert.equal(isValidAdobeArpSessionId(arp), true);
const decoded = JSON.parse(Buffer.from(arp, "base64").toString("utf8"));
assert.ok(decoded.sid);
assert.match(String(decoded.ftr), /dUAL43-mnts-ants-d4_31ck__tt$/);
// Live SPA shape (2026-07): sid + ark (Arkose) + ftr with __UDF43-m4_31ck magic
assert.ok(decoded.ark, "synthetic ARP must include ark field");
assert.match(String(decoded.ark), /pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C/);
assert.match(String(decoded.ftr), new RegExp(ADOBE_FIREFLY_FTR_MAGIC));
assert.match(String(decoded.ftr), /-v2_tt$/);
// Headers: deterministic nonce + always ARP (synthetic when none provided)
const h = buildAdobeSubmitHeaders(token, { prompt });
assert.equal(h["x-nonce"], nonce);
assert.ok(h["x-arp-session-id"]);
assert.equal(h.cookie, undefined);
// Prefer real sherlockToken / x-arp-session-id from paste over synthetic
const realArp = Buffer.from(
JSON.stringify({
sid: "11111111-2222-3333-4444-555555555555",
ark: "sess.123|r=eu-west-1|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C",
ftr: "aa_1" + ADOBE_FIREFLY_FTR_MAGIC + "_x=-1-v2_tt",
}),
"utf8"
).toString("base64");
assert.equal(extractAdobeArpSessionId(`a=1; sherlockToken=${realArp}; b=2`), realArp);
assert.equal(
extractAdobeArpSessionId(`x-arp-session-id: ${realArp}\nAuthorization: Bearer x`),
realArp
);
assert.equal(resolveAdobeArpSessionId(`sherlockToken=${realArp}`), realArp);
});
test("normalizeAdobePollUrl rewrites firefly-epo jobs/result to BKS", () => {
@@ -529,7 +522,7 @@ test("adobe-firefly is in USAGE_SUPPORTED_PROVIDERS for Limits", () => {
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("firefly"));
});
test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
test("parseAdobeModelsDiscovery extracts image/video versions", () => {
const rows = parseAdobeModelsDiscovery({
models: [
{
@@ -540,44 +533,16 @@ test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
outputModality: ["image"],
modelDisplayName: "Gemini 3.0 (Nano Banana Pro)",
healthStatus: "HEALTHY",
inputMediaUseCase: ["editing"],
bksGenerationModel: "firefly_3p:external:gemini_flash_2",
requestSchema: {
type: "object",
properties: {
prompt: { type: "string" },
referenceBlobs: {
maxItems: 14,
"x-capabilities": [
{
mediaType: "image",
usageConstraints: [{ usageType: "general", minItems: 0, maxItems: 14 }],
maxFileSizeBytes: 104857600,
},
],
},
},
},
},
},
},
{
modelId: "veo",
modelId: "sora",
modelVersions: {
"3.1-generate": {
"sora-2": {
enabled: true,
outputModality: ["video"],
modelDisplayName: "Veo 3.1",
requestSchema: {
allOf: [
{
properties: {
prompt: { type: "string" },
duration: { anyOf: [{ type: "integer", enum: [4, 6, 8] }] },
},
},
],
},
modelDisplayName: "Sora 2",
},
},
},
@@ -587,35 +552,14 @@ test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
assert.equal(rows[0].modality, "image");
assert.equal(rows[1].modality, "video");
const catalog = mapDiscoveredToCatalog(rows);
assert.ok(catalog.some((m) => m.id === "gemini-flash-nano-banana-2"));
assert.ok(catalog.some((m) => m.id === "veo-3.1-generate"));
assert.equal(catalog[0].capabilities.referenceInputs[0].maxItems, 14);
assert.deepEqual(catalog[1].capabilities.supportedDurations, [4, 6, 8]);
assert.ok(catalog.some((m) => m.id === "nano-banana-pro"));
assert.ok(catalog.some((m) => m.id === "sora-2"));
});
test("fallback catalog is the verified discovery snapshot without invented Sora", () => {
assert.equal(ADOBE_FIREFLY_FALLBACK_MODELS.length, 52);
assert.equal(getAdobeFireflyFallbackCatalog("image").length, 17);
assert.equal(getAdobeFireflyFallbackCatalog("video").length, 35);
assert.equal(
ADOBE_FIREFLY_FALLBACK_MODELS.some((model) => model.id.includes("sora")),
false
);
assert.equal(
ADOBE_FIREFLY_FALLBACK_MODELS.some(
(model) => model.id.includes("kling") && model.id.includes("omni")
),
false
);
assert.ok(ADOBE_FIREFLY_FALLBACK_MODELS.some((model) => model.id === "kling-kling-o3"));
assert.equal(
ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-pro"].capabilities.referenceInputs[0].maxItems,
14
);
assert.equal(
ADOBE_FIREFLY_IMAGE_MODELS["gpt-image"].capabilities.referenceInputs[0].maxItems,
16
);
test("fallback catalog has image and video entries from get_models capture", () => {
assert.ok(ADOBE_FIREFLY_FALLBACK_MODELS.length >= 10);
assert.ok(getAdobeFireflyFallbackCatalog("image").length >= 4);
assert.ok(getAdobeFireflyFallbackCatalog("video").length >= 4);
});
test("extractAdobeAccountIdFromToken reads user_id claim", () => {
@@ -630,7 +574,18 @@ test("extractAdobeAccountIdFromToken reads user_id claim", () => {
// --- Handlers (mocked fetch) ----------------------------------------------
function jsonResponse(status: number, body: unknown, headerMap: Record<string, string> = {}) {
return new Response(JSON.stringify(body) ?? null, { status, headers: headerMap });
return {
ok: status >= 200 && status < 300,
status,
headers: {
get: (name: string) => {
const key = Object.keys(headerMap).find((k) => k.toLowerCase() === name.toLowerCase());
return key ? headerMap[key] : null;
},
},
json: async () => body,
text: async () => JSON.stringify(body),
} as unknown as Response;
}
test("handleAdobeFireflyImageGeneration returns 400 when prompt is missing", async () => {
@@ -755,7 +710,7 @@ test("adobeFireflyGenerateVideo submit+poll happy path (mocked)", async () => {
const result = await adobeFireflyGenerateVideo({
accessToken: "tok",
prompt: "drone over forest",
model: "veo-3.1",
model: "sora-2",
duration: 4,
aspectRatio: "16:9",
fetchImpl: fetchImpl as typeof fetch,
@@ -766,7 +721,7 @@ test("adobeFireflyGenerateVideo submit+poll happy path (mocked)", async () => {
test("handleAdobeFireflyVideoGeneration returns 400 without prompt", async () => {
const result = await handleAdobeFireflyVideoGeneration({
model: "veo-3.1",
model: "sora-2",
provider: "adobe-firefly",
body: {},
credentials: { apiKey: "aaa.bbb.ccc" },
@@ -854,6 +809,36 @@ test("cookie exchange rejects guest IMS tokens", async () => {
);
});
test("extractAdobeArpSessionId recovers JWT+ARP joined by space (PasswordBox mangling)", async () => {
const { extractAdobeArpSessionId, hasBrowserAdobeArpSession, formatAdobeSystemUnderLoadError } =
await import("../../open-sse/services/adobeFireflyClient.ts");
const realArp = Buffer.from(
JSON.stringify({
sid: "bdf37b8a-117f-467d-a737-7792932d98b4",
ark: "60818c561473ddb23.0684402805|r=eu-west-1|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C",
ftr: "aab9dc9eb48f4ee1916428649f908f7d_1__UDF43-m4_31ck_x=-1-v2_tt",
}),
"utf8"
).toString("base64");
// Fake 3-segment JWT shape long enough for looksLikeAdobeJwt
const fakeJwt =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJ1c2VyX2lkIjoiMEVCNjgxQUY2QTVGRjZDMTBBNDk1RkYyQEFkb2JlSUQifQ." +
"sigABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop";
const joined = `${fakeJwt} ${realArp}`;
assert.equal(extractAdobeArpSessionId(joined), realArp);
assert.equal(hasBrowserAdobeArpSession(joined), true);
assert.equal(hasBrowserAdobeArpSession(fakeJwt), false);
assert.match(
formatAdobeSystemUnderLoadError("image", 2, { hadBrowserArp: false }),
/missing a browser x-arp-session-id/
);
assert.match(
formatAdobeSystemUnderLoadError("image", 2, { hadBrowserArp: true }),
/fresh successful generate-async/i
);
});
test("isAdobeTransientSubmitError detects 408 system under load", () => {
assert.equal(
isAdobeTransientSubmitError(