mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-19 07:19:45 +02:00
fix http response load when filter (#251)
This commit is contained in:
11
src-tauri/Cargo.lock
generated
11
src-tauri/Cargo.lock
generated
@@ -822,6 +822,16 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
@@ -7826,6 +7836,7 @@ dependencies = [
|
|||||||
name = "yaak-app"
|
name = "yaak-app"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"charset",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cookie",
|
"cookie",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ yaak-sse = { workspace = true }
|
|||||||
yaak-sync = { workspace = true }
|
yaak-sync = { workspace = true }
|
||||||
yaak-templates = { workspace = true }
|
yaak-templates = { workspace = true }
|
||||||
yaak-ws = { path = "yaak-ws" }
|
yaak-ws = { path = "yaak-ws" }
|
||||||
|
charset = "0.1.5"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
use encoding_rs::SHIFT_JIS;
|
use log::debug;
|
||||||
|
use mime_guess::{Mime, mime};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str::FromStr;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use yaak_models::models::HttpResponse;
|
|
||||||
|
|
||||||
pub async fn read_response_body<'a>(
|
pub async fn read_response_body(body_path: impl AsRef<Path>, content_type: &str) -> Option<String> {
|
||||||
response: HttpResponse,
|
let body = fs::read(body_path).await.ok()?;
|
||||||
) -> Option<String> {
|
let body_charset = parse_charset(content_type).unwrap_or("utf-8".to_string());
|
||||||
let body_path = match response.body_path {
|
debug!("body_charset: {}", body_charset);
|
||||||
None => return None,
|
if let Some(decoder) = charset::Charset::for_label(body_charset.as_bytes()) {
|
||||||
Some(p) => p,
|
debug!("Using decoder for charset: {}", body_charset);
|
||||||
};
|
let (cow, real_encoding, exist_replace) = decoder.decode(&body);
|
||||||
|
debug!(
|
||||||
|
"Decoded body with charset: {}, real_encoding: {:?}, exist_replace: {}",
|
||||||
|
body_charset, real_encoding, exist_replace
|
||||||
|
);
|
||||||
|
return cow.into_owned().into();
|
||||||
|
}
|
||||||
|
|
||||||
let body = fs::read(body_path).await.unwrap();
|
Some(String::from_utf8_lossy(&body).to_string())
|
||||||
let (s, _, _) = SHIFT_JIS.decode(body.as_slice());
|
}
|
||||||
Some(s.to_string())
|
|
||||||
|
fn parse_charset(content_type: &str) -> Option<String> {
|
||||||
|
let mime: Mime = Mime::from_str(content_type).ok()?;
|
||||||
|
mime.get_param(mime::CHARSET).map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -726,33 +726,43 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_filter_response<R: Runtime>(
|
async fn cmd_http_response_body<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
response_id: &str,
|
|
||||||
plugin_manager: State<'_, PluginManager>,
|
plugin_manager: State<'_, PluginManager>,
|
||||||
filter: &str,
|
response_id: &str,
|
||||||
|
filter: Option<&str>,
|
||||||
) -> YaakResult<FilterResponse> {
|
) -> YaakResult<FilterResponse> {
|
||||||
let response = app_handle.db().get_http_response(response_id)?;
|
let response = app_handle.db().get_http_response(response_id)?;
|
||||||
|
|
||||||
if let None = response.body_path {
|
let body_path = match response.body_path {
|
||||||
return Err(GenericError("Response body path not set".to_string()));
|
None => {
|
||||||
}
|
return Err(GenericError("Response body path not set".to_string()));
|
||||||
|
|
||||||
let mut content_type = "".to_string();
|
|
||||||
for header in response.headers.iter() {
|
|
||||||
if header.name.to_lowercase() == "content-type" {
|
|
||||||
content_type = header.value.to_string().to_lowercase();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
Some(p) => p,
|
||||||
|
};
|
||||||
|
|
||||||
let body = read_response_body(response)
|
let content_type = response
|
||||||
|
.headers
|
||||||
|
.iter()
|
||||||
|
.find_map(|h| {
|
||||||
|
if h.name.eq_ignore_ascii_case("content-type") { Some(h.value.as_str()) } else { None }
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let body = read_response_body(&body_path, content_type)
|
||||||
.await
|
.await
|
||||||
.ok_or(GenericError("Failed to find response body".to_string()))?;
|
.ok_or(GenericError("Failed to find response body".to_string()))?;
|
||||||
|
|
||||||
// TODO: Have plugins register their own content type (regex?)
|
match filter {
|
||||||
Ok(plugin_manager.filter_data(&window, filter, &body, &content_type).await?)
|
Some(filter) if !filter.is_empty() => {
|
||||||
|
Ok(plugin_manager.filter_data(&window, filter, &body, content_type).await?)
|
||||||
|
}
|
||||||
|
_ => Ok(FilterResponse {
|
||||||
|
content: body,
|
||||||
|
error: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1330,7 +1340,7 @@ pub fn run() {
|
|||||||
cmd_delete_send_history,
|
cmd_delete_send_history,
|
||||||
cmd_dismiss_notification,
|
cmd_dismiss_notification,
|
||||||
cmd_export_data,
|
cmd_export_data,
|
||||||
cmd_filter_response,
|
cmd_http_response_body,
|
||||||
cmd_format_json,
|
cmd_format_json,
|
||||||
cmd_get_http_authentication_summaries,
|
cmd_get_http_authentication_summaries,
|
||||||
cmd_get_http_authentication_config,
|
cmd_get_http_authentication_config,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Props) {
|
||||||
const rawTextBody = useResponseBodyText(response);
|
const rawTextBody = useResponseBodyText({ responseId: response.id, filter: null });
|
||||||
const contentType = getContentTypeFromHeaders(response.headers);
|
const contentType = getContentTypeFromHeaders(response.headers);
|
||||||
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
const language = languageFromContentType(contentType, rawTextBody.data ?? '');
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
|
|||||||
if (language === 'html' && pretty) {
|
if (language === 'html' && pretty) {
|
||||||
return <WebPageViewer response={response} />;
|
return <WebPageViewer response={response} />;
|
||||||
} else if (rawTextBody.data == null) {
|
} else if (rawTextBody.data == null) {
|
||||||
return <EmptyStateText>Empty response</EmptyStateText>
|
return <EmptyStateText>Empty response</EmptyStateText>;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<TextViewer
|
<TextViewer
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { ReactNode } from 'react';
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { createGlobalState } from 'react-use';
|
import { createGlobalState } from 'react-use';
|
||||||
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
||||||
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
|
||||||
import { useFormatText } from '../../hooks/useFormatText';
|
import { useFormatText } from '../../hooks/useFormatText';
|
||||||
|
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||||
import type { EditorProps } from '../core/Editor/Editor';
|
import type { EditorProps } from '../core/Editor/Editor';
|
||||||
import { Editor } from '../core/Editor/Editor';
|
import { Editor } from '../core/Editor/Editor';
|
||||||
import { hyperlink } from '../core/Editor/hyperlink/extension';
|
import { hyperlink } from '../core/Editor/hyperlink/extension';
|
||||||
@@ -36,7 +36,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isSearching = filterText != null;
|
const isSearching = filterText != null;
|
||||||
const filteredResponse = useFilterResponse({ filter: debouncedFilterText ?? '', responseId });
|
const filteredResponse = useResponseBodyText({ responseId, filter: debouncedFilterText ?? null });
|
||||||
|
|
||||||
const toggleSearch = useCallback(() => {
|
const toggleSearch = useCallback(() => {
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
@@ -58,7 +58,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
<div key="input" className="w-full !opacity-100">
|
<div key="input" className="w-full !opacity-100">
|
||||||
<Input
|
<Input
|
||||||
key={requestId}
|
key={requestId}
|
||||||
validate={!(filteredResponse.error || filteredResponse.data?.error)}
|
validate={!filteredResponse.error}
|
||||||
hideLabel
|
hideLabel
|
||||||
autoFocus
|
autoFocus
|
||||||
containerClassName="bg-surface"
|
containerClassName="bg-surface"
|
||||||
@@ -91,7 +91,6 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
}, [
|
}, [
|
||||||
canFilter,
|
canFilter,
|
||||||
filterText,
|
filterText,
|
||||||
filteredResponse.data?.error,
|
|
||||||
filteredResponse.error,
|
filteredResponse.error,
|
||||||
filteredResponse.isPending,
|
filteredResponse.isPending,
|
||||||
isSearching,
|
isSearching,
|
||||||
@@ -112,7 +111,7 @@ export function TextViewer({ language, text, responseId, requestId, pretty, clas
|
|||||||
if (filteredResponse.error) {
|
if (filteredResponse.error) {
|
||||||
body = '';
|
body = '';
|
||||||
} else {
|
} else {
|
||||||
body = filteredResponse.data?.content != null ? filteredResponse.data.content : '';
|
body = filteredResponse.data != null ? filteredResponse.data : '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
body = formattedBody;
|
body = formattedBody;
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { FilterResponse } from '@yaakapp-internal/plugins';
|
|
||||||
import { invokeCmd } from '../lib/tauri';
|
|
||||||
|
|
||||||
export function useFilterResponse({
|
|
||||||
responseId,
|
|
||||||
filter,
|
|
||||||
}: {
|
|
||||||
responseId: string | null;
|
|
||||||
filter: string;
|
|
||||||
}) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['filter_response', responseId, filter],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (filter === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = (await invokeCmd('cmd_filter_response', {
|
|
||||||
responseId,
|
|
||||||
filter,
|
|
||||||
})) as FilterResponse;
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
console.log("Failed to filter response:", result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
|
||||||
import { getResponseBodyBlob } from '../lib/responseBody';
|
|
||||||
|
|
||||||
export function useResponseBodyBlob(response: HttpResponse) {
|
|
||||||
return useQuery<Uint8Array | null>({
|
|
||||||
enabled: response != null,
|
|
||||||
queryKey: ['response-body-binary', response?.updatedAt],
|
|
||||||
initialData: null,
|
|
||||||
queryFn: () => getResponseBodyBlob(response),
|
|
||||||
}).data;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
|
||||||
import { getResponseBodyText } from '../lib/responseBody';
|
import { getResponseBodyText } from '../lib/responseBody';
|
||||||
|
|
||||||
export function useResponseBodyText(response: HttpResponse) {
|
export function useResponseBodyText({
|
||||||
|
responseId,
|
||||||
|
filter,
|
||||||
|
}: {
|
||||||
|
responseId: string;
|
||||||
|
filter: string | null;
|
||||||
|
}) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
placeholderData: (prev) => prev, // Keep previous data on refetch
|
queryKey: ['response_body_text', responseId, filter ?? ''],
|
||||||
queryKey: ['response-body-text', response.id, response.updatedAt, response.contentLength],
|
queryFn: () => getResponseBodyText({ responseId, filter }),
|
||||||
queryFn: () => getResponseBodyText(response),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { readFile } from '@tauri-apps/plugin-fs';
|
|
||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
|
import type { FilterResponse } from '@yaakapp-internal/plugins';
|
||||||
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
import type { ServerSentEvent } from '@yaakapp-internal/sse';
|
||||||
import { getCharsetFromContentType } from './model_util';
|
|
||||||
import { invokeCmd } from './tauri';
|
import { invokeCmd } from './tauri';
|
||||||
|
|
||||||
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
|
export async function getResponseBodyText({
|
||||||
if (!response.bodyPath) {
|
responseId,
|
||||||
return null;
|
filter,
|
||||||
|
}: {
|
||||||
|
responseId: string;
|
||||||
|
filter: string | null;
|
||||||
|
}): Promise<string | null> {
|
||||||
|
const result = await invokeCmd<FilterResponse>('cmd_http_response_body', {
|
||||||
|
responseId,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bytes = await readFile(response.bodyPath);
|
return result.content;
|
||||||
const charset = getCharsetFromContentType(response.headers);
|
|
||||||
|
|
||||||
return new TextDecoder(charset ?? 'utf-8', { fatal: false }).decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
|
|
||||||
if (!response.bodyPath) return null;
|
|
||||||
return readFile(response.bodyPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getResponseBodyEventSource(
|
export async function getResponseBodyEventSource(
|
||||||
|
|||||||
@@ -2,30 +2,29 @@ import type { InvokeArgs } from '@tauri-apps/api/core';
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
type TauriCmd =
|
type TauriCmd =
|
||||||
| 'cmd_get_themes'
|
|
||||||
| 'cmd_call_http_authentication_action'
|
|
||||||
| 'cmd_call_grpc_request_action'
|
| 'cmd_call_grpc_request_action'
|
||||||
|
| 'cmd_call_http_authentication_action'
|
||||||
| 'cmd_call_http_request_action'
|
| 'cmd_call_http_request_action'
|
||||||
| 'cmd_check_for_updates'
|
| 'cmd_check_for_updates'
|
||||||
| 'cmd_create_grpc_request'
|
| 'cmd_create_grpc_request'
|
||||||
| 'cmd_curl_to_request'
|
| 'cmd_curl_to_request'
|
||||||
| 'cmd_decrypt_template'
|
| 'cmd_decrypt_template'
|
||||||
| 'cmd_secure_template'
|
|
||||||
| 'cmd_delete_all_grpc_connections'
|
| 'cmd_delete_all_grpc_connections'
|
||||||
| 'cmd_delete_all_http_responses'
|
| 'cmd_delete_all_http_responses'
|
||||||
| 'cmd_delete_send_history'
|
| 'cmd_delete_send_history'
|
||||||
| 'cmd_dismiss_notification'
|
| 'cmd_dismiss_notification'
|
||||||
| 'cmd_export_data'
|
| 'cmd_export_data'
|
||||||
| 'cmd_filter_response'
|
|
||||||
| 'cmd_format_json'
|
| 'cmd_format_json'
|
||||||
| 'cmd_get_http_authentication_config'
|
| 'cmd_get_http_authentication_config'
|
||||||
| 'cmd_get_http_authentication_summaries'
|
| 'cmd_get_http_authentication_summaries'
|
||||||
| 'cmd_get_sse_events'
|
| 'cmd_get_sse_events'
|
||||||
|
| 'cmd_get_themes'
|
||||||
| 'cmd_get_workspace_meta'
|
| 'cmd_get_workspace_meta'
|
||||||
| 'cmd_grpc_go'
|
| 'cmd_grpc_go'
|
||||||
| 'cmd_grpc_reflect'
|
| 'cmd_grpc_reflect'
|
||||||
| 'cmd_grpc_request_actions'
|
| 'cmd_grpc_request_actions'
|
||||||
| 'cmd_http_request_actions'
|
| 'cmd_http_request_actions'
|
||||||
|
| 'cmd_http_response_body'
|
||||||
| 'cmd_import_data'
|
| 'cmd_import_data'
|
||||||
| 'cmd_install_plugin'
|
| 'cmd_install_plugin'
|
||||||
| 'cmd_metadata'
|
| 'cmd_metadata'
|
||||||
@@ -35,6 +34,7 @@ type TauriCmd =
|
|||||||
| 'cmd_reload_plugins'
|
| 'cmd_reload_plugins'
|
||||||
| 'cmd_render_template'
|
| 'cmd_render_template'
|
||||||
| 'cmd_save_response'
|
| 'cmd_save_response'
|
||||||
|
| 'cmd_secure_template'
|
||||||
| 'cmd_send_ephemeral_request'
|
| 'cmd_send_ephemeral_request'
|
||||||
| 'cmd_send_http_request'
|
| 'cmd_send_http_request'
|
||||||
| 'cmd_show_workspace_key'
|
| 'cmd_show_workspace_key'
|
||||||
|
|||||||
Reference in New Issue
Block a user