More response info

This commit is contained in:
Gregory Schier
2024-01-28 16:02:49 -08:00
parent a54fff93a6
commit 65c023c8b8
16 changed files with 230 additions and 89 deletions

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
"describe": {
"columns": [
{
@@ -64,13 +64,28 @@
"type_info": "Int64"
},
{
"name": "error",
"name": "elapsed_headers",
"ordinal": 12,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "version",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "remote_addr",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 13,
"ordinal": 16,
"type_info": "Text"
}
],
@@ -90,9 +105,12 @@
true,
true,
false,
false,
true,
true,
true,
false
]
},
"hash": "679a519475adeb50abf046114d3c0d1e48e103f2bb11ef47637d7f0b00ed241f"
"hash": "0fa6b56f8c996d14908a56928674b4b35af5fa36f63dc48b9b66ee6dfde78976"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "198bd086ccc87d2e6c24cb1c717f486d3ab58c0c958ede850c018fc266eade87"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE http_responses SET (\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n version,\n remote_addr,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers,\n version,\n remote_addr\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 13
},
"nullable": []
},
"hash": "a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
"describe": {
"columns": [
{
@@ -64,13 +64,28 @@
"type_info": "Int64"
},
{
"name": "error",
"name": "elapsed_headers",
"ordinal": 12,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "version",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "remote_addr",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 13,
"ordinal": 16,
"type_info": "Text"
}
],
@@ -90,9 +105,12 @@
true,
true,
false,
false,
true,
true,
true,
false
]
},
"hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
"hash": "ac38621cd947c3be9ca0d8ea73325fe35c3866d16f6482fc32c23762f112dc83"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
"query": "\n SELECT\n id, model, workspace_id, request_id, updated_at, created_at, url, status,\n status_reason, content_length, body_path, elapsed, elapsed_headers, error,\n version, remote_addr,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
"describe": {
"columns": [
{
@@ -64,13 +64,28 @@
"type_info": "Int64"
},
{
"name": "error",
"name": "elapsed_headers",
"ordinal": 12,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"type_info": "Text"
},
{
"name": "version",
"ordinal": 14,
"type_info": "Text"
},
{
"name": "remote_addr",
"ordinal": 15,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 13,
"ordinal": 16,
"type_info": "Text"
}
],
@@ -90,9 +105,12 @@
true,
true,
false,
false,
true,
true,
true,
false
]
},
"hash": "3d199d371be948211f4a50c869b307f5df60784293c52397d77a187633a406dd"
"hash": "d5e087caa163a0c7bfbbadf07eccb80105501cf5baab706aa6792dfe90af8fc9"
}

View File

@@ -0,0 +1,3 @@
ALTER TABLE http_responses ADD COLUMN elapsed_headers INTEGER NOT NULL DEFAULT 0;
ALTER TABLE http_responses ADD COLUMN remote_addr TEXT;
ALTER TABLE http_responses ADD COLUMN version TEXT;

View File

