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 .changeset/optional-exchange-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

Make remote dev `exchange_url` optional

The edge-preview API's `exchange_url` is now treated as optional. When unavailable or when the exchange fails, the initial token from the API response is used directly. The `prewarm` step and `inspector_websocket` have been removed from the remote dev flow in favour of `tail_url` for live logs.
11 changes: 11 additions & 0 deletions .changeset/remove-prewarm-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@cloudflare/edge-preview-authenticated-proxy": minor
"@cloudflare/playground-preview-worker": minor
"@cloudflare/workers-playground": minor
---

Remove prewarm, inspector_websocket, and exchange proxy from preview flow

The preview session exchange endpoint (`/exchange`) has been removed from the edge-preview-authenticated-proxy — it has been unused since the dash started fetching the exchange URL directly (DEVX-979). The `prewarm` parameter is no longer required or accepted by the `.update-preview-token` endpoint.

The playground preview worker now treats `exchange_url` as optional, falling back to the initial token from the edge-preview API when exchange is unavailable. Inspector websocket proxying and prewarm have been removed in favour of using `tail_url` for live logs.
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] {
},
{
name: "solid",
quarantine: true,
promptHandlers: [
{
matcher: /Which template would you like to use/,
Expand Down Expand Up @@ -810,6 +811,7 @@ function getExperimentalFrameworkTestConfig(
},
{
name: "solid",
quarantine: true,
promptHandlers: [
{
matcher: /Which template would you like to use/,
Expand Down
108 changes: 6 additions & 102 deletions packages/edge-preview-authenticated-proxy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,9 @@ class HttpError extends Error {
}
}

class NoExchangeUrl extends HttpError {
constructor() {
super("No exchange_url provided", 400, false);
}
}

class ExchangeFailed extends HttpError {
constructor(
readonly url: string,
readonly exchangeStatus: number,
readonly body: string
) {
super("Exchange failed", 400, true);
}

get data(): { url: string; status: number; body: string } {
return { url: this.url, status: this.exchangeStatus, body: this.body };
}
}

class TokenUpdateFailed extends HttpError {
constructor() {
super("Provide token, prewarmUrl and remote", 400, false);
super("Provide token and remote", 400, false);
}
}

Expand Down Expand Up @@ -101,14 +81,6 @@ function switchRemote(url: URL, remote: string) {
return workerUrl;
}

function isTokenExchangeRequest(request: Request, url: URL, env: Env) {
return (
request.method === "POST" &&
url.hostname === env.PREVIEW &&
url.pathname === "/exchange"
);
}

function isPreviewUpdateRequest(request: Request, url: URL, env: Env) {
return (
request.method === "GET" &&
Expand All @@ -121,19 +93,11 @@ function isRawHttpRequest(url: URL, env: Env) {
return url.hostname.endsWith(env.RAW_HTTP);
}

async function handleRequest(
request: Request,
env: Env,
ctx: ExecutionContext
) {
async function handleRequest(request: Request, env: Env) {
const url = new URL(request.url);

if (isTokenExchangeRequest(request, url, env)) {
return handleTokenExchange(url);
}

if (isPreviewUpdateRequest(request, url, env)) {
return updatePreviewToken(url, env, ctx);
return updatePreviewToken(url, env);
}

if (isRawHttpRequest(url, env)) {
Expand Down Expand Up @@ -295,27 +259,15 @@ async function handleRawHttp(request: Request, url: URL) {
* It will redirect to the suffix provide, setting a cookie with the `token` and `remote`
* for future use.
*/
async function updatePreviewToken(url: URL, env: Env, ctx: ExecutionContext) {
async function updatePreviewToken(url: URL, env: Env) {
const token = url.searchParams.get("token");
const prewarmUrl = url.searchParams.get("prewarm");
const remote = url.searchParams.get("remote");
// return Response.json([...url.searchParams.entries()]);
if (!token || !prewarmUrl || !remote) {
if (!token || !remote) {
throw new TokenUpdateFailed();
}

assertValidURL(prewarmUrl);
assertValidURL(remote);

ctx.waitUntil(
fetch(prewarmUrl, {
method: "POST",
headers: {
"cf-workers-preview-token": token,
},
})
);

// The token can sometimes be too large for a cookie (4096 bytes).
// Store the token in KV, and allow lookups

Expand All @@ -342,54 +294,6 @@ async function updatePreviewToken(url: URL, env: Env, ctx: ExecutionContext) {
});
}

/**
* Request the preview session associated with a given exchange_url
* exchange_url comes from an authenticated core API call made in the client
* It doesn't have CORS set up, so needs to be proxied
*/
async function handleTokenExchange(url: URL) {
const exchangeUrl = url.searchParams.get("exchange_url");
if (!exchangeUrl) {
throw new NoExchangeUrl();
}
assertValidURL(exchangeUrl);
const exchangeRes = await fetch(exchangeUrl);
if (exchangeRes.status !== 200) {
const exchange = new URL(exchangeUrl);
// Clear sensitive token
exchange.search = "";

throw new ExchangeFailed(
exchange.href,
exchangeRes.status,
await exchangeRes.text()
);
}
const session = await exchangeRes.json<{
prewarm: string;
token: string;
}>();
if (
typeof session.token !== "string" ||
typeof session.prewarm !== "string"
) {
const exchange = new URL(exchangeUrl);
// Clear sensitive token
exchange.search = "";
throw new ExchangeFailed(
exchange.href,
exchangeRes.status,
JSON.stringify(session)
);
}
return Response.json(session, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST",
},
});
}

// No ecosystem routers support hostname matching 😥
export default {
async fetch(
Expand Down Expand Up @@ -429,7 +333,7 @@ export default {
});

try {
return await handleRequest(request, env, ctx);
return await handleRequest(request, env);
} catch (e) {
console.error(e);
if (e instanceof HttpError) {
Expand Down
66 changes: 3 additions & 63 deletions packages/edge-preview-authenticated-proxy/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ function createMockFetchImplementation() {
return new Response("BAD", { status: 500 });
}

if (url.pathname === "/exchange") {
return Response.json({
token: "TEST_TOKEN",
prewarm: "TEST_PREWARM",
});
}
if (url.pathname === "/redirect") {
// Use manual redirect to avoid trailing slash being added
return new Response(null, {
Expand All @@ -61,10 +55,6 @@ function createMockFetchImplementation() {
headers.append("Set-Cookie", "bar=2");
return new Response(undefined, { headers });
}
if (url.pathname === "/prewarm") {
return new Response("OK");
}

return Response.json(
{
url: request.url,
Expand All @@ -86,36 +76,6 @@ afterEach(() => {
});

describe("Preview Worker", () => {
it("should obtain token from exchange_url", async ({ expect }) => {
const resp = await SELF.fetch(
`https://preview.devprod.cloudflare.dev/exchange?exchange_url=${encodeURIComponent(
`${MOCK_REMOTE_URL}/exchange`
)}`,
{
method: "POST",
}
);
const text = await resp.json();
expect(text).toMatchInlineSnapshot(
`
{
"prewarm": "TEST_PREWARM",
"token": "TEST_TOKEN",
}
`
);
});
it("should reject invalid exchange_url", async ({ expect }) => {
vi.spyOn(console, "error").mockImplementation(() => {});
const resp = await SELF.fetch(
`https://preview.devprod.cloudflare.dev/exchange?exchange_url=not_an_exchange_url`,
{ method: "POST" }
);
expect(resp.status).toBe(400);
expect(await resp.text()).toMatchInlineSnapshot(
`"{"error":"Error","message":"Invalid URL"}"`
);
});
it("should allow tokens > 4096 bytes", async ({ expect }) => {
// 4096 is the size limit for cookies
const token = randomBytes(4096).toString("hex");
Expand All @@ -124,8 +84,6 @@ describe("Preview Worker", () => {
let resp = await SELF.fetch(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=${encodeURIComponent(
token
)}&prewarm=${encodeURIComponent(
`${MOCK_REMOTE_URL}/prewarm`
)}&remote=${encodeURIComponent(
MOCK_REMOTE_URL
)}&suffix=${encodeURIComponent("/hello?world")}`,
Expand Down Expand Up @@ -164,9 +122,7 @@ describe("Preview Worker", () => {
});
it("should be redirected with cookie", async ({ expect }) => {
const resp = await SELF.fetch(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent(
`${MOCK_REMOTE_URL}/prewarm`
)}&remote=${encodeURIComponent(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent(
MOCK_REMOTE_URL
)}&suffix=${encodeURIComponent("/hello?world")}`,
{
Expand All @@ -186,9 +142,7 @@ describe("Preview Worker", () => {

async function getToken() {
const resp = await SELF.fetch(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent(
`${MOCK_REMOTE_URL}/prewarm`
)}&remote=${encodeURIComponent(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent(
MOCK_REMOTE_URL
)}&suffix=${encodeURIComponent("/hello?world")}`,
{
Expand All @@ -198,24 +152,10 @@ describe("Preview Worker", () => {
);
return (resp.headers.get("set-cookie") ?? "").split(";")[0].split("=")[1];
}
it("should reject invalid prewarm url", async ({ expect }) => {
vi.spyOn(console, "error").mockImplementation(() => {});
const resp = await SELF.fetch(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=not_a_prewarm_url&remote=${encodeURIComponent(
MOCK_REMOTE_URL
)}&suffix=${encodeURIComponent("/hello?world")}`
);
expect(resp.status).toBe(400);
expect(await resp.text()).toMatchInlineSnapshot(
`"{"error":"Error","message":"Invalid URL"}"`
);
});
it("should reject invalid remote url", async ({ expect }) => {
vi.spyOn(console, "error").mockImplementation(() => {});
const resp = await SELF.fetch(
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent(
`${MOCK_REMOTE_URL}/prewarm`
)}&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}`
`https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}`
);
expect(resp.status).toBe(400);
expect(await resp.text()).toMatchInlineSnapshot(
Expand Down
17 changes: 0 additions & 17 deletions packages/playground-preview-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,23 +200,6 @@ app.post(`${rootDomain}/api/worker`, async (c) => {
});
});

app.get(`${rootDomain}/api/inspector`, async (c) => {
const url = new URL(c.req.url);
const userId = url.searchParams.get("user");
if (!userId) {
throw new PreviewRequestFailed("", false);
}
let userObjectId: DurableObjectId;
try {
userObjectId = c.env.UserSession.idFromString(userId);
} catch {
throw new PreviewRequestFailed(userId, false);
}
const userObject = c.env.UserSession.get(userObjectId);

return userObject.fetch(c.req.raw);
});

/**
* Given a preview session, the client should obtain a specific preview token
* This endpoint takes in the URL parameters:
Expand Down
Loading
Loading