ข้ามไปยังเนื้อหา

รีวิวเคสจริง 3: dashboard แบบ on-demand ที่มี middleware, session, และ env var

โจทย์ที่ให้ AI คือ “สร้าง dashboard แบบ on-demand: middleware ที่อ่าน session แล้วเอา user ไปไว้บน locals, login endpoint, หน้า dashboard ที่อ่าน cart จาก session, หน้า metrics ที่ cache ไว้, และ script ฝั่ง client ที่ report analytics key” ผลลัพธ์ด้านล่างคือแบบร่างที่ AI มักส่งกลับมา — middleware ดูเหมือนตัวอย่าง auth ของทุก tutorial, login endpoint ตั้งค่า session, และหน้า metrics ก็มี config เรื่อง caching อยู่ในนั้น อ่านตามลำดับการอ่านที่ index ของโมดูลนี้แนะนำ: rendering mode กับ adapter ก่อน แล้วค่อย server-side Astro แล้วก็ security แล้วก็ performance กับ caching โค้ดนี้มี บั๊กจริง 7 จุด ตรงกับข้อเท็จจริงที่พิสูจน์ไว้แล้วในบทเรียน SSR & Adapters, Middleware & Sessions, และ Caching & Edge ของคอร์สนี้ — ดู AI bug catalog สำหรับ pattern ทั่วไปเบื้องหลังแต่ละจุด

// astro.config.mjs — as generated
import { defineConfig, memoryCache } from 'astro/config';
export default defineConfig({
output: 'server',
cache: { provider: memoryCache() },
routeRules: {
'/dash-metrics': { maxAge: 20 },
},
});
// src/middleware.ts — as generated
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const user = (await context.session?.get('user')) ?? null;
// BUG: reassigns locals instead of setting a property on it
context.locals = { user };
return next();
});
// src/pages/api/dash-login.ts — as generated
import type { APIRoute } from 'astro';
export const prerender = false;
export const POST: APIRoute = async ({ session }) => {
await session?.set('user', { id: 'u1' });
// BUG: no session.regenerate() after establishing the authenticated session
return Response.json({ ok: true });
};
---
// src/pages/dash-shell.astro — as generated
export const prerender = true;
const cart = (await Astro.session?.get('cart')) ?? [];
Astro.response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
---
<p>{cart.length} items in your cart</p>
---
// src/pages/dash-metrics.astro — as generated
export const prerender = false;
const id = crypto.randomUUID();
---
<p>Request id: {id}</p>
---
// src/pages/dashboard.astro — as generated
export const prerender = false;
---
<html>
<head><title>Dashboard</title></head>
<body>
<p id="key"></p>
<script>
const key = import.meta.env.ANALYTICS_KEY;
document.getElementById('key')!.textContent = `key: ${key}`;
</script>
</body>
</html>

config ตั้งค่า output: 'server' — ทุก route เป็น on-demand โดยค่าเริ่มต้น — แต่ไม่เคยติดตั้ง adapter เลย

🐛 หา bug ในโค้ดนี้

export default defineConfig({
output: 'server',
cache: { provider: memoryCache() },
// (no adapter)
});
เฉลย

การ render แบบ on-demand รันโค้ด component ต่อ request ซึ่งต้องมี runtime ให้รันจริง ๆ — runtime นั้นคือสิ่งที่ adapter เป็นคนให้มา ถ้าไม่มี adapter Astro จะไม่เดา platform ปลายทางให้ มันปฏิเสธ build ไปเลย พิสูจน์ด้วย astro build จริงกับ config นี้ตรง ๆ ที่มีหน้าซึ่งอ่าน Astro.params/ข้อมูล session (มี export const prerender = false แต่ไม่มี adapter ติดตั้งไว้):

[NoAdapterInstalled] Cannot use server-rendered pages without an adapter. Please install and configure the appropriate server adapter for your final deployment.
Hint:
See https://docs.astro.build/en/guides/on-demand-rendering/ for more information.

นี่คือความล้มเหลวแบบแข็ง ที่บล็อก build ไปเลย ไม่ใช่แค่ warning — บั๊กอื่น ๆ ในรีวิวนี้จะเข้าถึงได้ก็ต่อเมื่อมี adapter จริง (@astrojs/node ในเวอร์ชันที่แก้แล้ว) ถูกติดตั้งและตั้งค่าไว้แล้วเท่านั้น

middleware อ่าน user ที่มาจาก session แล้วทำ context.locals = { user } — การ reassign ทั้ง object locals ตรง ๆ