@@ -1,5 +1,5 @@
use std::fs;
use std::fs::{create_dir_all, File};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
@@ -7,13 +7,13 @@ use std::sync::Arc;
use std::time::Duration;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use log::{error, info, warn};
use reqwest::redirect::Policy;
use reqwest::{multipart, Url};
use sqlx::types::{Json, JsonValue};
use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite};
use sqlx::types::{Json, JsonValue};
use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err};
@@ -27,7 +27,6 @@ pub async fn send_http_request(
pool: &Pool<Sqlite>,
download_path: Option<PathBuf>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
@@ -298,11 +297,13 @@ pub async fn send_http_request(
}
};
let start = std::time::Instant::now();
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.elapsed_headers = start.elapsed().as_millis() as i64;
let response_headers = v.headers().clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
@@ -316,8 +317,25 @@ pub async fn send_http_request(
.collect(),
);
response.url = v.url().to_string();
response.remote_addr = v.remote_addr().map(|a| a.to_string());
response.version = match v.version() {
http::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
http::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
http::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
http::Version::HTTP_2 => Some("HTTP/2".to_string()),
http::Version::HTTP_3 => Some("HTTP/3".to_string()),
_ => None,
};
let content_length = v.content_length();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
response.elapsed = start.elapsed().as_millis() as i64;
// Use content length if available, otherwise use body length
response.content_length = match content_length {
Some(l) => Some(l as i64),
None => Some(body_bytes.len() as i64),
};
{
// Write body to FS
@@ -344,7 +362,6 @@ pub async fn send_http_request(
);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");

View File

@@ -10,7 +10,7 @@ extern crate objc;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{create_dir_all, File, read_to_string};
use std::fs::{create_dir_all, read_to_string, File};
use std::process::exit;
use fern::colors::ColoredLevelConfig;
@@ -18,13 +18,13 @@ use log::{debug, error, info, warn};
use rand::random;
use serde::Serialize;
use serde_json::{json, Value};
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
use sqlx::types::Json;
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent};
use sqlx::{Pool, Sqlite, SqlitePool};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, LogTarget};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex;
@@ -292,9 +292,22 @@ async fn send_request(
None => None,
};
let response = models::create_response(&request.id, 0, "", 0, None, None, None, vec![], pool)
.await
.expect("Failed to create response");
let response = models::create_response(
&request.id,
0,
0,
"",
0,
None,
None,
None,
vec![],
None,
None,
pool,
)
.await
.expect("Failed to create response");
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())

View File

