diff --git a/src-tauri/yaak-http/src/types.rs b/src-tauri/yaak-http/src/types.rs index cd3432f4..135ce8bb 100644 --- a/src-tauri/yaak-http/src/types.rs +++ b/src-tauri/yaak-http/src/types.rs @@ -326,8 +326,14 @@ async fn build_multipart_body( if file_path.is_empty() { // Text field - let header = - format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n{}", name, value); + let header = if !content_type.is_empty() { + format!( + "Content-Disposition: form-data; name=\"{}\"\r\nContent-Type: {}\r\n\r\n{}", + name, content_type, value + ) + } else { + format!("Content-Disposition: form-data; name=\"{}\"\r\n\r\n{}", name, value) + }; let header_bytes = header.into_bytes(); total_size += header_bytes.len(); readers.push(ReaderType::Bytes(header_bytes)); diff --git a/src-web/components/HttpResponsePane.tsx b/src-web/components/HttpResponsePane.tsx index 6465bc24..02c575c8 100644 --- a/src-web/components/HttpResponsePane.tsx +++ b/src-web/components/HttpResponsePane.tsx @@ -6,6 +6,7 @@ import { useLocalStorage } from 'react-use'; 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 { getMimeTypeFromContentType } from '../lib/contentType'; import { getContentTypeFromHeaders } from '../lib/model_util'; @@ -216,19 +217,19 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { ) : 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) ? ( - + ) : mimeType?.match(/^multipart/i) && viewMode === 'pretty' ? ( + ) : mimeType?.match(/pdf/i) ? ( ) : mimeType?.match(/csv|tab-separated/i) ? ( - + ) : ( ; } + +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 ; +} diff --git a/src-web/components/RequestBodyViewer.tsx b/src-web/components/RequestBodyViewer.tsx index 215c5d25..7e0ccd51 100644 --- a/src-web/components/RequestBodyViewer.tsx +++ b/src-web/components/RequestBodyViewer.tsx @@ -1,9 +1,21 @@ import type { HttpResponse } from '@yaakapp-internal/models'; +import { lazy, Suspense } from 'react'; import { useHttpRequestBody } from '../hooks/useHttpRequestBody'; -import { languageFromContentType } from '../lib/contentType'; +import { getMimeTypeFromContentType, languageFromContentType } from '../lib/contentType'; import { EmptyStateText } from './EmptyStateText'; -import { Editor } from './core/Editor/LazyEditor'; import { LoadingIcon } from './core/LoadingIcon'; +import { AudioViewer } from './responseViewers/AudioViewer'; +import { CsvViewer } from './responseViewers/CsvViewer'; +import { ImageViewer } from './responseViewers/ImageViewer'; +import { MultipartViewer } from './responseViewers/MultipartViewer'; +import { SvgViewer } from './responseViewers/SvgViewer'; +import { TextViewer } from './responseViewers/TextViewer'; +import { VideoViewer } from './responseViewers/VideoViewer'; +import { WebPageViewer } from './responseViewers/WebPageViewer'; + +const PdfViewer = lazy(() => + import('./responseViewers/PdfViewer').then((m) => ({ default: m.PdfViewer })), +); interface Props { response: HttpResponse; @@ -32,21 +44,59 @@ function RequestBodyViewerInner({ response }: Props) { return No request body; } - const { bodyText } = data; + const { bodyText, body } = data; // Try to detect language from content-type header that was sent const contentTypeHeader = response.requestHeaders.find( (h) => h.name.toLowerCase() === 'content-type', ); const contentType = contentTypeHeader?.value ?? null; + const mimeType = contentType ? getMimeTypeFromContentType(contentType).essence : null; const language = languageFromContentType(contentType, bodyText); + // Route to appropriate viewer based on content type + if (mimeType?.match(/^multipart/i)) { + const boundary = contentType?.split('boundary=')[1] ?? 'unknown'; + // Create a copy because parseMultipart may detach the buffer + const bodyCopy = new Uint8Array(body); + return ( + + ); + } + + if (mimeType?.match(/^image\/svg/i)) { + return ; + } + + if (mimeType?.match(/^image/i)) { + return ; + } + + if (mimeType?.match(/^audio/i)) { + return ; + } + + if (mimeType?.match(/^video/i)) { + return ; + } + + if (mimeType?.match(/csv|tab-separated/i)) { + return ; + } + + if (mimeType?.match(/^text\/html/i)) { + return ; + } + + if (mimeType?.match(/pdf/i)) { + return ( + }> + + + ); + } + return ( - + ); } diff --git a/src-web/components/RouteError.tsx b/src-web/components/RouteError.tsx index fdce24e3..d6ec5cdc 100644 --- a/src-web/components/RouteError.tsx +++ b/src-web/components/RouteError.tsx @@ -20,7 +20,7 @@ export default function RouteError({ error }: { error: unknown }) { {stack && (
{stack}
diff --git a/src-web/components/core/FormattedError.tsx b/src-web/components/core/FormattedError.tsx index 0d95cade..b7889467 100644 --- a/src-web/components/core/FormattedError.tsx +++ b/src-web/components/core/FormattedError.tsx @@ -3,12 +3,14 @@ import type { ReactNode } from 'react'; interface Props { children: ReactNode; + className?: string; } -export function FormattedError({ children }: Props) { +export function FormattedError({ children, className }: Props) { return (
();
+
+  useEffect(() => {
+    if (bodyPath) {
+      setSrc(convertFileSrc(bodyPath));
+    } else if (data) {
+      const blob = new Blob([data], { type: 'audio/mpeg' });
+      const url = URL.createObjectURL(blob);
+      setSrc(url);
+      return () => URL.revokeObjectURL(url);
+    } else {
+      setSrc(undefined);
+    }
+  }, [bodyPath, data]);
 
   // biome-ignore lint/a11y/useMediaCaption: none
   return