🐛 หา bug ในโค้ดนี้

export const onRequest = defineMiddleware(async (context, next) => {
const user = (await context.session?.get('user')) ?? null;
context.locals = { user };
return next();
});
เฉลย

context.locals ถูกออกแบบมาให้ reassign ไม่ได้โดยตั้งใจ — property locals ของ Astro เองมี setter ที่ throw เสมอไม่มีเงื่อนไข เพื่อให้ middleware ตัวถัดไปใน chain ของ sequence() ไม่มีทางเผลอลบสิ่งที่ตัวก่อนหน้าตั้งไว้แล้วได้ พิสูจน์ด้วย build จริงกับ middleware นี้ตรง ๆ:

[LocalsReassigned] `locals` cannot be assigned directly.
Hint:
Set a `locals` property instead.
Error reference:
https://docs.astro.build/en/reference/errors/locals-reassigned/

ทุก request เดียวที่ไหลผ่าน middleware นี้ fail ด้วย error นี้ — ไม่ใช่บั๊กที่เกิดเป็นบางครั้งแบบแอบแฝง มันคือ fail ทั้งหมด ทางแก้คือ Object.assign(context.locals, { user }) (หรือ set ทีละ property context.locals.user = user) — พิสูจน์ด้วย build เดียวกันเป๊ะที่ผ่านสำเร็จทันทีที่เปลี่ยนการ reassign เป็นการ merge

dash-shell.astro ตั้ง export const prerender = true (หน้า static ที่ build ครั้งเดียว) แต่ยังอ่าน Astro.session เพื่อเอา cart ของ user อยู่

🐛 หา bug ในโค้ดนี้

export const prerender = true;
const cart = (await Astro.session?.get('cart')) ?? [];
เฉลย

session ถูก resolve จาก cookie ของ request จริง ๆ ต่อครั้ง — หน้าที่ prerender รันแค่ครั้งเดียวตอน build โดยไม่มี request ของผู้เข้าชมจริงอยู่เบื้องหลังเลย พิสูจน์ด้วย build จริงของหน้านี้ตรง ๆ (มี adapter ติดตั้ง เปิด session ใช้งาน):

[WARN] `Astro.request.headers` was used when rendering the route `src/pages/dash-shell.astro'`. `Astro.request.headers` is not available on prerendered pages.

นี่ไม่ใช่ error ที่บล็อก build — build เสร็จสมบูรณ์ และหน้าก็ถูก ship ออกไป แต่มันเป็นบั๊กด้าน behavior แบบเงียบ ๆ: การอ่าน session บนหน้าที่ prerender ไม่เคยเห็น cart จริงเลย พิสูจน์ด้วย HTML ที่ build จริงของหน้านี้ตรง ๆ:

<!DOCTYPE html><p>0 items</p>

0 items เสมอ ถูก bake เข้าไปในไฟล์ static ตอน build ครั้งเดียว สำหรับผู้เข้าชมทุกคน ตลอดไป — ข้อเท็จจริงเดียวกับที่บทเรียน Rendering & Output Modes ของคอร์สนี้พิสูจน์ไว้แล้วสำหรับ Astro.cookies/Astro.session/Astro.request.headers บน route ที่ prerender: build warning จริง และค่าที่กลับมาว่างเปล่า แทนที่จะเป็นข้อมูลตอน request จริงที่โค้ดสมมติว่าจะได้ ทางแก้คือ export const prerender = false (พร้อม adapter ที่ตั้งค่าไว้) เพื่อให้หน้านี้รันต่อ request จริง ๆ โดยมี session จริงให้อ่าน

ไฟล์เดียวกันยังเรียก Astro.response.headers.set('Cache-Control', ...) — บนหน้าที่ prerender

🐛 หา bug ในโค้ดนี้

Astro.response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
เฉลย

Astro.response.headers แก้ไข response ขาออกของ Response จริงต่อ request — หน้าที่ prerender ไม่มี response แบบนั้นเลย มันเป็นแค่ไฟล์ธรรมดาที่เขียนลงดิสก์ครั้งเดียวตอน build call นี้ build ผ่านโดยไม่มี error หรือ warning เลย แต่มันเป็น no-op สมบูรณ์ พิสูจน์ด้วยไฟล์ที่ build จริงทั้งไฟล์ของหน้านี้ตรง ๆ — ไม่มี header sidecar เลย (ไม่มีไฟล์ _headers ไม่มีอะไรเลย) อยู่ที่ไหนใน output:

