fix: render failed-response retry refusals as readable text
Decode only Retry's HTTP 400 error field for the existing banner. Preserve raw model/key refusal envelopes and other submission-error handling. Verified on the accepted-main integration: 2424 full-suite tests, typecheck, full ESLint, production build and isolated synthetic production-browser Retry/Submit smoke. No real provider calls.
This commit is contained in:
@@ -715,10 +715,17 @@ export class SessionModel {
|
|||||||
throw new Error("A turn is already running — try again in a moment.");
|
throw new Error("A turn is already running — try again in a moment.");
|
||||||
}
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// Refusal envelopes (503 BYOK/key gates) and 400s: the RAW body
|
// Keep model/key refusal envelopes verbatim: the page's refusal
|
||||||
// text rides the error message — the banner shows it, and the
|
// flow parses them. Retry's 400s are ordinary user-facing
|
||||||
// page's refusal flow JSON-parses the envelope out of it.
|
// guidance, so show their error text without JSON syntax.
|
||||||
throw new Error((await res.text()) || `The send failed (${res.status}).`);
|
let message = (await res.text()) || `The send failed (${res.status}).`;
|
||||||
|
if (input.type === "retry" && res.status === 400) {
|
||||||
|
try {
|
||||||
|
const refusal = JSON.parse(message) as { error?: unknown } | null;
|
||||||
|
if (typeof refusal?.error === "string" && refusal.error.length > 0) message = refusal.error;
|
||||||
|
} catch { /* Non-envelope failures keep their original text. */ }
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
// CONNECTION DIET B (owner, 2026-10-10 — parallel-session freeze
|
// CONNECTION DIET B (owner, 2026-10-10 — parallel-session freeze
|
||||||
// postmortem): the POST acknowledges and closes; there is no stream
|
// postmortem): the POST acknowledges and closes; there is no stream
|
||||||
|
|||||||
@@ -487,11 +487,27 @@ describe("the Retry button's client path", () => {
|
|||||||
});
|
});
|
||||||
await m.send({ type: "retry" });
|
await m.send({ type: "retry" });
|
||||||
assert.equal(m.getState().status, "error");
|
assert.equal(m.getState().status, "error");
|
||||||
assert.equal(JSON.parse(m.getState().error!.message).error, refused, "the existing refusal-envelope path carries both messages");
|
assert.equal(m.getState().error!.message, refused, "the banner keeps both messages as readable text, without JSON syntax");
|
||||||
assert.deepEqual(m.getState().nativeMessages, compacted);
|
assert.deepEqual(m.getState().nativeMessages, compacted);
|
||||||
assert.equal(endsInFailedTurn(m.getState().nativeMessages), false);
|
assert.equal(endsInFailedTurn(m.getState().nativeMessages), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const { status, body, expected } of [
|
||||||
|
{ status: 400, body: '{"error":"retry input requires a failed turn to retry"}', expected: "retry input requires a failed turn to retry" },
|
||||||
|
{ status: 400, body: "Plain refusal", expected: "Plain refusal" },
|
||||||
|
{ status: 400, body: '{"reason":"not an error envelope"}', expected: '{"reason":"not an error envelope"}' },
|
||||||
|
{ status: 503, body: '{"error":"model gate","code":"models_not_configured"}', expected: '{"error":"model gate","code":"models_not_configured"}' },
|
||||||
|
]) {
|
||||||
|
it(`preserves the appropriate Retry refusal format for ${status}: ${body}`, async () => {
|
||||||
|
const m = new SessionModel({
|
||||||
|
chatId: "chat_x", initialMessages: [newUserMessage("hello"), failed("failed-response")],
|
||||||
|
fetch: async () => new Response(body, { status }),
|
||||||
|
});
|
||||||
|
await m.send({ type: "retry" });
|
||||||
|
assert.equal(m.getState().error?.message, expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("after a dropped request, Retry posts straight from the error status", async () => {
|
it("after a dropped request, Retry posts straight from the error status", async () => {
|
||||||
const failedTurn = [newUserMessage("hello"), failed("a1")];
|
const failedTurn = [newUserMessage("hello"), failed("a1")];
|
||||||
const posted: string[] = [];
|
const posted: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user