diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx
index 9d5d425e..0aec0b1f 100644
--- a/src-web/components/ResponsePane.tsx
+++ b/src-web/components/ResponsePane.tsx
@@ -1,3 +1,4 @@
+import type { HttpRequest } from '@yaakapp/api';
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo, useMemo } from 'react';
@@ -5,7 +6,6 @@ import { createGlobalState } from 'react-use';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
-import type { HttpRequest } from '@yaakapp/api';
import { isResponseLoading } from '../lib/models';
import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge';
@@ -23,11 +23,10 @@ import { ResponseHeaders } from './ResponseHeaders';
import { ResponseInfo } from './ResponseInfo';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
+import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
-import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
-import { WebPageViewer } from './responseViewers/WebPageViewer';
interface Props {
style?: CSSProperties;
@@ -171,13 +170,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
) : contentType?.match(/csv|tab-separated/) ? (
- ) : viewMode === 'pretty' && contentType?.includes('html') ? (
-
) : (
// ) : viewMode === 'pretty' && contentType?.includes('json') ? (
//
-
diff --git a/src-web/components/responseViewers/HTMLOrTextViewer.tsx b/src-web/components/responseViewers/HTMLOrTextViewer.tsx
new file mode 100644
index 00000000..c63b5eef
--- /dev/null
+++ b/src-web/components/responseViewers/HTMLOrTextViewer.tsx
@@ -0,0 +1,37 @@
+import type { HttpResponse } from '@yaakapp/api';
+import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
+import { useResponseBodyText } from '../../hooks/useResponseBodyText';
+import { isJSON, languageFromContentType } from '../../lib/contentType';
+import { BinaryViewer } from './BinaryViewer';
+import { TextViewer } from './TextViewer';
+import { WebPageViewer } from './WebPageViewer';
+
+interface Props {
+ response: HttpResponse;
+ pretty: boolean;
+ textViewerClassName?: string;
+}
+
+export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
+ const rawBody = useResponseBodyText(response);
+ let language = languageFromContentType(useContentTypeFromHeaders(response.headers));
+
+ // A lot of APIs return JSON with `text/html` content type, so interpret as JSON if so
+ if (language === 'html' && isJSON(rawBody.data ?? '')) {
+ language = 'json';
+ }
+
+ if (rawBody.isLoading) {
+ return null;
+ }
+
+ if (rawBody.data == null) {
+ return ;
+ }
+
+ if (language === 'html' && pretty) {
+ return ;
+ } else {
+ return ;
+ }
+}
diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx
index 76b5ca7e..c19ffd73 100644
--- a/src-web/components/responseViewers/TextViewer.tsx
+++ b/src-web/components/responseViewers/TextViewer.tsx
@@ -9,7 +9,7 @@ import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { useSaveResponse } from '../../hooks/useSaveResponse';
import { useToggle } from '../../hooks/useToggle';
-import { languageFromContentType } from '../../lib/contentType';
+import { isJSON, languageFromContentType } from '../../lib/contentType';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner';
@@ -46,9 +46,15 @@ export function TextViewer({ response, pretty, className }: Props) {
[setFilterTextMap, response],
);
- const saveResponse = useSaveResponse(response);
- const contentType = useContentTypeFromHeaders(response.headers);
const rawBody = useResponseBodyText(response);
+ const saveResponse = useSaveResponse(response);
+ let language = languageFromContentType(useContentTypeFromHeaders(response.headers));
+
+ // A lot of APIs return JSON with `text/html` content type, so interpret as JSON if so
+ if (language === 'html' && isJSON(rawBody.data ?? '')) {
+ language = 'json';
+ }
+
const isSearching = filterText != null;
const filteredResponse = useFilterResponse({
@@ -64,11 +70,7 @@ export function TextViewer({ response, pretty, className }: Props) {
}
}, [isSearching, setFilterText]);
- console.log('HELLO', contentType);
-
- const isJson = contentType?.includes('json');
- const isXml = contentType?.includes('xml') || contentType?.includes('html');
- const canFilter = isJson || isXml;
+ const canFilter = language === 'json' || language === 'xml' || language === 'html';
const actions = useMemo(() => {
const nodes: ReactNode[] = [];
@@ -85,7 +87,7 @@ export function TextViewer({ response, pretty, className }: Props) {
autoFocus
containerClassName="bg-surface"
size="sm"
- placeholder={isJson ? 'JSONPath expression' : 'XPath expression'}
+ placeholder={language === 'json' ? 'JSONPath expression' : 'XPath expression'}
label="Filter expression"
name="filter"
defaultValue={filterText}
@@ -112,8 +114,8 @@ export function TextViewer({ response, pretty, className }: Props) {
canFilter,
filterText,
filteredResponse.error,
- isJson,
isSearching,
+ language,
response.id,
setFilterText,
toggleSearch,
@@ -156,9 +158,9 @@ export function TextViewer({ response, pretty, className }: Props) {
}
const formattedBody =
- pretty && contentType?.includes('json')
+ pretty && language === 'json'
? tryFormatJson(rawBody.data)
- : pretty && contentType?.includes('xml')
+ : pretty && (language === 'xml' || language === 'html')
? tryFormatXml(rawBody.data)
: rawBody.data;
@@ -179,7 +181,7 @@ export function TextViewer({ response, pretty, className }: Props) {
className={className}
forceUpdateKey={body}
defaultValue={body}
- language={languageFromContentType(contentType)}
+ language={language}
actions={actions}
extraExtensions={extraExtensions}
/>
diff --git a/src-web/hooks/useResponseBodyText.ts b/src-web/hooks/useResponseBodyText.ts
index 006804d7..4ee80f48 100644
--- a/src-web/hooks/useResponseBodyText.ts
+++ b/src-web/hooks/useResponseBodyText.ts
@@ -4,7 +4,7 @@ import { getResponseBodyText } from '../lib/responseBody';
export function useResponseBodyText(response: HttpResponse) {
return useQuery({
- queryKey: ['response-body-text', response?.updatedAt],
+ queryKey: ['response-body-text', response.id, response?.updatedAt],
queryFn: () => getResponseBodyText(response),
});
}
diff --git a/src-web/lib/contentType.ts b/src-web/lib/contentType.ts
index 7d85103c..4329160f 100644
--- a/src-web/lib/contentType.ts
+++ b/src-web/lib/contentType.ts
@@ -14,3 +14,12 @@ export function languageFromContentType(contentType: string | null): EditorProps
return 'text';
}
}
+
+export function isJSON(text: string): boolean {
+ try {
+ JSON.parse(text);
+ return true;
+ } catch (_) {
+ return false;
+ }
+}