<!-- the entire built file for dash-shell — no header sidecar anywhere in dist/ -->
<!DOCTYPE html><p>0 items</p>

ถ้า route ที่ prerender ต้องการ caching header จริง ๆ นั่นเป็นเรื่องของ hosting/CDN (ไฟล์ _headers สำหรับ Netlify, rule เฉพาะ platform) — Astro เองเดินสาย Astro.response.headers ให้แค่กับ route แบบ on-demand เท่านั้น ที่มี Response จริงต่อ request ให้แนบเข้าไป พอแก้หน้านี้ให้เป็น on-demand แล้ว (ข้อค้นพบก่อนหน้า) call Astro.response.headers.set(...) เดิมเป๊ะ ๆ ก็จะเริ่มทำงานจริง

script ฝั่ง client ของ dashboard.astro อ่าน import.meta.env.ANALYTICS_KEY ตรง ๆ แล้วเขียนลงหน้า

🐛 หา bug ในโค้ดนี้

<script>
const key = import.meta.env.ANALYTICS_KEY;
document.getElementById('key')!.textContent = `key: ${key}`;
</script>
เฉลย

การจัดการ env ของ Astro สำหรับ <script> ฝั่ง client ธรรมดา (ไม่ใช่ astro:env) จะ inline ให้แค่ตัวแปรที่ชื่อขึ้นต้นด้วย PUBLIC_ เท่านั้น — ที่เหลือเป็น server-only ทั้งหมด นี่ไม่ใช่การรั่วไหลและไม่ใช่ build error มันคือ undefined (void 0) ที่ถูกแทนที่แบบ static ตอน build อย่างเงียบ ๆ สำหรับชื่อที่ไม่มี PUBLIC_ ใด ๆ พิสูจน์ตรง ๆ ในบทเรียน Project & Astro Files ของคอร์สนี้ด้วย script ที่ build จริง ANALYTICS_KEY ไม่มี prefix PUBLIC_ หน้าที่ ship ออกไปจึงอ่านได้:

key: undefined

…สำหรับผู้เข้าชมทุกคน ไม่ว่า ANALYTICS_KEY จะถูกตั้งค่าจริงเป็นอะไรใน .env ก็ตาม — analytics integration ไม่เคยได้รับ key จริงเลยแบบเงียบ ๆ และไม่มีอะไรใน build บอกเลยว่ามีปัญหา ทางแก้ขึ้นกับว่าค่านี้คืออะไรจริง ๆ: ถ้าตั้งใจให้เป็น public ก็เปลี่ยนชื่อเป็น PUBLIC_ANALYTICS_KEY; ถ้าเป็น secret จริง (API token ที่ห้ามถึง browser เด็ดขาด) ก็ไม่ควรอ่านจากโค้ดฝั่ง client เลย — สร้างค่าที่หมดอายุเร็วและจำกัดขอบเขตฝั่ง server แล้วส่งแค่ค่านั้นแทน

dash-login.ts ตั้ง session.set('user', ...) เมื่อ login สำเร็จ แต่ไม่เคยหมุน session id เลย

🐛 หา bug ในโค้ดนี้

export const POST: APIRoute = async ({ session }) => {
await session?.set('user', { id: 'u1' });
return Response.json({ ok: true });
};
เฉลย

การใช้ session id เดิมข้าม login คือ session fixation: ถ้าผู้ไม่หวังดีทำให้เหยื่อพก session id ที่ผู้ไม่หวังดีรู้อยู่แล้ว (cookie ที่ปลูกไว้ล่วงหน้า หรือดักจับไว้ก่อน login) และแอปไม่เคยหมุน id หลังยืนยันตัวตน id เดิมนั้นก็กลายเป็น session ที่ authenticated ถูกต้องแล้ว — สำหรับผู้ไม่หวังดีด้วย พิสูจน์แบบครบวงจรกับ handler นี้ตรง ๆ: สร้าง session id จริงที่ backed ด้วย storage แบบ anonymous ก่อน แล้ว login พร้อมยื่น id เดิมนั้น โดยไม่มีการเรียก regenerate():

attacker-known real session id (before login): ffb428e9-457a-4353-9a0f-9f78557742bd
$ curl -s -D - -b "astro-session=ffb428e9-..." -H "Origin: ..." http://127.0.0.1:5201/api/dash-login -X POST
HTTP/1.1 200 OK
set-cookie: astro-session=ffb428e9-457a-4353-9a0f-9f78557742bd; Path=/; HttpOnly; Secure; SameSite=Lax

