From b06b5b351a62edcc302bfc766ad57b40b4748801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasni=C4=8D=C3=A1k?= Date: Tue, 11 Jun 2024 15:31:59 +0200 Subject: [PATCH] fix: properly handle redirects (#423) * fix(debugger): show examples only if they are available * feat(debugger): show response headers * fix: properly handle redirects * feat: show previous frame when redirected --- .changeset/cold-bulldogs-scream.md | 5 + .changeset/fast-shirts-help.md | 5 + .changeset/nasty-paws-rule.md | 5 + .changeset/rare-olives-stare.md | 5 + .../frame-debugger-examples-section.tsx | 4 +- .../frame-debugger-request-details.tsx | 135 ++++++++++++++++ .../app/components/frame-debugger.tsx | 147 ++---------------- packages/debugger/app/debugger-page.tsx | 1 - packages/debugger/app/frames/route.ts | 11 ++ packages/debugger/app/page.tsx | 8 +- .../app/utils/url-search-params-to-object.ts | 18 +++ packages/render/src/frame-ui.tsx | 5 +- packages/render/src/next/POST.tsx | 14 ++ packages/render/src/types.ts | 16 ++ packages/render/src/use-fetch-frame.ts | 113 ++++++++++++-- packages/render/src/use-frame-stack.ts | 25 +++ 16 files changed, 367 insertions(+), 150 deletions(-) create mode 100644 .changeset/cold-bulldogs-scream.md create mode 100644 .changeset/fast-shirts-help.md create mode 100644 .changeset/nasty-paws-rule.md create mode 100644 .changeset/rare-olives-stare.md create mode 100644 packages/debugger/app/components/frame-debugger-request-details.tsx create mode 100644 packages/debugger/app/utils/url-search-params-to-object.ts diff --git a/.changeset/cold-bulldogs-scream.md b/.changeset/cold-bulldogs-scream.md new file mode 100644 index 000000000..a66f89550 --- /dev/null +++ b/.changeset/cold-bulldogs-scream.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +feat(debugger): show response headers diff --git a/.changeset/fast-shirts-help.md b/.changeset/fast-shirts-help.md new file mode 100644 index 000000000..a447cc5e4 --- /dev/null +++ b/.changeset/fast-shirts-help.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": patch +--- + +fix(@frames.js/render): properly handle redirects diff --git a/.changeset/nasty-paws-rule.md b/.changeset/nasty-paws-rule.md new file mode 100644 index 000000000..62573fb25 --- /dev/null +++ b/.changeset/nasty-paws-rule.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +fix(debugger): show examples links only if examples are available diff --git a/.changeset/rare-olives-stare.md b/.changeset/rare-olives-stare.md new file mode 100644 index 000000000..95a0cf866 --- /dev/null +++ b/.changeset/rare-olives-stare.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +fix(debugger): properly handle redirects diff --git a/packages/debugger/app/components/frame-debugger-examples-section.tsx b/packages/debugger/app/components/frame-debugger-examples-section.tsx index 5177c326e..bfa84aa43 100644 --- a/packages/debugger/app/components/frame-debugger-examples-section.tsx +++ b/packages/debugger/app/components/frame-debugger-examples-section.tsx @@ -9,13 +9,13 @@ export type ExampleItem = { }; type FrameDebuggerExamplesSectionProps = { - examples: ExampleItem[] | null; + examples: ExampleItem[]; }; export function FrameDebuggerExamplesSection({ examples, }: FrameDebuggerExamplesSectionProps) { - if (!examples || examples.length === 0) { + if (examples.length === 0) { return null; } diff --git a/packages/debugger/app/components/frame-debugger-request-details.tsx b/packages/debugger/app/components/frame-debugger-request-details.tsx new file mode 100644 index 000000000..3400e3138 --- /dev/null +++ b/packages/debugger/app/components/frame-debugger-request-details.tsx @@ -0,0 +1,135 @@ +import type { FramesStackItem } from "@frames.js/render"; +import { JSONTree } from "react-json-tree"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@/components/table"; +import { urlSearchParamsToObject } from "../utils/url-search-params-to-object"; + +type FrameDebuggerRequestDetailsProps = { + frameStackItem: FramesStackItem; +}; + +export function FrameDebuggerRequestDetails({ + frameStackItem, +}: FrameDebuggerRequestDetailsProps) { + return ( + <> +

+ Request +

+ + + + URL + {frameStackItem.url} + + + Method + {frameStackItem.request.method} + + + Query Params + + + + + {frameStackItem.request.method === "POST" ? ( + + Payload + + + + + ) : null} + +
+ {frameStackItem.status !== "pending" ? ( + <> +

+ Response +

+ + + + Response status + + {frameStackItem.responseStatus} + + + {frameStackItem.response && ( + + Response headers + + + + + )} + {"frame" in frameStackItem ? ( + + Frame Response + + + + + ) : ( + + Response + + + + + )} + {frameStackItem.status === "requestError" && + !!frameStackItem.requestError && ( + + Error + + + + + )} + +
+ + ) : null} + + ); +} diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index 7dc1a5ccd..5f2194ad7 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -22,14 +22,7 @@ import { defaultTheme, } from "@frames.js/render"; import { FrameImageNext } from "@frames.js/render/next"; -import { JSONTree } from "react-json-tree"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, -} from "@/components/table"; +import { Table, TableBody, TableCell, TableRow } from "@/components/table"; import { AlertTriangle, BanIcon, @@ -40,6 +33,7 @@ import { LoaderIcon, RefreshCwIcon, XCircle, + ExternalLinkIcon, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { MockHubConfig } from "./mock-hub-config"; @@ -58,26 +52,13 @@ import { useRouter } from "next/navigation"; import { WithTooltip } from "./with-tooltip"; import { DebuggerConsole } from "./debugger-console"; import { FrameDebuggerLinksSidebarSection } from "./frame-debugger-links-sidebar-section"; +import { FrameDebuggerRequestDetails } from "./frame-debugger-request-details"; +import { urlSearchParamsToObject } from "../utils/url-search-params-to-object"; type FrameDiagnosticsProps = { stackItem: FramesStackItem; }; -function paramsToObject(entries: IterableIterator<[string, string]>): object { - const result: Record = {}; - for (const [key, value] of entries) { - // each 'entry' is a [key, value] tupple - if (value.startsWith("{")) { - try { - result[key] = JSON.parse(value); - continue; - } catch (err) {} - } - result[key] = value; - } - return result; -} - function isPropertyExperimental([key, value]: [string, string]) { // tx is experimental return false; @@ -103,6 +84,10 @@ function FrameDiagnostics({ stackItem }: FrameDiagnosticsProps) { return { validProperties, invalidProperties, isValid: true }; } + if (stackItem.status === "doneRedirect") { + return { validProperties, invalidProperties, isValid: true }; + } + const result = stackItem.frameResult; // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid @@ -271,6 +256,10 @@ const FramesRequestCardContentIcon: React.FC<{ } } + if (stackItem.status === "doneRedirect") { + return ; + } + if (stackItem.frameResult?.status === "failure") { return ; } @@ -342,8 +331,8 @@ const FramesRequestCardContent: React.FC<{ }} > {JSON.stringify( - paramsToObject( - new URL(frameStackItem.url).searchParams.entries() + urlSearchParamsToObject( + new URL(frameStackItem.url).searchParams ), null, 2 @@ -626,111 +615,9 @@ export const FrameDebugger = React.forwardRef< > -

- Request -

- - - - URL - - {currentFrameStackItem.url} - - - - Method - - {currentFrameStackItem.request.method} - - - - Query Params - - - - - {currentFrameStackItem.request.method === "POST" ? ( - - Payload - - - - - ) : null} - -
- {currentFrameStackItem.status !== "pending" ? ( - <> -

- Response -

- - - - Response status - - {currentFrameStackItem.responseStatus} - - - {"frame" in currentFrameStackItem ? ( - - Frame Response - - - - - ) : ( - - Response - - - - - )} - {currentFrameStackItem.status === "requestError" && - !!currentFrameStackItem.requestError && ( - - Error - - - - - )} - -
- - ) : null} +
{currentFrameStackItem.status === "done" ? ( diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index 46e4f1524..e08dfbfad 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -48,7 +48,6 @@ import { import { useLensIdentity } from "./hooks/use-lens-identity"; import { useLensFrameContext } from "./hooks/use-lens-context"; import { ProfileSelectorModal } from "./components/lens-profile-select"; -import { FrameDebuggerExamplesSection } from "./components/frame-debugger-examples-section"; const FALLBACK_URL = process.env.NEXT_PUBLIC_DEBUGGER_DEFAULT_URL || "http://localhost:3000"; diff --git a/packages/debugger/app/frames/route.ts b/packages/debugger/app/frames/route.ts index c99f7c9a3..621442491 100644 --- a/packages/debugger/app/frames/route.ts +++ b/packages/debugger/app/frames/route.ts @@ -111,9 +111,20 @@ export async function POST(req: NextRequest): Promise { // this is message response if ("message" in json) { return Response.json({ message: json.message }, { status: r.status }); + } else { + return r; } } + if (isPostRedirect && r.status !== 302) { + return Response.json( + { + message: `Invalid response status code for post redirect button, 302 expected, got ${r.status}`, + }, + { status: 400 } + ); + } + if (isTransactionRequest) { const transaction = (await r.json()) as JSON; return Response.json(transaction); diff --git a/packages/debugger/app/page.tsx b/packages/debugger/app/page.tsx index 9be07c3e6..aab719b1d 100644 --- a/packages/debugger/app/page.tsx +++ b/packages/debugger/app/page.tsx @@ -16,9 +16,11 @@ export default async function Homepage({ return ( + examples && examples.length > 0 ? ( + + ) : null } searchParams={searchParams} > diff --git a/packages/debugger/app/utils/url-search-params-to-object.ts b/packages/debugger/app/utils/url-search-params-to-object.ts new file mode 100644 index 000000000..1548cdbfa --- /dev/null +++ b/packages/debugger/app/utils/url-search-params-to-object.ts @@ -0,0 +1,18 @@ +export function urlSearchParamsToObject( + searchParams: URLSearchParams +): Record { + const result: Record = {}; + + for (const [key, value] of searchParams.entries()) { + // each 'entry' is a [key, value] tupple + if (value.startsWith("{")) { + try { + result[key] = JSON.parse(value); + continue; + } catch (err) {} + } + result[key] = value; + } + + return result; +} diff --git a/packages/render/src/frame-ui.tsx b/packages/render/src/frame-ui.tsx index eb94269fd..b33c71bc7 100644 --- a/packages/render/src/frame-ui.tsx +++ b/packages/render/src/frame-ui.tsx @@ -150,7 +150,10 @@ export function FrameUI({ if (currentFrame.status === "done") { frame = currentFrame.frameResult.frame; - } else if (currentFrame.status === "message") { + } else if ( + currentFrame.status === "message" || + currentFrame.status === "doneRedirect" + ) { frame = currentFrame.request.sourceFrame; } else if (currentFrame.status === "requestError") { frame = diff --git a/packages/render/src/next/POST.tsx b/packages/render/src/next/POST.tsx index 396dea13f..ac9f63ea7 100644 --- a/packages/render/src/next/POST.tsx +++ b/packages/render/src/next/POST.tsx @@ -32,6 +32,10 @@ export async function POST(req: Request | NextRequest): Promise { body: JSON.stringify(body), }); + if (r.status >= 500) { + return r; + } + if (r.status === 302) { return Response.json( { @@ -43,11 +47,21 @@ export async function POST(req: Request | NextRequest): Promise { if (r.status >= 400 && r.status < 500) { const json = (await r.json()) as { message?: string }; + if ("message" in json) { return Response.json({ message: json.message }, { status: r.status }); + } else { + return r; } } + if (isPostRedirect && r.status !== 302) { + return Response.json( + { message: "Invalid response for redirect button" }, + { status: 500 } + ); + } + if (isTransactionRequest) { const transaction = (await r.json()) as JSON; return Response.json(transaction); diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index ec359dc9c..be63c7025 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -160,18 +160,28 @@ export type GetFrameResult = ReturnType; export type FrameStackDone = FrameStackBase & { request: FrameRequest; + response: Response; frameResult: GetFrameResult; status: "done"; }; +export type FrameStackDoneRedirect = FrameStackBase & { + request: FramePOSTRequest; + response: Response; + location: string; + status: "doneRedirect"; +}; + export type FrameStackRequestError = FrameStackBase & { request: FrameRequest; + response: Response | null; status: "requestError"; requestError: Error; }; export type FrameStackMessage = FrameStackBase & { request: FramePOSTRequest; + response: Response; status: "message"; message: string; type: "info" | "error"; @@ -180,6 +190,7 @@ export type FrameStackMessage = FrameStackBase & { export type FramesStackItem = | FrameStackPending | FrameStackDone + | FrameStackDoneRedirect | FrameStackRequestError | FrameStackMessage; @@ -204,6 +215,11 @@ export type FrameReducerActions = pendingItem: FrameStackPending; item: FrameStackRequestError; } + | { + action: "DONE_REDIRECT"; + pendingItem: FrameStackPending; + item: FrameStackDoneRedirect; + } | { action: "DONE"; pendingItem: FrameStackPending; diff --git a/packages/render/src/use-fetch-frame.ts b/packages/render/src/use-fetch-frame.ts index 219bce4a9..fc88a5835 100644 --- a/packages/render/src/use-fetch-frame.ts +++ b/packages/render/src/use-fetch-frame.ts @@ -103,6 +103,7 @@ export function useFetchFrame({ const stackItem: FrameStackMessage = { ...(frameStackPendingItem as FrameStackPostPending), responseStatus: response.status, + response: response.clone(), speed: computeDurationInSeconds(startTime, endTime), status: "message", type: "error", @@ -136,6 +137,7 @@ export function useFetchFrame({ startTime, endTime, responseBody, + response, }); } @@ -146,6 +148,7 @@ export function useFetchFrame({ startTime, endTime, responseBody, + response, }: { frameStackPendingItem: FrameStackPending; requestError: Error; @@ -153,6 +156,7 @@ export function useFetchFrame({ startTime: Date; endTime: Date; responseBody: unknown; + response: Response | null; }): void { stackDispatch({ action: "REQUEST_ERROR", @@ -162,6 +166,7 @@ export function useFetchFrame({ requestDetails: frameStackPendingItem.requestDetails, timestamp: frameStackPendingItem.timestamp, url: frameStackPendingItem.url, + response: response?.clone() ?? null, responseStatus, requestError, speed: computeDurationInSeconds(startTime, endTime), @@ -217,7 +222,7 @@ export function useFetchFrame({ return; } - const loadedFrame = (await response.json()) as GetFrameResult; + const loadedFrame = (await response.clone().json()) as GetFrameResult; stackDispatch({ action: "DONE", @@ -227,6 +232,7 @@ export function useFetchFrame({ status: "done", frameResult: loadedFrame, speed: computeDurationInSeconds(startTime, endTime), + response: response.clone(), responseStatus: response.status, responseBody: loadedFrame, }, @@ -242,6 +248,7 @@ export function useFetchFrame({ responseBody: "none", responseStatus: 500, requestError: response, + response: null, }); } @@ -296,6 +303,7 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: signedDataOrError, + response: null, }); return; @@ -331,7 +339,87 @@ export function useFetchFrame({ ); const endTime = new Date(); + async function handleRedirect( + res: Response, + pendingItem: FrameStackPostPending + ): Promise { + // check that location is proper fully formatted url + try { + let location = res.headers.get("location"); + + if (!location) { + const responseData = (await res.clone().json()) as + | Record + | string + | null + | number; + + if ( + responseData && + typeof responseData === "object" && + "location" in responseData && + typeof responseData.location === "string" + ) { + location = responseData.location; + } + } + + if (!location) { + throw new Error( + `Response data does not contain 'location' key and no 'location' header is found.` + ); + } + + // check the URL is valid + const locationUrl = new URL(location); + + if (window.confirm(`You are about to be redirected to ${location}`)) { + window.open(locationUrl, "_blank")?.focus(); + } + + stackDispatch({ + action: "DONE_REDIRECT", + pendingItem, + item: { + ...pendingItem, + location, + response: res.clone(), + responseBody: await res.clone().text(), + responseStatus: res.status, + status: "doneRedirect", + speed: computeDurationInSeconds(startTime, endTime), + }, + }); + } catch (e) { + stackDispatch({ + action: "REQUEST_ERROR", + pendingItem: frameStackPendingItem, + item: { + ...frameStackPendingItem, + status: "requestError", + requestError: + e instanceof Error + ? e + : new Error( + "Response body must be a json with 'location' property or response 'Location' header must contain fully qualified URL." + ), + response: res.clone(), + responseStatus: res.status, + responseBody: await res.text(), + speed: computeDurationInSeconds(startTime, endTime), + }, + }); + } + } + if (response instanceof Response) { + // handle valid redirect + if (response.status === 302) { + await handleRedirect(response, frameStackPendingItem); + + return; + } + if (!response.ok) { await handleFailedResponse({ response, @@ -343,22 +431,11 @@ export function useFetchFrame({ return; } - const responseData = (await response.json()) as + const responseData = (await response.clone().json()) as | GetFrameResult - | { location: string } | { message: string } | { type: "frame"; frameUrl: string }; - if ("location" in responseData) { - const location = responseData.location; - - if (window.confirm(`You are about to be redirected to ${location}`)) { - window.open(location, "_blank")?.focus(); - } - - return; - } - // cast action message response if ("message" in responseData) { stackDispatch({ @@ -369,6 +446,7 @@ export function useFetchFrame({ status: "message", message: responseData.message, type: "info", + response: response.clone(), responseStatus: response.status, speed: computeDurationInSeconds(startTime, endTime), responseBody: responseData, @@ -425,6 +503,7 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: error, + response, }); return; @@ -440,6 +519,7 @@ export function useFetchFrame({ frameResult: responseData, status: "done", speed: computeDurationInSeconds(startTime, endTime), + response: response.clone(), responseStatus: response.status, responseBody: responseData, }, @@ -455,6 +535,10 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: response, + response: new Response(response.message, { + status: 500, + headers: { "Content-Type": "text/plain" }, + }), }); } @@ -505,6 +589,7 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: signedTransactionDataActionOrError, + response: null, }); return; @@ -550,6 +635,7 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: transactionDataResponse, + response: null, }); return; @@ -592,6 +678,7 @@ export function useFetchFrame({ responseStatus: 500, startTime, requestError: transactionIdOrError, + response: null, }); return; diff --git a/packages/render/src/use-frame-stack.ts b/packages/render/src/use-frame-stack.ts index fc22373d2..f54276f48 100644 --- a/packages/render/src/use-frame-stack.ts +++ b/packages/render/src/use-frame-stack.ts @@ -39,6 +39,23 @@ function framesStackReducer( return state.slice(); } + case "DONE_REDIRECT": { + const index = state.findIndex( + (item) => item.timestamp === action.pendingItem.timestamp + ); + + if (index === -1) { + return state; + } + + state[index] = { + ...action.pendingItem, + ...action.item, + status: "doneRedirect", + }; + + return state.slice(); + } case "DONE": case "REQUEST_ERROR": { const index = state.findIndex( @@ -81,6 +98,10 @@ function framesStackReducer( }, url: action.homeframeUrl ?? "", requestDetails: {}, + response: new Response(JSON.stringify(frameResult), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), responseStatus: 200, timestamp: new Date(), speed: 0, @@ -124,6 +145,10 @@ export function useFrameStack({ }; return [ { + response: new Response(JSON.stringify(frameResult), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), responseStatus: 200, responseBody: frameResult, timestamp: new Date(),