import type { HttpResponse, HttpResponseEvent } from "@yaakapp-internal/models"; import classNames from "classnames"; import type { ComponentType, CSSProperties } from "react"; import { lazy, Suspense, useMemo } from "react"; import { useCancelHttpResponse } from "../hooks/useCancelHttpResponse"; import { useHttpResponseEvents } from "../hooks/useHttpResponseEvents"; import { usePinnedHttpResponse } from "../hooks/usePinnedHttpResponse"; import { useResponseBodyBytes, useResponseBodyText } from "../hooks/useResponseBodyText"; import { useResponseViewMode } from "../hooks/useResponseViewMode"; import { useTimelineViewMode } from "../hooks/useTimelineViewMode"; import { getMimeTypeFromContentType } from "../lib/contentType"; import { getContentTypeFromHeaders, getCookieCounts } from "../lib/model_util"; import { ConfirmLargeResponse } from "./ConfirmLargeResponse"; import { ConfirmLargeResponseRequest } from "./ConfirmLargeResponseRequest"; import { Banner } from "./core/Banner"; import { Button } from "./core/Button"; import { CountBadge } from "./core/CountBadge"; import { HotkeyList } from "./core/HotkeyList"; import { HttpResponseDurationTag } from "./core/HttpResponseDurationTag"; import { HttpStatusTag } from "./core/HttpStatusTag"; import { Icon } from "./core/Icon"; import { LoadingIcon } from "./core/LoadingIcon"; import { PillButton } from "./core/PillButton"; import { SizeTag } from "./core/SizeTag"; import { HStack, VStack } from "./core/Stacks"; import type { TabItem } from "./core/Tabs/Tabs"; import { TabContent, Tabs } from "./core/Tabs/Tabs"; import { Tooltip } from "./core/Tooltip"; import { EmptyStateText } from "./EmptyStateText"; import { ErrorBoundary } from "./ErrorBoundary"; import { HttpResponseTimeline } from "./HttpResponseTimeline"; import { RecentHttpResponsesDropdown } from "./RecentHttpResponsesDropdown"; import { RequestBodyViewer } from "./RequestBodyViewer"; import { ResponseCookies } from "./ResponseCookies"; import { ResponseHeaders } from "./ResponseHeaders"; import { AudioViewer } from "./responseViewers/AudioViewer"; import { CsvViewer } from "./responseViewers/CsvViewer"; import { EventStreamViewer } from "./responseViewers/EventStreamViewer"; import { HTMLOrTextViewer } from "./responseViewers/HTMLOrTextViewer"; import { ImageViewer } from "./responseViewers/ImageViewer"; import { MultipartViewer } from "./responseViewers/MultipartViewer"; import { SvgViewer } from "./responseViewers/SvgViewer"; import { VideoViewer } from "./responseViewers/VideoViewer"; const PdfViewer = lazy(() => import("./responseViewers/PdfViewer").then((m) => ({ default: m.PdfViewer })), ); interface Props { style?: CSSProperties; className?: string; activeRequestId: string; } const TAB_BODY = "body"; const TAB_REQUEST = "request"; const TAB_HEADERS = "headers"; const TAB_COOKIES = "cookies"; const TAB_TIMELINE = "timeline"; export type TimelineViewMode = "timeline" | "text"; interface RedirectDropWarning { droppedBodyCount: number; droppedHeaders: string[]; } export function HttpResponsePane({ style, className, activeRequestId }: Props) { const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId); const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId); const [timelineViewMode, setTimelineViewMode] = useTimelineViewMode(); const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null); const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence; const responseEvents = useHttpResponseEvents(activeResponse); const redirectDropWarning = useMemo( () => getRedirectDropWarning(responseEvents.data), [responseEvents.data], ); const shouldShowRedirectDropWarning = activeResponse?.state === "closed" && redirectDropWarning != null; const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]); const tabs = useMemo( () => [ { value: TAB_BODY, label: "Response", options: { value: viewMode, onChange: setViewMode, items: [ { label: "Response", value: "pretty" }, ...(mimeType?.startsWith("image") ? [] : [{ label: "Response (Raw)", shortLabel: "Raw", value: "raw" }]), ], }, }, { value: TAB_REQUEST, label: "Request", rightSlot: (activeResponse?.requestContentLength ?? 0) > 0 ? : null, }, { value: TAB_HEADERS, label: "Headers", rightSlot: ( ), }, { value: TAB_COOKIES, label: "Cookies", rightSlot: cookieCounts.sent > 0 || cookieCounts.received > 0 ? ( ) : null, }, { value: TAB_TIMELINE, rightSlot: , options: { value: timelineViewMode, onChange: (v) => setTimelineViewMode((v as TimelineViewMode) ?? "timeline"), items: [ { label: "Timeline", value: "timeline" }, { label: "Timeline (Text)", shortLabel: "Timeline", value: "text" }, ], }, }, ], [ activeResponse?.headers, activeResponse?.requestContentLength, activeResponse?.requestHeaders.length, cookieCounts.sent, cookieCounts.received, mimeType, responseEvents.data?.length, setViewMode, viewMode, timelineViewMode, setTimelineViewMode, ], ); const cancel = useCancelHttpResponse(activeResponse?.id ?? null); return (
{activeResponse == null ? ( ) : (
{activeResponse && (
{activeResponse.state !== "closed" && } {shouldShowRedirectDropWarning ? ( Redirect changed this request {redirectDropWarning.droppedBodyCount > 0 && ( Body dropped on {redirectDropWarning.droppedBodyCount}{" "} {redirectDropWarning.droppedBodyCount === 1 ? "redirect hop" : "redirect hops"} )} {redirectDropWarning.droppedHeaders.length > 0 && ( Headers dropped:{" "} {redirectDropWarning.droppedHeaders.join(", ")} )} See Timeline for details. } > } > {getRedirectWarningLabel(redirectDropWarning)} ) : ( )}
)}
{activeResponse?.error && ( {activeResponse.error} )} {/* Show tabs if we have any data (headers, body, etc.) even if there's an error */} {activeResponse.state === "initialized" ? ( Sending Request ) : activeResponse.state === "closed" && (activeResponse.contentLength ?? 0) === 0 ? ( Empty ) : mimeType?.match(/^text\/event-stream/i) && viewMode === "pretty" ? ( ) : mimeType?.match(/^image\/svg/) ? ( ) : mimeType?.match(/^image/i) ? ( ) : mimeType?.match(/^audio/i) ? ( ) : mimeType?.match(/^video/i) ? ( ) : mimeType?.match(/^multipart/i) && viewMode === "pretty" ? ( ) : mimeType?.match(/pdf/i) ? ( ) : mimeType?.match(/csv|tab-separated/i) && viewMode === "pretty" ? ( ) : ( )}
)}
); } function getRedirectDropWarning( events: HttpResponseEvent[] | undefined, ): RedirectDropWarning | null { if (events == null || events.length === 0) return null; let droppedBodyCount = 0; const droppedHeaders = new Set(); for (const e of events) { const event = e.event; if (event.type !== "redirect") { continue; } if (event.dropped_body) { droppedBodyCount += 1; } for (const headerName of event.dropped_headers ?? []) { pushHeaderName(droppedHeaders, headerName); } } if (droppedBodyCount === 0 && droppedHeaders.size === 0) { return null; } return { droppedBodyCount, droppedHeaders: Array.from(droppedHeaders).sort(), }; } function pushHeaderName(headers: Set, headerName: string): void { const existing = Array.from(headers).find((h) => h.toLowerCase() === headerName.toLowerCase()); if (existing == null) { headers.add(headerName); } } function getRedirectWarningLabel(warning: RedirectDropWarning): string { if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) { return "Dropped body and headers"; } if (warning.droppedBodyCount > 0) { return "Dropped body"; } return "Dropped headers"; } function EnsureCompleteResponse({ response, Component, }: { response: HttpResponse; Component: ComponentType<{ bodyPath: string }>; }) { if (response.bodyPath === null) { return
Empty response body
; } // Wait until the response has been fully-downloaded if (response.state !== "closed") { return ( ); } return ; } function HttpSvgViewer({ response }: { response: HttpResponse }) { const body = useResponseBodyText({ response, filter: null }); if (!body.data) return null; return ; } function HttpCsvViewer({ response, className }: { response: HttpResponse; className?: string }) { const body = useResponseBodyText({ response, filter: null }); return ; } function HttpMultipartViewer({ response }: { response: HttpResponse }) { const body = useResponseBodyBytes({ response }); if (body.data == null) return null; const contentTypeHeader = getContentTypeFromHeaders(response.headers); const boundary = contentTypeHeader?.split("boundary=")[1] ?? "unknown"; return ; }