bosia 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosia",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.4"
package/src/core/cache.ts CHANGED
@@ -404,6 +404,9 @@ export function serveCached(entry: CacheEntry, req: Request): Response {
404
404
  return new Response(null, { status: 304, headers });
405
405
  }
406
406
  }
407
+ // BosiaApp drops HEAD bodies, and a null body would report Content-Length 0.
408
+ // GET leaves it to the runtime, so a hook that rewrites the body stays valid.
409
+ if (req.method === "HEAD") headers["content-length"] = String(body.byteLength);
407
410
  if (encoding) {
408
411
  headers["content-encoding"] = encoding;
409
412
  return new Response(body, { ...PRECOMPRESSED, status: entry.status, headers });
@@ -615,15 +615,24 @@ export async function renderSSRStream(
615
615
  // into the cached HTML but the CSP header is re-derived each request, so a
616
616
  // cached page would ship with a dead nonce and the browser would block its
617
617
  // inline scripts. Operators who turn on CSP_DIRECTIVES forfeit the cache.
618
- const cacheable =
619
- CACHE_ENABLED && !CSP_ENABLED && !warmingUp && routeCacheable && req.method === "GET";
618
+ // HEAD reads the GET entry but never coalesces or writes: it isn't a leader
619
+ // that fills it.
620
+ const cacheRead =
621
+ CACHE_ENABLED &&
622
+ !CSP_ENABLED &&
623
+ !warmingUp &&
624
+ routeCacheable &&
625
+ (req.method === "GET" || req.method === "HEAD");
626
+ const cacheable = cacheRead && req.method === "GET";
620
627
  let cacheKey: string | null = null;
621
628
  let releaseMiss: (() => void) | null = null;
622
- if (cacheable) {
629
+ if (cacheRead) {
623
630
  cacheKey = computeCacheKey(url, req, cookies as CookieJar);
624
631
  if (!cacheBypass) {
625
632
  const hit = cacheGet(cacheKey);
626
633
  if (hit) return serveCached(hit, req);
634
+ }
635
+ if (cacheable && !cacheBypass) {
627
636
  // Miss coalescing: the first miss leads the build; concurrent misses
628
637
  // wait, then re-check the cache. A waiter that still misses (leader
629
638
  // skipped the write) builds independently — no re-leadering.
@@ -606,15 +606,16 @@ async function resolve(event: RequestEvent): Promise<Response> {
606
606
  let releaseApiMiss: (() => void) | null = null;
607
607
  try {
608
608
  const mod = await apiMatch.route.module();
609
- const handler = mod[method];
609
+ // HEAD falls back to GET (an explicit HEAD export still wins). BosiaApp
610
+ // strips the body on the way out, for every route kind.
611
+ const handler = mod[method] ?? (method === "HEAD" ? mod.GET : undefined);
610
612
 
611
613
  if (!handler) {
612
- const allowed = Object.keys(mod)
613
- .filter((k) => /^[A-Z]+$/.test(k))
614
- .join(", ");
614
+ const allowed = Object.keys(mod).filter((k) => /^[A-Z]+$/.test(k));
615
+ if (allowed.includes("GET") && !allowed.includes("HEAD")) allowed.push("HEAD");
615
616
  return Response.json(
616
617
  { error: `Method ${method} not allowed` },
617
- { status: 405, headers: { Allow: allowed } },
618
+ { status: 405, headers: { Allow: allowed.join(", ") } },
618
619
  );
619
620
  }
620
621
 
@@ -623,15 +624,23 @@ async function resolve(event: RequestEvent): Promise<Response> {
623
624
  // ── Response cache for +server.ts GET handlers ──
624
625
  // CSP is skipped because cached responses would ship with a stale
625
626
  // nonce (see renderer.ts for the same gate). The cache key includes
626
- // URL + identity so per-user responses stay isolated.
627
- const apiCacheable =
628
- CACHE_ENABLED && !CSP_ENABLED && (mod as any).cache !== false && method === "GET";
627
+ // URL + identity so per-user responses stay isolated. HEAD reads the GET
628
+ // entry but never coalesces or writes: it isn't a leader that fills it.
629
+ // An explicit HEAD export skips the cache so it always runs.
630
+ const apiCacheRead =
631
+ CACHE_ENABLED &&
632
+ !CSP_ENABLED &&
633
+ (mod as any).cache !== false &&
634
+ (method === "GET" || (method === "HEAD" && !mod.HEAD));
635
+ const apiCacheable = apiCacheRead && method === "GET";
629
636
  let apiCacheKey: string | null = null;
630
- if (apiCacheable) {
637
+ if (apiCacheRead) {
631
638
  apiCacheKey = computeCacheKey(url, request, cookies as CookieJar);
632
639
  if (!url.searchParams.has("_invalidated")) {
633
640
  const hit = cacheGet(apiCacheKey);
634
641
  if (hit) return serveCached(hit, request);
642
+ }
643
+ if (apiCacheable && !url.searchParams.has("_invalidated")) {
635
644
  // Miss coalescing: first miss runs the handler; concurrent misses
636
645
  // wait, re-check the cache, and on a still-miss run independently.
637
646
  const gate = coalesceMiss(apiCacheKey);