@@ -171,7 +171,10 @@ pub struct HttpResponse {
pub error: Option<String>,
pub url: String,
pub content_length: Option<i64>,
pub version: Option<String>,
pub elapsed: i64,
pub elapsed_headers: i64,
pub remote_addr: Option<String>,
pub status: i64,
pub status_reason: Option<String>,
pub body_path: Option<String>,
@@ -851,15 +854,19 @@ pub async fn delete_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest
Ok(req)
}
#[allow(clippy::too_many_arguments)]
pub async fn create_response(
request_id: &str,
elapsed: i64,
elapsed_headers: i64,
url: &str,
status: i64,
status_reason: Option<&str>,
content_length: Option<i64>,
body_path: Option<&str>,
headers: Vec<HttpResponseHeader>,
version: Option<&str>,
remote_addr: Option<&str>,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
let req = get_request(request_id, pool).await?;
@@ -872,25 +879,31 @@ pub async fn create_response(
request_id,
workspace_id,
elapsed,
elapsed_headers,
url,
status,
status_reason,
content_length,
body_path,
headers
headers,
version,
remote_addr
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
id,
request_id,
req.workspace_id,
elapsed,
elapsed_headers,
url,
status,
status_reason,
content_length,
body_path,
headers_json,
version,
remote_addr,
)
.execute(pool)
.await?;
@@ -975,6 +988,7 @@ pub async fn update_response(
r#"
UPDATE http_responses SET (
elapsed,
elapsed_headers,
url,
status,
status_reason,
@@ -982,10 +996,13 @@ pub async fn update_response(
body_path,
error,
headers,
version,
remote_addr,
updated_at
) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
"#,
response.elapsed,
response.elapsed_headers,
response.url,
response.status,
response.status_reason,
@@ -993,6 +1010,8 @@ pub async fn update_response(
response.body_path,
response.error,
headers_json,
response.version,
response.remote_addr,
response.id,
)
.execute(pool)
@@ -1004,8 +1023,10 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body_path, elapsed, error,
SELECT
id, model, workspace_id, request_id, updated_at, created_at, url, status,
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
version, remote_addr,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE id = ?
@@ -1021,15 +1042,14 @@ pub async fn find_responses(
limit: Option<i64>,
pool: &Pool<Sqlite>,
) -> Result<Vec<HttpResponse>, sqlx::Error> {
let limit_unwrapped = match limit {
Some(l) => l,
None => i64::MAX,
};
let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX);
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body_path, elapsed, error,
SELECT
id, model, workspace_id, request_id, updated_at, created_at, url, status,
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
version, remote_addr,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE request_id = ?
@@ -1050,8 +1070,10 @@ pub async fn find_responses_by_workspace_id(
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body_path, elapsed, error,
SELECT
id, model, workspace_id, request_id, updated_at, created_at, url, status,
status_reason, content_length, body_path, elapsed, elapsed_headers, error,
version, remote_addr,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE workspace_id = ?

View File

@@ -1,26 +1,47 @@
import classNames from 'classnames';
import type { HttpResponse } from '../lib/models';
import { Separator } from './core/Separator';
import { HStack } from './core/Stacks';
interface Props {
headers: HttpResponse['headers'];
response: HttpResponse;
}
export function ResponseHeaders({ headers }: Props) {
export function ResponseHeaders({ response }: Props) {
return (
<dl className="text-xs w-full h-full font-mono overflow-auto">
{headers.map((h, i) => {
return (
<HStack
space={3}
key={i}
className={classNames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')}
>
<dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd>
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>
</HStack>
);
})}
</dl>
<div className="overflow-auto h-full pb-4">
<dl className="text-xs w-full font-mono flex flex-col">
{response.headers.map((h, i) => (
<Row key={i} label={h.name} value={h.value} labelClassName="!text-violet-600" />
))}
</dl>
<Separator className="my-4">Other Info</Separator>
<dl className="text-xs w-full font-mono divide-highlightSecondary">
<Row label="Version" value={response.version} />
<Row label="Remote Address" value={response.remoteAddr} />
<Row label="Status" value={response.status} />
<Row label="Reason" value={response.statusReason} />
<Row label="URL" value={response.url} />
</dl>
</div>
);
}
function Row({
label,
value,
labelClassName,
}: {
label: string;
value: string | number;
labelClassName?: string;
}) {
return (
<HStack space={3} className="py-0.5">
<dd className={classNames(labelClassName, 'w-1/3 text-gray-700 select-text cursor-text')}>
{label}
</dd>
<dt className="w-2/3 select-text cursor-text break-all">{value}</dt>
</HStack>
);
}

View File

@@ -130,7 +130,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
{activeResponse.elapsed > 0 && (
<>
<span>&bull;</span>
<DurationTag millis={activeResponse.elapsed} />
<DurationTag
headers={activeResponse.elapsedHeaders}
total={activeResponse.elapsed}
/>
</>
)}
{!!activeResponse.contentLength && (
@@ -160,7 +163,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
tabListClassName="mt-1.5"
>
<TabContent value="headers">
<ResponseHeaders headers={activeResponse?.headers ?? []} />
<ResponseHeaders response={activeResponse} />
</TabContent>
<TabContent value="body">
{!activeResponse.contentLength ? (

View File

@@ -1,8 +1,17 @@
interface Props {
millis: number;
total: number;
headers: number;
}
export function DurationTag({ millis }: Props) {
export function DurationTag({ total, headers }: Props) {
return (
<span title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}>
{formatMillis(total)}
</span>
);
}
function formatMillis(millis: number) {
let num;
let unit;
@@ -17,9 +26,5 @@ export function DurationTag({ millis }: Props) {
unit = 'ms';
}
return (
<span title={`${millis} milliseconds`}>
{Math.round(num * 10) / 10} {unit}
</span>
);
return `${Math.round(num * 10) / 10} ${unit}`;
}

View File

@@ -15,7 +15,7 @@ export function Separator({
}: Props) {
return (
<div role="separator" className={classNames(className, 'flex items-center')}>
{children && <div className="text-xs text-gray-500 mx-2 whitespace-nowrap">{children}</div>}
{children && <div className="text-xs text-gray-500 mr-2 whitespace-nowrap">{children}</div>}
<div
className={classNames(
variant === 'primary' && 'bg-highlight',

View File

@@ -133,7 +133,10 @@ export interface HttpResponse extends BaseModel {
readonly error: string;
readonly status: number;
readonly elapsed: number;
readonly elapsedHeaders: number;
readonly statusReason: string;
readonly version: string;
readonly remoteAddr: string;
readonly url: string;
readonly headers: HttpHeader[];
}