fix 3
This commit is contained in:
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
use tokio::sync::{Mutex, oneshot};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct HttpResponse {
|
struct HttpResponse {
|
||||||
@@ -9,6 +10,15 @@ struct HttpResponse {
|
|||||||
headers: HashMap<String, String>,
|
headers: HashMap<String, String>,
|
||||||
body: String,
|
body: String,
|
||||||
time_elapsed: u128, // milliseconds
|
time_elapsed: u128, // milliseconds
|
||||||
|
headers_size: usize,
|
||||||
|
body_size: usize,
|
||||||
|
total_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct AppError {
|
||||||
|
code: String,
|
||||||
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -42,19 +52,84 @@ struct AuthConfig {
|
|||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
pending_requests: Mutex<HashMap<String, oneshot::Sender<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppError {
|
||||||
|
fn new(code: &str, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: code.to_string(),
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_reqwest_error(error: reqwest::Error) -> AppError {
|
||||||
|
if error.is_timeout() {
|
||||||
|
return AppError::new("timeout", "The request timed out after 30 seconds.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.is_connect() {
|
||||||
|
return AppError::new("connect", format!("Failed to connect: {error}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.is_decode() {
|
||||||
|
return AppError::new("decode", format!("Failed to decode response body: {error}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.is_request() {
|
||||||
|
return AppError::new("request", format!("Failed to build or send the request: {error}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
AppError::new("network", format!("Request failed: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn perform_request(
|
||||||
|
request_builder: reqwest::RequestBuilder,
|
||||||
|
) -> Result<HttpResponse, AppError> {
|
||||||
|
let start_time = Instant::now();
|
||||||
|
let response = request_builder.send().await.map_err(map_reqwest_error)?;
|
||||||
|
let time_elapsed = start_time.elapsed().as_millis();
|
||||||
|
|
||||||
|
let status = response.status().as_u16();
|
||||||
|
|
||||||
|
let mut response_headers = HashMap::new();
|
||||||
|
let mut headers_size = 2usize;
|
||||||
|
for (key, value) in response.headers() {
|
||||||
|
let val_bytes = value.as_bytes();
|
||||||
|
let val_str = value.to_str().unwrap_or("").to_string();
|
||||||
|
headers_size += key.as_str().len() + 2 + val_bytes.len() + 2;
|
||||||
|
response_headers.insert(key.to_string(), val_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body_text = response.text().await.map_err(map_reqwest_error)?;
|
||||||
|
let body_size = body_text.as_bytes().len();
|
||||||
|
let total_size = headers_size + body_size;
|
||||||
|
|
||||||
|
Ok(HttpResponse {
|
||||||
|
status,
|
||||||
|
headers: response_headers,
|
||||||
|
body: body_text,
|
||||||
|
time_elapsed,
|
||||||
|
headers_size,
|
||||||
|
body_size,
|
||||||
|
total_size,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn execute_request(
|
async fn execute_request(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
request_id: String,
|
||||||
method: String,
|
method: String,
|
||||||
url: String,
|
url: String,
|
||||||
headers: HashMap<String, String>,
|
headers: HashMap<String, String>,
|
||||||
body: Option<String>,
|
body: Option<String>,
|
||||||
query_params: Option<HashMap<String, String>>,
|
query_params: Option<HashMap<String, String>>,
|
||||||
auth: Option<AuthConfig>,
|
auth: Option<AuthConfig>,
|
||||||
) -> Result<HttpResponse, String> {
|
) -> Result<HttpResponse, AppError> {
|
||||||
let req_method = method.parse::<reqwest::Method>().map_err(|e| e.to_string())?;
|
let req_method = method.parse::<reqwest::Method>()
|
||||||
|
.map_err(|e| AppError::new("invalid_method", format!("Invalid HTTP method: {e}")))?;
|
||||||
let mut request_builder = state.client.request(req_method, &url);
|
let mut request_builder = state.client.request(req_method, &url);
|
||||||
|
|
||||||
// Add Query Params
|
// Add Query Params
|
||||||
@@ -99,29 +174,40 @@ async fn execute_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
|
||||||
// Execute request
|
{
|
||||||
let response = request_builder.send().await.map_err(|e| e.to_string())?;
|
let mut pending_requests = state.pending_requests.lock().await;
|
||||||
let time_elapsed = start_time.elapsed().as_millis();
|
pending_requests.insert(request_id.clone(), cancel_tx);
|
||||||
|
|
||||||
let status = response.status().as_u16();
|
|
||||||
|
|
||||||
let mut response_headers = HashMap::new();
|
|
||||||
for (key, value) in response.headers() {
|
|
||||||
// Handle header value to string conversion (skipping non-utf8 for simplicity or lossy conv)
|
|
||||||
let val_str = value.to_str().unwrap_or("").to_string();
|
|
||||||
// Capitalize or keep standard key format? keeping standard.
|
|
||||||
response_headers.insert(key.to_string(), val_str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let body_text = response.text().await.map_err(|e| e.to_string())?;
|
let result = tokio::select! {
|
||||||
|
_ = &mut cancel_rx => Err(AppError::new("canceled", "The request was canceled.")),
|
||||||
|
result = perform_request(request_builder) => result,
|
||||||
|
};
|
||||||
|
|
||||||
Ok(HttpResponse {
|
let mut pending_requests = state.pending_requests.lock().await;
|
||||||
status,
|
pending_requests.remove(&request_id);
|
||||||
headers: response_headers,
|
|
||||||
body: body_text,
|
result
|
||||||
time_elapsed,
|
}
|
||||||
})
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cancel_request(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
request_id: String,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
|
let sender = {
|
||||||
|
let mut pending_requests = state.pending_requests.lock().await;
|
||||||
|
pending_requests.remove(&request_id)
|
||||||
|
};
|
||||||
|
|
||||||
|
match sender {
|
||||||
|
Some(cancel_tx) => {
|
||||||
|
let _ = cancel_tx.send(());
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
None => Ok(false),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -132,9 +218,12 @@ pub fn run() {
|
|||||||
.expect("failed to create HTTP client");
|
.expect("failed to create HTTP client");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(AppState { client })
|
.manage(AppState {
|
||||||
|
client,
|
||||||
|
pending_requests: Mutex::new(HashMap::new()),
|
||||||
|
})
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![execute_request])
|
.invoke_handler(tauri::generate_handler![execute_request, cancel_request])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,120 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, ref } from 'vue';
|
import { computed, defineAsyncComponent, ref } from 'vue';
|
||||||
import { useRequestStore } from '../stores/requestStore';
|
import { useRequestStore, type RequestError, type KeyValue } from '../stores/requestStore';
|
||||||
import KeyValueEditor from './KeyValueEditor.vue';
|
import KeyValueEditor from './KeyValueEditor.vue';
|
||||||
import AuthPanel from './AuthPanel.vue';
|
import AuthPanel from './AuthPanel.vue';
|
||||||
import CustomSelect from './CustomSelect.vue';
|
import CustomSelect from './CustomSelect.vue';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { Play, Loader2 } from 'lucide-vue-next';
|
import { Play, Loader2, Square } from 'lucide-vue-next';
|
||||||
|
|
||||||
const CodeEditor = defineAsyncComponent(() => import('./CodeEditor.vue'));
|
const CodeEditor = defineAsyncComponent(() => import('./CodeEditor.vue'));
|
||||||
const store = useRequestStore();
|
const store = useRequestStore();
|
||||||
const activeTab = ref('params');
|
const activeTab = ref('params');
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
const currentRequestId = ref<string | null>(null);
|
||||||
|
const bodyMode = ref<'auto' | 'json' | 'text'>('auto');
|
||||||
|
|
||||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
||||||
|
const bodyModeOptions = [
|
||||||
|
{ value: 'auto', label: 'Auto' },
|
||||||
|
{ value: 'json', label: 'JSON' },
|
||||||
|
{ value: 'text', label: 'Text' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const methodSupportsBody = computed(() => !['GET', 'HEAD'].includes(store.activeRequest.method));
|
||||||
|
|
||||||
|
const inferredBodyLanguage = computed<'json' | 'text'>(() => {
|
||||||
|
if (bodyMode.value !== 'auto') return bodyMode.value;
|
||||||
|
|
||||||
|
const contentTypeHeader = store.activeRequest.headers.find((header) => (
|
||||||
|
header.enabled && header.key.trim().toLowerCase() === 'content-type'
|
||||||
|
));
|
||||||
|
const contentType = contentTypeHeader?.value.trim().toLowerCase() || '';
|
||||||
|
return contentType.includes('json') ? 'json' : 'text';
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestBodyPlaceholder = computed(() => (
|
||||||
|
inferredBodyLanguage.value === 'json'
|
||||||
|
? 'Request Body (JSON)'
|
||||||
|
: 'Request Body'
|
||||||
|
));
|
||||||
|
|
||||||
|
const normalizeError = (error: unknown): RequestError => {
|
||||||
|
if (error && typeof error === 'object' && 'code' in error && 'message' in error) {
|
||||||
|
return {
|
||||||
|
code: String((error as { code: unknown }).code),
|
||||||
|
message: String((error as { message: unknown }).message),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 'unknown',
|
||||||
|
message: String(error),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const findDuplicateKey = (items: KeyValue[]) => {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const key = item.key.trim().toLowerCase();
|
||||||
|
if (!item.enabled || !key) continue;
|
||||||
|
if (seen.has(key)) return item.key.trim();
|
||||||
|
seen.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateRequest = (): RequestError | null => {
|
||||||
|
const url = store.activeRequest.url.trim();
|
||||||
|
if (!url) {
|
||||||
|
return { code: 'validation', message: 'URL is required.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedUrl: URL;
|
||||||
|
try {
|
||||||
|
parsedUrl = new URL(url);
|
||||||
|
} catch {
|
||||||
|
return { code: 'invalid_url', message: 'URL is invalid. Use a full http:// or https:// address.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
||||||
|
return { code: 'invalid_url', message: 'Only http:// and https:// URLs are supported.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateHeader = findDuplicateKey(store.activeRequest.headers);
|
||||||
|
if (duplicateHeader) {
|
||||||
|
return { code: 'duplicate_header', message: `Duplicate enabled header: ${duplicateHeader}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicateParam = findDuplicateKey(store.activeRequest.params);
|
||||||
|
if (duplicateParam) {
|
||||||
|
return { code: 'duplicate_query_param', message: `Duplicate enabled query param: ${duplicateParam}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inferredBodyLanguage.value === 'json' && store.activeRequest.body.trim()) {
|
||||||
|
try {
|
||||||
|
JSON.parse(store.activeRequest.body);
|
||||||
|
} catch {
|
||||||
|
return { code: 'invalid_json', message: 'Request body is set to JSON mode but contains invalid JSON.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const executeRequest = async () => {
|
const executeRequest = async () => {
|
||||||
if (isLoading.value || !store.activeRequest.url) return;
|
if (isLoading.value || !store.activeRequest.url) return;
|
||||||
|
|
||||||
|
const validationError = validateRequest();
|
||||||
|
if (validationError) {
|
||||||
|
store.setRequestError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
|
currentRequestId.value = requestId;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
store.setRequestError(null);
|
store.setRequestError(null);
|
||||||
|
|
||||||
@@ -28,10 +126,11 @@ const executeRequest = async () => {
|
|||||||
store.activeRequest.params.filter(p => p.enabled && p.key).forEach(p => params[p.key] = p.value);
|
store.activeRequest.params.filter(p => p.enabled && p.key).forEach(p => params[p.key] = p.value);
|
||||||
|
|
||||||
const response: any = await invoke('execute_request', {
|
const response: any = await invoke('execute_request', {
|
||||||
|
requestId,
|
||||||
method: store.activeRequest.method,
|
method: store.activeRequest.method,
|
||||||
url: store.activeRequest.url,
|
url: store.activeRequest.url,
|
||||||
headers,
|
headers,
|
||||||
body: store.activeRequest.body || null,
|
body: store.activeRequest.body.trim() ? store.activeRequest.body : null,
|
||||||
queryParams: params,
|
queryParams: params,
|
||||||
auth: store.activeRequest.auth,
|
auth: store.activeRequest.auth,
|
||||||
});
|
});
|
||||||
@@ -42,7 +141,9 @@ const executeRequest = async () => {
|
|||||||
headers: response.headers,
|
headers: response.headers,
|
||||||
body: response.body,
|
body: response.body,
|
||||||
time: response.time_elapsed,
|
time: response.time_elapsed,
|
||||||
size: new Blob([response.body]).size
|
size: response.total_size,
|
||||||
|
headersSize: response.headers_size,
|
||||||
|
bodySize: response.body_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.addToHistory(store.activeRequest);
|
store.addToHistory(store.activeRequest);
|
||||||
@@ -50,11 +151,23 @@ const executeRequest = async () => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
store.activeRequest.response = undefined;
|
store.activeRequest.response = undefined;
|
||||||
store.setRequestError(String(error));
|
store.setRequestError(normalizeError(error));
|
||||||
} finally {
|
} finally {
|
||||||
|
currentRequestId.value = null;
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelRequest = async () => {
|
||||||
|
if (!currentRequestId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('cancel_request', { requestId: currentRequestId.value });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
store.setRequestError(normalizeError(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -76,21 +189,30 @@ const executeRequest = async () => {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
v-if="!isLoading"
|
||||||
@click="executeRequest"
|
@click="executeRequest"
|
||||||
:disabled="isLoading"
|
class="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg font-medium text-sm flex items-center gap-2 transition-all shadow-lg shadow-indigo-900/20"
|
||||||
class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg font-medium text-sm flex items-center gap-2 transition-all shadow-lg shadow-indigo-900/20"
|
|
||||||
>
|
>
|
||||||
<Loader2 v-if="isLoading" class="w-4 h-4 animate-spin" />
|
<Play class="w-4 h-4 fill-current" />
|
||||||
<Play v-else class="w-4 h-4 fill-current" />
|
|
||||||
Send
|
Send
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
@click="cancelRequest"
|
||||||
|
class="bg-rose-600 hover:bg-rose-500 text-white px-4 py-2 rounded-lg font-medium text-sm flex items-center gap-2 transition-all shadow-lg shadow-rose-900/20"
|
||||||
|
>
|
||||||
|
<Loader2 class="w-4 h-4 animate-spin" />
|
||||||
|
<Square class="w-3.5 h-3.5 fill-current" />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="store.requestError"
|
v-if="store.requestError"
|
||||||
class="mx-4 mt-4 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-200"
|
class="mx-4 mt-4 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-200"
|
||||||
>
|
>
|
||||||
{{ store.requestError }}
|
<div class="font-medium uppercase tracking-wide text-[11px] text-rose-300/80">{{ store.requestError.code }}</div>
|
||||||
|
<div class="mt-1">{{ store.requestError.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Tabs -->
|
<!-- Configuration Tabs -->
|
||||||
@@ -121,10 +243,26 @@ const executeRequest = async () => {
|
|||||||
/>
|
/>
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
<div v-if="activeTab === 'body'" class="h-full w-full overflow-hidden">
|
<div v-if="activeTab === 'body'" class="h-full w-full overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between border-b border-slate-800 px-4 py-2 text-xs">
|
||||||
|
<div class="text-slate-500">
|
||||||
|
Mode: <span class="text-slate-300 uppercase">{{ inferredBodyLanguage }}</span>
|
||||||
|
</div>
|
||||||
|
<CustomSelect
|
||||||
|
v-model="bodyMode"
|
||||||
|
:options="bodyModeOptions"
|
||||||
|
triggerClass="bg-slate-950 text-xs font-medium px-3 py-1.5 text-slate-200 border border-slate-800 rounded-md focus:outline-none hover:bg-slate-900 cursor-pointer min-w-[88px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!methodSupportsBody"
|
||||||
|
class="mx-4 mt-4 rounded-lg border border-amber-500/20 bg-amber-500/10 px-3 py-2 text-sm text-amber-100"
|
||||||
|
>
|
||||||
|
{{ store.activeRequest.method }} requests usually do not send a body. The editor stays available, but some servers may ignore it.
|
||||||
|
</div>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
v-model="store.activeRequest.body"
|
v-model="store.activeRequest.body"
|
||||||
placeholder="Request Body (JSON)"
|
:placeholder="requestBodyPlaceholder"
|
||||||
language="json"
|
:language="inferredBodyLanguage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
|
|||||||
@@ -8,23 +8,35 @@ const store = useRequestStore();
|
|||||||
const isCopied = ref(false);
|
const isCopied = ref(false);
|
||||||
const activeTab = ref<'body' | 'headers'>('body');
|
const activeTab = ref<'body' | 'headers'>('body');
|
||||||
|
|
||||||
const isJsonResponse = computed(() => {
|
const responseFormat = computed<'json' | 'xml' | 'html' | 'text'>(() => {
|
||||||
const response = store.activeRequest.response;
|
const response = store.activeRequest.response;
|
||||||
if (!response) return false;
|
if (!response) return 'text';
|
||||||
|
|
||||||
const contentType = response.headers['content-type'] || response.headers['Content-Type'] || '';
|
const contentType = response.headers['content-type'] || response.headers['Content-Type'] || '';
|
||||||
if (contentType.toLowerCase().includes('json')) {
|
const normalizedContentType = contentType.toLowerCase();
|
||||||
return true;
|
|
||||||
|
if (normalizedContentType.includes('json')) {
|
||||||
|
return 'json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedContentType.includes('xml')) {
|
||||||
|
return 'xml';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedContentType.includes('html')) {
|
||||||
|
return 'html';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSON.parse(response.body);
|
JSON.parse(response.body);
|
||||||
return true;
|
return 'json';
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return 'text';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isJsonResponse = computed(() => responseFormat.value === 'json');
|
||||||
|
|
||||||
const formattedHeaders = computed(() => {
|
const formattedHeaders = computed(() => {
|
||||||
const response = store.activeRequest.response;
|
const response = store.activeRequest.response;
|
||||||
if (!response) return '';
|
if (!response) return '';
|
||||||
@@ -83,7 +95,14 @@ const copyToClipboard = async () => {
|
|||||||
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
|
Time: <span class="text-slate-300">{{ store.activeRequest.response.time }}ms</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-slate-500">
|
<div class="text-slate-500">
|
||||||
Size: <span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
|
Size:
|
||||||
|
<span class="text-slate-300">{{ (store.activeRequest.response.size / 1024).toFixed(2) }} KB</span>
|
||||||
|
<span class="ml-1 text-slate-500">
|
||||||
|
(H {{ (store.activeRequest.response.headersSize / 1024).toFixed(2) }} / B {{ (store.activeRequest.response.bodySize / 1024).toFixed(2) }} KB)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-slate-500">
|
||||||
|
Format: <span class="text-slate-300 uppercase">{{ responseFormat }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -120,14 +139,15 @@ const copyToClipboard = async () => {
|
|||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
:model-value="displayedContent"
|
:model-value="displayedContent"
|
||||||
:language="activeTab === 'body' && isJsonResponse ? 'json' : 'text'"
|
:language="activeTab === 'body' && responseFormat === 'json' ? 'json' : 'text'"
|
||||||
:read-only="true"
|
:read-only="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="store.requestError" class="flex-1 flex items-center justify-center p-6">
|
<div v-else-if="store.requestError" class="flex-1 flex items-center justify-center p-6">
|
||||||
<div class="max-w-2xl rounded-xl border border-rose-500/30 bg-rose-500/10 p-4 text-sm text-rose-200">
|
<div class="max-w-2xl rounded-xl border border-rose-500/30 bg-rose-500/10 p-4 text-sm text-rose-200">
|
||||||
{{ store.requestError }}
|
<div class="font-medium uppercase tracking-wide text-[11px] text-rose-300/80">{{ store.requestError.code }}</div>
|
||||||
|
<div class="mt-1">{{ store.requestError.message }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex-1 flex items-center justify-center text-slate-600 text-sm">
|
<div v-else class="flex-1 flex items-center justify-center text-slate-600 text-sm">
|
||||||
|
|||||||
@@ -29,10 +29,17 @@ export interface RequestData {
|
|||||||
body: string;
|
body: string;
|
||||||
time: number;
|
time: number;
|
||||||
size: number;
|
size: number;
|
||||||
|
headersSize: number;
|
||||||
|
bodySize: number;
|
||||||
};
|
};
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RequestError {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
const createEmptyKeyValue = (): KeyValue => ({
|
const createEmptyKeyValue = (): KeyValue => ({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
key: '',
|
key: '',
|
||||||
@@ -99,6 +106,8 @@ const cloneRequest = (req: Partial<RequestData>): RequestData => ({
|
|||||||
body: req.response.body,
|
body: req.response.body,
|
||||||
time: req.response.time,
|
time: req.response.time,
|
||||||
size: req.response.size,
|
size: req.response.size,
|
||||||
|
headersSize: req.response.headersSize ?? 0,
|
||||||
|
bodySize: req.response.bodySize ?? req.response.body.length,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
timestamp: typeof req.timestamp === 'number' ? req.timestamp : Date.now(),
|
timestamp: typeof req.timestamp === 'number' ? req.timestamp : Date.now(),
|
||||||
@@ -128,7 +137,7 @@ export const useRequestStore = defineStore('request', () => {
|
|||||||
// State
|
// State
|
||||||
const history = ref<RequestData[]>([]);
|
const history = ref<RequestData[]>([]);
|
||||||
const activeRequest = ref<RequestData>(createDefaultRequest());
|
const activeRequest = ref<RequestData>(createDefaultRequest());
|
||||||
const requestError = ref<string | null>(null);
|
const requestError = ref<RequestError | null>(null);
|
||||||
|
|
||||||
// Load history from local storage
|
// Load history from local storage
|
||||||
const savedHistory = localStorage.getItem('request_history');
|
const savedHistory = localStorage.getItem('request_history');
|
||||||
@@ -183,8 +192,8 @@ export const useRequestStore = defineStore('request', () => {
|
|||||||
requestError.value = null;
|
requestError.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setRequestError = (message: string | null) => {
|
const setRequestError = (error: RequestError | null) => {
|
||||||
requestError.value = message;
|
requestError.value = error;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch and Save
|
// Watch and Save
|
||||||
|
|||||||
Reference in New Issue
Block a user