Disable cache storage on special route error responses
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user