id เดิมเป๊ะ ๆ กลับมาหลัง login — cookie ที่ผู้ไม่หวังดีปลูกไว้ตอนนี้เป็น session ที่ authenticated แล้ว ทางแก้คือหนึ่งบรรทัด await session?.regenerate() เรียกทันทีหลัง session ได้รับสิทธิ์:

attacker-known real session id (before login): dfa91e28-daab-41dd-b405-e79a0514717f
$ curl -s -D - -b "astro-session=dfa91e28-..." -H "Origin: ..." http://127.0.0.1:5201/api/dash-login -X POST
HTTP/1.1 200 OK
set-cookie: astro-session=8efedc94-76bf-4757-81a7-a920775d6104; Path=/; HttpOnly; Secure; SameSite=Lax

id ใหม่เอี่ยม — cookie ที่ปลูกไว้ไม่พก session ที่ authenticated ใด ๆ อีกเลย

dash-metrics.astro เป็น on-demand และ astro.config.mjs ตั้ง routeRules: { '/dash-metrics': { maxAge: 20 } } คู่กับ cache.provider — อ่านราวกับว่า config นี้อย่างเดียวทำให้ browser (หรือ CDN ปลายทาง) cache response ได้แล้ว

🐛 หา bug ในโค้ดนี้

routeRules: {
'/dash-metrics': { maxAge: 20 },
},
เฉลย

routeRules/cache ตั้งค่า cache layer ของ Astro เอง — cache ที่ Astro ตรวจสอบก่อนจะรันหน้าซ้ำ โดยใช้ header คู่ CDN-Cache-Control/Cache-Tag ภายในที่ถูกลบทิ้งก่อนที่ response จะไปถึง client จริง มันไม่เคยตั้ง header Cache-Control ที่ browser เห็นเลย พิสูจน์ด้วย request จริงสองครั้งกับ route นี้ตรง ๆ ที่ render id สุ่มใหม่ทุกครั้งที่ render:

$ curl -s -D - http://127.0.0.1:5201/dash-metrics/
HTTP/1.1 200 OK
content-type: text/html
x-astro-cache: MISS
<p>568a5dd8-bee2-4628-a072-ebf882ca827a</p>
$ curl -s -D - http://127.0.0.1:5201/dash-metrics/
HTTP/1.1 200 OK
content-type: text/html
x-astro-cache: HIT
<p>568a5dd8-bee2-4628-a072-ebf882ca827a</p>

x-astro-cache เปลี่ยนจาก MISS → HIT และ UUID เดิมเป๊ะกลับมาทั้งสองครั้ง — cache ของ Astro เองส่ง request ที่สองให้จริง ๆ โดยไม่ render ซ้ำ แต่ดู header ดี ๆ: ไม่มี Cache-Control (หรือ CDN-Cache-Control) ปรากฏในทั้งสอง response เลย browser หรือ CDN ที่อยู่หน้าเซิร์ฟเวอร์นี้ไม่รู้เลยว่า route นี้ cache ได้ ทางแก้ ถ้า browser/CDN ควร cache response นี้ด้วย คือตั้ง Cache-Control เองแยกต่างหาก: Astro.response.headers.set('Cache-Control', 'public, max-age=20') ข้างในหน้า — routeRules กับการตั้ง Cache-Control เองเป็นสอง layer ที่แยกกัน ซึ่งบังเอิญใช้ vocabulary maxAge ร่วมกันเท่านั้น ไม่ใช่อันหนึ่งตั้งให้อีกอันหนึ่ง

