Disable cache storage on special route error responses

This commit is contained in:
kris
2026-04-07 14:08:27 +08:00
parent b1fa3c9b26
commit 233f61a649
4 changed files with 66 additions and 19 deletions

View File

@@ -2,6 +2,7 @@ import { createReadStream } from "node:fs";
import { stat } from "node:fs/promises";
import { Readable } from "node:stream";
import { NextRequest, NextResponse } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { canSessionAccessAttachmentProject } from "@/lib/boss-attachment-access";
import { getAttachmentById, getAttachmentStorageConfig, getMasterAgentTask, readState } from "@/lib/boss-data";
@@ -38,17 +39,17 @@ export async function GET(
const session = await requireRequestSession(request);
const taskTokenAccess = session ? false : await hasTaskTokenAccess(request, attachmentId);
if (!session && !taskTokenAccess) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const record = await getAttachmentById(attachmentId);
if (!record) {
return NextResponse.json({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 });
return jsonNoStore({ ok: false, message: "ATTACHMENT_NOT_FOUND" }, { status: 404 });
}
if (session) {
const state = await readState();
if (!canSessionAccessAttachmentProject(state, session, record.project)) {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
}
@@ -74,7 +75,7 @@ export async function GET(
? storageConfig.aliyunOss
: null);
if (!resolvedConfig) {
return NextResponse.json({ ok: false, message: "ATTACHMENT_STORAGE_CONFIG_NOT_FOUND" }, { status: 404 });
return jsonNoStore({ ok: false, message: "ATTACHMENT_STORAGE_CONFIG_NOT_FOUND" }, { status: 404 });
}
const signedUrl = await getAliyunOssSignedDownloadUrl(resolvedConfig, record.attachment.storagePath);
return NextResponse.redirect(signedUrl, {
@@ -84,19 +85,19 @@ export async function GET(
}
if (record.attachment.storageBackend !== "server_file") {
return NextResponse.json({ ok: false, message: "UNSUPPORTED_ATTACHMENT_STORAGE_BACKEND" }, { status: 501 });
return jsonNoStore({ ok: false, message: "UNSUPPORTED_ATTACHMENT_STORAGE_BACKEND" }, { status: 501 });
}
let absolutePath: string;
try {
absolutePath = resolveServerFileAttachmentAbsolutePath(record.attachment.storagePath);
} catch {
return NextResponse.json({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
return jsonNoStore({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
}
try {
await stat(absolutePath);
} catch {
return NextResponse.json({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
return jsonNoStore({ ok: false, message: "ATTACHMENT_FILE_NOT_FOUND" }, { status: 404 });
}
const stream = createReadStream(absolutePath);

View File

@@ -1,4 +1,5 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { subscribeBossEvents } from "@/lib/boss-events";
import { getAuditSummaryView, getConversationItems, getOpsSummaryView } from "@/lib/boss-projections";
@@ -13,10 +14,7 @@ function sseEvent(event: string, data: unknown) {
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return new Response(JSON.stringify({ ok: false, message: "UNAUTHORIZED" }), {
status: 401,
headers: { "Content-Type": "application/json; charset=utf-8" },
});
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const encoder = new TextEncoder();
let heartbeatTimer: ReturnType<typeof setInterval> | undefined;

View File

@@ -1,23 +1,18 @@
import { promises as fs } from "node:fs";
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { getPublishedOtaAsset } from "@/lib/boss-ota";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return new Response(JSON.stringify({ ok: false, message: "UNAUTHORIZED" }), {
status: 401,
headers: { "Content-Type": "application/json; charset=utf-8" },
});
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const asset = await getPublishedOtaAsset();
if (!asset) {
return new Response(JSON.stringify({ ok: false, message: "OTA_PACKAGE_NOT_FOUND" }), {
status: 404,
headers: { "Content-Type": "application/json; charset=utf-8" },
});
return jsonNoStore({ ok: false, message: "OTA_PACKAGE_NOT_FOUND" }, { status: 404 });
}
const content = await fs.readFile(asset.absolutePath);

View File

@@ -11,6 +11,7 @@ let AUTH_SESSION_COOKIE = "";
let getConversationHomeRoute: (typeof import("../src/app/api/v1/conversations/home/route"))["GET"];
let getConversationsRoute: (typeof import("../src/app/api/v1/conversations/route"))["GET"];
let getFolderRoute: (typeof import("../src/app/api/v1/conversation-folders/[folderKey]/route"))["GET"];
let getEventsRoute: (typeof import("../src/app/api/v1/events/route"))["GET"];
let getProjectDetailRoute: (typeof import("../src/app/api/v1/projects/[projectId]/route"))["GET"];
let getDevicesRoute: (typeof import("../src/app/api/v1/devices/route"))["GET"];
let getSettingsRoute: (typeof import("../src/app/api/v1/settings/route"))["GET"];
@@ -35,6 +36,8 @@ let getMasterAgentPromptRoute: (typeof import("../src/app/api/v1/master-agent/pr
let getMasterAgentMemoriesRoute: (typeof import("../src/app/api/v1/master-agent/memories/route"))["GET"];
let getStorageConfigRoute: (typeof import("../src/app/api/v1/storage/config/route"))["GET"];
let getAccountDetailRoute: (typeof import("../src/app/api/v1/accounts/[accountId]/route"))["GET"];
let getOtaPackageRoute: (typeof import("../src/app/api/v1/user/ota/package/route"))["GET"];
let getAttachmentDownloadRoute: (typeof import("../src/app/api/v1/attachments/[attachmentId]/download/route"))["GET"];
async function setup() {
if (runtimeRoot) return;
@@ -47,6 +50,7 @@ async function setup() {
homeRoute,
conversationsRoute,
folderRoute,
eventsRoute,
projectRoute,
devicesRoute,
settingsRoute,
@@ -71,6 +75,8 @@ async function setup() {
masterAgentMemoriesRoute,
storageConfigRoute,
accountDetailRoute,
otaPackageRoute,
attachmentDownloadRoute,
dataModule,
authModule,
] =
@@ -78,6 +84,7 @@ async function setup() {
import("../src/app/api/v1/conversations/home/route.ts"),
import("../src/app/api/v1/conversations/route.ts"),
import("../src/app/api/v1/conversation-folders/[folderKey]/route.ts"),
import("../src/app/api/v1/events/route.ts"),
import("../src/app/api/v1/projects/[projectId]/route.ts"),
import("../src/app/api/v1/devices/route.ts"),
import("../src/app/api/v1/settings/route.ts"),
@@ -102,6 +109,8 @@ async function setup() {
import("../src/app/api/v1/master-agent/memories/route.ts"),
import("../src/app/api/v1/storage/config/route.ts"),
import("../src/app/api/v1/accounts/[accountId]/route.ts"),
import("../src/app/api/v1/user/ota/package/route.ts"),
import("../src/app/api/v1/attachments/[attachmentId]/download/route.ts"),
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
]);
@@ -109,6 +118,7 @@ async function setup() {
getConversationHomeRoute = homeRoute.GET;
getConversationsRoute = conversationsRoute.GET;
getFolderRoute = folderRoute.GET;
getEventsRoute = eventsRoute.GET;
getProjectDetailRoute = projectRoute.GET;
getDevicesRoute = devicesRoute.GET;
getSettingsRoute = settingsRoute.GET;
@@ -133,6 +143,8 @@ async function setup() {
getMasterAgentMemoriesRoute = masterAgentMemoriesRoute.GET;
getStorageConfigRoute = storageConfigRoute.GET;
getAccountDetailRoute = accountDetailRoute.GET;
getOtaPackageRoute = otaPackageRoute.GET;
getAttachmentDownloadRoute = attachmentDownloadRoute.GET;
createAuthSession = dataModule.createAuthSession;
AUTH_SESSION_COOKIE = authModule.AUTH_SESSION_COOKIE;
}
@@ -163,6 +175,47 @@ function assertNoStoreHeader(response: Response) {
assert.equal(response.headers.get("Cache-Control"), "private, no-store, max-age=0");
}
test("event stream keeps SSE cache headers while unauthorized event JSON disables caching", async () => {
await setup();
const streamResponse = await getEventsRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/events"),
);
assert.equal(streamResponse.headers.get("Content-Type"), "text/event-stream; charset=utf-8");
assert.equal(streamResponse.headers.get("Cache-Control"), "no-cache, no-transform");
await streamResponse.body?.cancel();
const unauthorizedResponse = await getEventsRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/events"),
);
assert.equal(unauthorizedResponse.status, 401);
assertNoStoreHeader(unauthorizedResponse);
});
test("download error JSON responses disable cache storage", async () => {
await setup();
const otaUnauthorizedResponse = await getOtaPackageRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/user/ota/package"),
);
assert.equal(otaUnauthorizedResponse.status, 401);
assertNoStoreHeader(otaUnauthorizedResponse);
const attachmentUnauthorizedResponse = await getAttachmentDownloadRoute(
new NextRequest("http://127.0.0.1:3000/api/v1/attachments/missing/download"),
{ params: Promise.resolve({ attachmentId: "missing" }) },
);
assert.equal(attachmentUnauthorizedResponse.status, 401);
assertNoStoreHeader(attachmentUnauthorizedResponse);
const attachmentNotFoundResponse = await getAttachmentDownloadRoute(
await createAuthedRequest("http://127.0.0.1:3000/api/v1/attachments/missing/download"),
{ params: Promise.resolve({ attachmentId: "missing" }) },
);
assert.equal(attachmentNotFoundResponse.status, 404);
assertNoStoreHeader(attachmentNotFoundResponse);
});
test("live conversation and device routes disable cache storage", async () => {
await setup();