- );
- }
-
- // TODO
- if (body?.type === "binary") {
- return (
-
-
-
-
-
- );
- }
-
- // TODO - Stylize
- if (body?.type === "unknown") {
- return ;
- }
-
- return ;
- }
-
- if (!isRequestorActiveResponse(response)) {
- const body = response?.app_responses?.responseBody;
-
- // Special rendering for JSON
- if (body && isJson(body)) {
- const prettyBody = JSON.stringify(JSON.parse(body), null, 2);
-
- return (
-
-
-
- );
- }
-
- // For text responses, just split into lines and render with rudimentary line numbers
- // TODO - if response is empty, show that in a ux friendly way, with 204 for example
-
- return (
-
+ Make sure your api is up and has FPX Middleware enabled!
+
+
+
+ );
+}
diff --git a/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/ResponseBody.tsx b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/ResponseBody.tsx
new file mode 100644
index 000000000..1c3929e89
--- /dev/null
+++ b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/ResponseBody.tsx
@@ -0,0 +1,178 @@
+import { CodeMirrorJsonEditor } from "@/components/CodeMirrorEditor";
+import { SubSectionHeading } from "@/components/Timeline";
+import { TextOrJsonViewer } from "@/components/Timeline/DetailsList/TextJsonViewer";
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import { cn, isJson, noop, safeParseJson } from "@/utils";
+import {
+ CaretDownIcon,
+ CaretRightIcon,
+ QuestionMarkIcon,
+} from "@radix-ui/react-icons";
+import { useState } from "react";
+import type { ProxiedRequestResponse } from "../../queries";
+import {
+ type RequestorActiveResponse,
+ isRequestorActiveResponse,
+} from "../../store/types";
+import { FailedRequest } from "./FailedRequest";
+import { ResponseBodyBinary } from "./ResponseBodyBinary";
+import { ResponseBodyText } from "./ResponseBodyText";
+
+export function ResponseBody({
+ response,
+ className,
+}: {
+ response?: ProxiedRequestResponse | RequestorActiveResponse;
+ className?: string;
+}) {
+ const isFailure = isRequestorActiveResponse(response)
+ ? response?.isFailure
+ : response?.app_responses?.isFailure;
+
+ // This means we couldn't even contact the service
+ if (isFailure) {
+ return ;
+ }
+
+ // NOTE - This means we have the *actual* response from the service in the store,
+ // which may contain binary data that we can render in the UI.
+ // This is different from history responses, which will only have whatever data was stored in the trace
+ if (isRequestorActiveResponse(response)) {
+ const body = response?.responseBody;
+ if (body?.type === "error") {
+ return ;
+ }
+
+ if (body?.type === "text" || body?.type === "html") {
+ return (
+
+ );
+ }
+
+ // TODO - Stylize
+ if (body?.type === "unknown") {
+ return ;
+ }
+
+ return ;
+ }
+
+ if (!isRequestorActiveResponse(response)) {
+ const body = response?.app_responses?.responseBody;
+
+ // Special rendering for JSON
+ if (body && isJson(body)) {
+ const prettyBody = JSON.stringify(JSON.parse(body), null, 2);
+
+ return (
+
+
+
+ );
+ }
+
+ // For text responses, just split into lines and render with rudimentary line numbers
+ // TODO - if response is empty, show that in a ux friendly way, with 204 for example
+
+ return (
+
+ );
+}
+
+function useTextPreview(
+ body: string,
+ isExpanded: boolean,
+ maxPreviewLength: number | null,
+ maxPreviewLines: number | null,
+) {
+ return useMemo(() => {
+ let hiddenCharsCount = 0;
+ let hiddenLinesCount = 0;
+ const allLinesCount = body.split("\n")?.length;
+
+ const exceedsMaxPreviewLength = maxPreviewLength
+ ? body.length > maxPreviewLength
+ : false;
+
+ const exceedsMaxPreviewLines = maxPreviewLines
+ ? allLinesCount > maxPreviewLines
+ : false;
+
+ // If we're not expanded, we want to show a preview of the body depending on the maxPreviewLength
+ let previewBody = body;
+ if (maxPreviewLength && exceedsMaxPreviewLength && !isExpanded) {
+ previewBody = body ? `${body.slice(0, maxPreviewLength)}...` : "";
+ hiddenCharsCount = body.length - maxPreviewLength;
+ }
+
+ let previewLines = previewBody?.split("\n");
+ if (
+ maxPreviewLines &&
+ !isExpanded &&
+ previewLines.length > maxPreviewLines
+ ) {
+ previewLines = previewLines.slice(0, maxPreviewLines);
+ previewBody = `${previewLines.join("\n")}...`;
+ hiddenLinesCount = allLinesCount - previewLines.length;
+ }
+
+ const lines = (isExpanded ? body : previewBody)
+ ?.split("\n")
+ ?.map((line, index) => (
+
+
+ {index + 1}
+
+ {line}
+
+ ));
+
+ return {
+ lines,
+ shouldShowExpandButton: exceedsMaxPreviewLength || exceedsMaxPreviewLines,
+ hiddenLinesCount,
+ hiddenCharsCount,
+ };
+ }, [body, maxPreviewLines, maxPreviewLength, isExpanded]);
+}
diff --git a/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/index.ts b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/index.ts
new file mode 100644
index 000000000..56e3c2963
--- /dev/null
+++ b/studio/src/pages/RequestorPage/ResponsePanel/ResponseBody/index.ts
@@ -0,0 +1,3 @@
+export * from "./ResponseBody";
+export * from "./ResponseBodyText";
+export * from "./FailedRequest";
diff --git a/studio/src/pages/RequestorPage/store/slices/requestResponseSlice.ts b/studio/src/pages/RequestorPage/store/slices/requestResponseSlice.ts
index 07d2ca528..4c1509ece 100644
--- a/studio/src/pages/RequestorPage/store/slices/requestResponseSlice.ts
+++ b/studio/src/pages/RequestorPage/store/slices/requestResponseSlice.ts
@@ -193,7 +193,13 @@ export const requestResponseSlice: StateCreator<
showResponseBodyFromHistory: (traceId) =>
set((state) => {
state.activeHistoryResponseTraceId = traceId;
- state.activeResponse = null;
+ // Recall that an 'active response' is one for which we will have the body we received from the service
+ // This means it can contain binary data, whereas the history response will not
+ // We should prefer to keep the active response as response to render, so long as its traceId matches the current history response traceId
+ const activeTraceId = state.activeResponse?.traceId;
+ if (!activeTraceId || activeTraceId !== traceId) {
+ state.activeResponse = null;
+ }
}),
clearResponseBodyFromHistory: () =>
set((state) => {
diff --git a/www/src/content/changelog/!canary.mdx b/www/src/content/changelog/!canary.mdx
index 22f44edcd..1ab032dac 100644
--- a/www/src/content/changelog/!canary.mdx
+++ b/www/src/content/changelog/!canary.mdx
@@ -12,6 +12,8 @@ draft: true
- **Improved Route Updating** We've added some improvements to how we refresh your list of API routes in the Studio.
+- **Render audio files** When your api returns a binary audio response, we will render an audio player for you to listen to it.
+
### Bug fixes
- **D1 autoinstrumentation** The client library, `@fiberplane/hono-otel`, now instruments D1 queries in latest versions of wrangler/miniflare.
@@ -21,3 +23,5 @@ draft: true
- **Side panel not closing** Fixed an issue where the side panel would not close when clicking outside of it.
- **Missing log information** Fixed an issue where logs would not be displayed fully in the logs panel.
+
+- **Render images returned from API** Fixed an issue where images returned from an API request made through Studio would not be rendered in the response panel.