astro.config.mjs
// FIX (no adapter): output: 'server' needs a real runtime to execute in.
import { defineConfig, memoryCache } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
cache: { provider: memoryCache() },
routeRules: {
'/dash-metrics': { maxAge: 20 },
},
});
src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const user = (await context.session?.get('user')) ?? null;
// FIX (LocalsReassigned): merge into locals, never reassign it.
Object.assign(context.locals, { user });
return next();
});
src/pages/api/dash-login.ts
import type { APIRoute } from 'astro';
export const prerender = false;
export const POST: APIRoute = async ({ session }) => {
await session?.set('user', { id: 'u1' });
// FIX (session fixation): rotate the id now that this session is
// authenticated — closes the fixation hole proven above.
await session?.regenerate();
return Response.json({ ok: true });
};
src/pages/dash-shell.astro
---
// FIX (Astro.session + Cache-Control on a prerendered page): this page
// genuinely needs a live request, so it has to be on-demand.
export const prerender = false;
const cart = (await Astro.session?.get('cart')) ?? [];
Astro.response.headers.set('Cache-Control', 'private, no-store');
---
<p>{cart.length} items in your cart</p>
src/pages/dash-metrics.astro
---
// FIX (routeRules doesn't set the browser Cache-Control): set it
// explicitly, as its own independent layer.
export const prerender = false;
const id = crypto.randomUUID();
Astro.response.headers.set('Cache-Control', 'public, max-age=20');
---
<p>Request id: {id}</p>
src/pages/dashboard.astro
---
// FIX (secret read in a client script): astro:env/server, never a plain
// import.meta.env read inside a <script>, and never handed to the client.
export const prerender = false;
---
<html>
<head><title>Dashboard</title></head>
<body>
<p id="key">Analytics enabled server-side</p>
</body>
</html>

astro build จริงของโปรเจกต์ที่แก้แล้ว (adapter จริง, output: 'server', session, และ config routeRules/cache นี้เป๊ะ ๆ) เสร็จสมบูรณ์โดยไม่มี error เลย:

[build] Complete!
# บั๊ก หมวด ทางแก้
1 output: 'server' โดยไม่ตั้งค่า adapter Rendering mode & adapter ติดตั้งและตั้งค่า @astrojs/node (หรือ adapter อื่น) — พิสูจน์ด้วย NoAdapterInstalled จริงที่ทำให้ build fail
2 context.locals = { user } ใน middleware Server-side Astro Object.assign(context.locals, { user }) — พิสูจน์ด้วย LocalsReassigned จริงที่ทำให้ทุก request fail
3 อ่าน Astro.session บน prerender = true Rendering mode & adapter export const prerender = false — พิสูจน์ด้วย build warning จริงและ HTML ที่ build ออกมาแสดง 0 items เสมอ
4 Astro.response.headers.set('Cache-Control', ...) บนหน้า prerender เดียวกัน Performance & caching พอแก้ #3 แล้ว call นี้จะทำงานจริง บนหน้าที่ static จริง ๆ มันคือ no-op ที่พิสูจน์แล้วว่าไม่มี header sidecar เลยใน dist/
5 อ่าน import.meta.env.ANALYTICS_KEY ข้างใน <script> ฝั่ง client Server-side Astro ใส่ prefix PUBLIC_ ถ้าตั้งใจให้ public หรือไม่ส่ง secret จริงถึง client เลย — พิสูจน์ด้วย script ที่ build จริงอ่านได้ undefined
6 ไม่มี session.regenerate() หลัง login Server-side Astro await session?.regenerate() ทันทีหลังตั้ง session ที่ authenticated แล้ว — พิสูจน์ด้วย curl จริง: id เดิม เทียบกับ id ใหม่
7 คาดหวังให้ routeRules: { maxAge: 20 } ตั้ง Cache-Control ของ browser Performance & caching Astro.response.headers.set('Cache-Control', ...) เป็น call แยกต่างหากของตัวเอง — พิสูจน์ด้วย request จริงสองครั้งที่แสดง x-astro-cache: MISS → HIT และไม่มี Cache-Control ปรากฏเลยทั้งสองครั้ง

