Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ async function expectOk<T>(res: Response, fallback: string): Promise<T> {
/**
* Fetches the client-side app configuration from the worker.
* GET /api/config
*
* If the worker injected the config via a <script> tag (window.__APP_CONFIG__),
* that is used immediately without a network round-trip.
*/
export async function fetchConfig(): Promise<AppConfig> {
const injected = (globalThis as Record<string, unknown>).__APP_CONFIG__ as AppConfig | undefined;
if (injected?.supabaseUrl && injected?.supabasePublishableKey) {
return injected;
}
const response = await fetch("/api/config");
if (!response.ok) throw new Error("Config unavailable");
return response.json() as Promise<AppConfig>;
Expand Down
26 changes: 25 additions & 1 deletion src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,38 @@ export default {
return await previewShare(request, env, ctx);
}

return withDiscoveryHeaders(await env.ASSETS.fetch(request));
const assetResponse = await env.ASSETS.fetch(request);
const contentType = assetResponse.headers.get("content-type") ?? "";
if (contentType.includes("text/html") && request.method === "GET") {
return withDiscoveryHeaders(await injectConfig(assetResponse, env));
}
return withDiscoveryHeaders(assetResponse);
} catch (error) {
console.error(JSON.stringify({ event: "unhandled_error", message: errorMessage(error) }));
return json({ error: "Internal server error" }, 500);
}
}
};

/**
* Injects the Supabase client config into an HTML response so the browser
* can initialise the Supabase client without a separate /api/config fetch.
* The injected script sets window.__APP_CONFIG__ before any module scripts run.
*/
async function injectConfig(response: Response, env: Env): Promise<Response> {
const configScript = `<script>window.__APP_CONFIG__=${JSON.stringify({
supabaseUrl: env.SUPABASE_URL,
supabasePublishableKey: env.SUPABASE_PUBLISHABLE_KEY
})}</script>`;
const html = await response.text();
const injected = html.replace("</head>", `${configScript}</head>`);
return new Response(injected, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
Comment on lines +168 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not forward entity headers after mutating the HTML body.

After response.text() + replacement, reusing original headers can keep stale entity metadata (content-length, content-encoding, etag) for a different payload, which can break decoding/caching behavior on Line 166.

Suggested fix
 async function injectConfig(response: Response, env: Env): Promise<Response> {
@@
   const html = await response.text();
   const injected = html.replace("</head>", `${configScript}</head>`);
+  const headers = new Headers(response.headers);
+  headers.delete("content-length");
+  headers.delete("content-encoding");
+  headers.delete("etag");
   return new Response(injected, {
     status: response.status,
     statusText: response.statusText,
-    headers: response.headers
+    headers
   });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Response(injected, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
const headers = new Headers(response.headers);
headers.delete("content-length");
headers.delete("content-encoding");
headers.delete("etag");
return new Response(injected, {
status: response.status,
statusText: response.statusText,
headers
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/worker/index.ts` around lines 163 - 167, The Response constructor is
reusing the original response.headers directly after mutating the HTML body with
text replacement. This causes stale entity metadata headers like content-length,
content-encoding, and etag to persist with the new payload, breaking decoding
and caching behavior. Create a new Headers object or clone the existing one and
remove entity headers (content-length, content-encoding, etag, and similar
body-related metadata) before passing it to the Response constructor on line
166. Keep other safe headers like content-type, cache-control, and other
directives that don't depend on the original payload.

}

function discoveryRoute(request: Request, url: URL): Response | null {
if (request.method !== "GET" && request.method !== "HEAD") {
return null;
Expand Down
Loading