Astro ให้กลไกหลายอย่างกับ dashboard นี้ที่ฟังดูเหมือนเป็น lever เดียวกัน — Astro.response.headers, routeRules/cache, context.locals, session, import.meta.env — และทุกบั๊กในรีวิวนี้คือการหยิบเครื่องมือผิดตัวจากสองตัวที่ชื่อคล้ายกัน หรือใช้เครื่องมือที่ถูกต้องบนหน้าที่ rendering mode ไม่รองรับมันเลย ไม่มีอันไหน throw type error เพราะไม่มีอันไหนผิดในระดับ type: Astro.session มีอยู่จริงเป็น property ที่ optional, context.locals = {...} ก็เป็น JavaScript ที่ถูกต้องจริง, routeRules ก็เป็น config key ที่มีเอกสารรองรับจริง ทางเดียวที่จะจับบั๊กพวกนี้ได้คือรัน request ที่ตรงจุดที่ใช้ช่องว่างนั้นจริง ๆ — build โดยไม่มี adapter, ขอหน้า prerender ที่คาดหวังข้อมูล session สด, curl route เดิมสองครั้ง, login ด้วย session id ที่ควบคุมอยู่แล้ว

  • “ถ้า build ผ่าน แปลว่าฟีเจอร์แบบ on-demand ต่อสายไว้ถูกต้องแล้ว” พิสูจน์แล้วข้างต้น: Astro.session, Astro.response.headers, และ routeRules ต่างก็ build ผ่านสะอาดแม้ตอนที่มันไม่ทำอะไรเลยบน route หนึ่ง ๆ — build ที่เขียวแปลว่าโค้ด type-check ผ่านกับ bundle ได้เท่านั้น ไม่ได้แปลว่ากลไกที่กำลังใช้ใช้ได้กับ rendering mode ของ route นั้นจริง
  • “context.locals = {...} ก็แค่ object assignment ธรรมดา” พิสูจน์แล้วข้างต้น: setter ของ locals ใน Astro throw LocalsReassigned เสมอไม่มีเงื่อนไข — object นี้ถูกออกแบบมาให้ reassign ไม่ได้โดยตั้งใจ เพื่อให้ middleware ตัวถัดไปใน chain ไม่มีทางเผลอลบสิ่งที่ตัวก่อนหน้าตั้งไว้ได้
  • “แค่ตรวจ session cookie ตอน login ก็พอแล้ว ตัว id เองไม่สำคัญ” พิสูจน์แล้วข้างต้นด้วย login จริงสองครั้ง: ถ้าไม่มี session.regenerate() session id ที่ปลูกไว้ล่วงหน้าโดยผู้ไม่หวังดีจะรอดผ่าน login โดยไม่เปลี่ยน และกลายเป็น session ที่ authenticated แล้วสำหรับผู้ไม่หวังดีด้วย
flowchart TD
  A["astro.config.mjs"] -->|"1: output: server, no adapter"| A
  A --> B["src/middleware.ts"]
  B -->|"2: locals reassigned"| B
  B --> C["dash-shell.astro (prerender = true)"]
  C -->|"3: Astro.session on a prerendered page"| C
  C -->|"4: Cache-Control has nowhere to go"| C
  B --> D["dashboard.astro"]
  D -->|"5: import.meta.env.ANALYTICS_KEY in a client script"| D
  B --> E["api/dash-login.ts"]
  E -->|"6: no session.regenerate()"| E
  B --> F["dash-metrics.astro (on-demand, routeRules)"]
  F -->|"7: routeRules is not the browser Cache-Control"| F
บั๊กแต่ละจุดอยู่ตรงไหนใน request flow ของ dashboard นี้
config นี้ไม่มี adapter เกิดอะไรขึ้นตอน astro build จริงที่มี route แบบ on-demand อยู่?
export default defineConfig({
  output: 'server',
  cache: { provider: memoryCache() },
});
เกิดอะไรขึ้นกับ request จริงที่ผ่าน middleware นี้?
export const onRequest = defineMiddleware(async (context, next) => {
  context.locals = { user };
  return next();
});
หน้าหนึ่งตั้ง export const prerender = true แล้วอ่าน Astro.session?.get('cart') HTML ที่ build จริงแสดงอะไร ตามที่พิสูจน์ในรีวิวนี้?
นี่คือ output ที่ build จริงของ <script> ฝั่ง client ที่อ่าน import.meta.env.ANALYTICS_KEY (ไม่มี prefix PUBLIC_) แล้วเขียนลงหน้า เกิดอะไรขึ้นกับค่านี้จริง ๆ?
key: undefined
ผู้ไม่หวังดีดักจับ session id จริงที่ backed ด้วย storage ไว้ก่อนที่เหยื่อจะ login ถ้าไม่มี session.regenerate() การ login จริงด้วย curl ด้วย id นั้นพิสูจน์อะไร ตามรีวิวนี้?
routeRules: { '/dash-metrics': { maxAge: 20 } } ถูกตั้งค่าไว้ response จริงทั้งสองนี้พิสูจน์อะไรเกี่ยวกับ header Cache-Control ที่ browser เห็น?
$ curl -s -D - http://.../dash-metrics/
x-astro-cache: MISS
<p>568a5dd8-...</p>

$ curl -s -D - http://.../dash-metrics/
x-astro-cache: HIT
<p>568a5dd8-...</p>
ทำไม Astro.response.headers.set('Cache-Control', ...) ถึง build ผ่านโดยไม่มี error เลยบนหน้าที่มี export const prerender = true ทั้งที่รีวิวนี้เรียกมันว่าบั๊ก?